Skip to content

Commit 360169a

Browse files
authored
Add support for RubyGems (#40)
* Add first pass at rubygems.go * Add countLeadingSpaces func to util * Add rubygems option for lang flag * Add go.mod * Run go fmt * Return recursive call when being rate limited * Add package regardless of presence of version * Add print for JSON unmarshaling issue * Add comment for json unmarshaling error case
1 parent 0bffa50 commit 360169a

File tree

6 files changed

+188
-26
lines changed

6 files changed

+188
-26
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/visma-prodsec/confused
2+
3+
go 1.18

main.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func main() {
2121
verbose := false
2222
filename := ""
2323
safespaces := ""
24-
flag.StringVar(&lang, "l", "npm", "Package repository system. Possible values: \"pip\", \"npm\", \"composer\", \"mvn\"")
24+
flag.StringVar(&lang, "l", "npm", "Package repository system. Possible values: \"pip\", \"npm\", \"composer\", \"mvn\", \"rubygems\"")
2525
flag.StringVar(&safespaces, "s", "", "Comma-separated list of known-secure namespaces. Supports wildcards")
2626
flag.BoolVar(&verbose, "v", false, "Verbose output")
2727
flag.Parse()
@@ -34,18 +34,23 @@ func main() {
3434
}
3535

3636
filename = flag.Args()[0]
37-
if lang == "pip" {
37+
38+
switch lang {
39+
case "pip":
3840
resolver = NewPythonLookup(verbose)
39-
} else if lang == "npm" {
41+
case "npm":
4042
resolver = NewNPMLookup(verbose)
41-
} else if lang == "composer" {
43+
case "composer":
4244
resolver = NewComposerLookup(verbose)
43-
} else if lang == "mvn" {
45+
case "mvn":
4446
resolver = NewMVNLookup(verbose)
45-
} else {
47+
case "rubygems":
48+
resolver = NewRubyGemsLookup(verbose)
49+
default:
4650
fmt.Printf("Unknown package repository system: %s\n", lang)
4751
os.Exit(1)
4852
}
53+
4954
err := resolver.ReadPackagesFromFile(filename)
5055
if err != nil {
5156
fmt.Printf("Encountered an error while trying to read packages from file: %s\n", err)

mvn.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ type MVNLookup struct {
1818
}
1919

2020
type MVNPackage struct {
21-
Group string
21+
Group string
2222
Artifact string
23-
Version string
23+
Version string
2424
}
2525

2626
// NewNPMLookup constructs an `MVNLookup` struct and returns it.
@@ -68,7 +68,7 @@ func (n *MVNLookup) PackagesNotInPublic() []string {
6868
notavail := []string{}
6969
for _, pkg := range n.Packages {
7070
if !n.isAvailableInPublic(pkg, 0) {
71-
notavail = append(notavail, pkg.Group + "/" + pkg.Artifact)
71+
notavail = append(notavail, pkg.Group+"/"+pkg.Artifact)
7272
}
7373
}
7474
return notavail
@@ -86,11 +86,11 @@ func (n *MVNLookup) isAvailableInPublic(pkg MVNPackage, retry int) bool {
8686
return true
8787
}
8888

89-
group := strings.Replace(pkg.Group, ".", "/",-1)
89+
group := strings.Replace(pkg.Group, ".", "/", -1)
9090
if n.Verbose {
91-
fmt.Print("Checking: https://repo1.maven.org/maven2/"+group+"/ ")
91+
fmt.Print("Checking: https://repo1.maven.org/maven2/" + group + "/ ")
9292
}
93-
resp, err := http.Get("https://repo1.maven.org/maven2/"+group+"/")
93+
resp, err := http.Get("https://repo1.maven.org/maven2/" + group + "/")
9494
if err != nil {
9595
fmt.Printf(" [W] Error when trying to request https://repo1.maven.org/maven2/"+group+"/ : %s\n", err)
9696
return false
@@ -114,7 +114,7 @@ func (n *MVNLookup) isAvailableInPublic(pkg MVNPackage, retry int) bool {
114114
fmt.Printf(" [!] Server responded with 429 (Too many requests), throttling and retrying...\n")
115115
time.Sleep(10 * time.Second)
116116
retry = retry + 1
117-
n.isAvailableInPublic(pkg, retry)
117+
return n.isAvailableInPublic(pkg, retry)
118118
}
119119
return false
120-
}
120+
}

npm.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ type NpmResponse struct {
2828
}
2929

3030
type NpmResponseUnpublished struct {
31-
Maintainers []struct {
32-
Email string `json:"email"`
33-
Name string `json:"name"`
34-
} `json:"maintainers"`
35-
Name string `json:"name"`
36-
Tags struct {
37-
Latest string `json:"latest"`
38-
} `json:"tags"`
39-
Time time.Time `json:"time"`
40-
Versions []string `json:"versions"`
31+
Maintainers []struct {
32+
Email string `json:"email"`
33+
Name string `json:"name"`
34+
} `json:"maintainers"`
35+
Name string `json:"name"`
36+
Tags struct {
37+
Latest string `json:"latest"`
38+
} `json:"tags"`
39+
Time time.Time `json:"time"`
40+
Versions []string `json:"versions"`
4141
}
4242

4343
// NotAvailable returns true if the package has its all versions unpublished making it susceptible for takeover
@@ -53,7 +53,7 @@ type NPMLookup struct {
5353
}
5454

5555
type NPMPackage struct {
56-
Name string
56+
Name string
5757
Version string
5858
}
5959

@@ -155,7 +155,7 @@ func (n *NPMLookup) isAvailableInPublic(pkgname string, retry int) bool {
155155
fmt.Printf(" [!] Server responded with 429 (Too many requests), throttling and retrying...\n")
156156
time.Sleep(10 * time.Second)
157157
retry = retry + 1
158-
n.isAvailableInPublic(pkgname, retry)
158+
return n.isAvailableInPublic(pkgname, retry)
159159
}
160160
return false
161161
}

rubygems.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"os"
10+
"strings"
11+
"time"
12+
)
13+
14+
type Gem struct {
15+
Remote string
16+
IsLocal bool
17+
IsRubyGems bool
18+
IsTransitive bool
19+
Name string
20+
Version string
21+
}
22+
23+
type RubyGemsResponse struct {
24+
Name string `json:"name"`
25+
Downloads int64 `json:"downloads"`
26+
Version string `json:"version"`
27+
}
28+
29+
// RubyGemsLookup represents a collection of rubygems packages to be tested for dependency confusion.
30+
type RubyGemsLookup struct {
31+
Packages []Gem
32+
Verbose bool
33+
}
34+
35+
// NewRubyGemsLookup constructs an `RubyGemsLookup` struct and returns it.
36+
func NewRubyGemsLookup(verbose bool) PackageResolver {
37+
return &RubyGemsLookup{Packages: []Gem{}, Verbose: verbose}
38+
}
39+
40+
// ReadPackagesFromFile reads package information from a Gemfile.lock file
41+
//
42+
// Returns any errors encountered
43+
func (r *RubyGemsLookup) ReadPackagesFromFile(filename string) error {
44+
file, err := os.Open(filename)
45+
if err != nil {
46+
return err
47+
}
48+
defer file.Close()
49+
scanner := bufio.NewScanner(file)
50+
var remote string
51+
for scanner.Scan() {
52+
line := scanner.Text()
53+
trimmedLine := strings.TrimSpace(line)
54+
if strings.HasPrefix(trimmedLine, "remote:") {
55+
remote = strings.TrimSpace(strings.SplitN(trimmedLine, ":", 2)[1])
56+
} else if trimmedLine == "revision:" {
57+
continue
58+
} else if trimmedLine == "branch:" {
59+
continue
60+
} else if trimmedLine == "GIT" {
61+
continue
62+
} else if trimmedLine == "GEM" {
63+
continue
64+
} else if trimmedLine == "PATH" {
65+
continue
66+
} else if trimmedLine == "PLATFORMS" {
67+
break
68+
} else if trimmedLine == "specs:" {
69+
continue
70+
} else if len(trimmedLine) > 0 {
71+
parts := strings.SplitN(trimmedLine, " ", 2)
72+
name := strings.TrimSpace(parts[0])
73+
var version string
74+
if len(parts) > 1 {
75+
version = strings.TrimRight(strings.TrimLeft(parts[1], "("), ")")
76+
} else {
77+
version = ""
78+
}
79+
r.Packages = append(r.Packages, Gem{
80+
Remote: remote,
81+
IsLocal: !strings.HasPrefix(remote, "http"),
82+
IsRubyGems: strings.HasPrefix(remote, "https://rubygems.org"),
83+
IsTransitive: countLeadingSpaces(line) == 6,
84+
Name: name,
85+
Version: version,
86+
})
87+
} else {
88+
continue
89+
}
90+
}
91+
return nil
92+
}
93+
94+
// PackagesNotInPublic determines if a rubygems package does not exist in the public rubygems package repository.
95+
//
96+
// Returns a slice of strings with any rubygem packages not in the public rubygems package repository
97+
func (r *RubyGemsLookup) PackagesNotInPublic() []string {
98+
notavail := []string{}
99+
for _, pkg := range r.Packages {
100+
if pkg.IsLocal || !pkg.IsRubyGems {
101+
continue
102+
}
103+
if !r.isAvailableInPublic(pkg.Name, 0) {
104+
notavail = append(notavail, pkg.Name)
105+
}
106+
}
107+
return notavail
108+
}
109+
110+
// isAvailableInPublic determines if a rubygems package exists in the public rubygems.org package repository.
111+
//
112+
// Returns true if the package exists in the public rubygems package repository.
113+
func (r *RubyGemsLookup) isAvailableInPublic(pkgname string, retry int) bool {
114+
if retry > 3 {
115+
fmt.Printf(" [W] Maximum number of retries exhausted for package: %s\n", pkgname)
116+
return false
117+
}
118+
url := fmt.Sprintf("https://rubygems.org/api/v1/gems/%s.json", pkgname)
119+
if r.Verbose {
120+
fmt.Printf("Checking: %s : \n", url)
121+
}
122+
resp, err := http.Get(url)
123+
if err != nil {
124+
fmt.Printf(" [W] Error when trying to request %s: %s\n", url, err)
125+
return false
126+
}
127+
defer resp.Body.Close()
128+
if r.Verbose {
129+
fmt.Printf("%s\n", resp.Status)
130+
}
131+
if resp.StatusCode == http.StatusOK {
132+
rubygemsResp := RubyGemsResponse{}
133+
body, _ := ioutil.ReadAll(resp.Body)
134+
err = json.Unmarshal(body, &rubygemsResp)
135+
if err != nil {
136+
// This shouldn't ever happen because if it doesn't return JSON, it likely has returned
137+
// a non-200 status code.
138+
fmt.Printf(" [W] Error when trying to unmarshal response from %s: %s\n", url, err)
139+
return false
140+
}
141+
return true
142+
} else if resp.StatusCode == 429 {
143+
fmt.Printf(" [!] Server responded with 429 (Too many requests), throttling and retrying...\n")
144+
time.Sleep(10 * time.Second)
145+
retry = retry + 1
146+
return r.isAvailableInPublic(pkgname, retry)
147+
}
148+
return false
149+
}

util.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package main
22

3+
import "strings"
4+
35
func inSlice(what rune, where []rune) bool {
46
for _, r := range where {
57
if r == what {
@@ -9,3 +11,6 @@ func inSlice(what rune, where []rune) bool {
911
return false
1012
}
1113

14+
func countLeadingSpaces(line string) int {
15+
return len(line) - len(strings.TrimLeft(line, " "))
16+
}

0 commit comments

Comments
 (0)