Skip to content

Commit

Permalink
Add support for RubyGems (#40)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mrecachinas committed Oct 28, 2022
1 parent 0bffa50 commit 360169a
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 26 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/visma-prodsec/confused

go 1.18
17 changes: 11 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func main() {
verbose := false
filename := ""
safespaces := ""
flag.StringVar(&lang, "l", "npm", "Package repository system. Possible values: \"pip\", \"npm\", \"composer\", \"mvn\"")
flag.StringVar(&lang, "l", "npm", "Package repository system. Possible values: \"pip\", \"npm\", \"composer\", \"mvn\", \"rubygems\"")
flag.StringVar(&safespaces, "s", "", "Comma-separated list of known-secure namespaces. Supports wildcards")
flag.BoolVar(&verbose, "v", false, "Verbose output")
flag.Parse()
Expand All @@ -34,18 +34,23 @@ func main() {
}

filename = flag.Args()[0]
if lang == "pip" {

switch lang {
case "pip":
resolver = NewPythonLookup(verbose)
} else if lang == "npm" {
case "npm":
resolver = NewNPMLookup(verbose)
} else if lang == "composer" {
case "composer":
resolver = NewComposerLookup(verbose)
} else if lang == "mvn" {
case "mvn":
resolver = NewMVNLookup(verbose)
} else {
case "rubygems":
resolver = NewRubyGemsLookup(verbose)
default:
fmt.Printf("Unknown package repository system: %s\n", lang)
os.Exit(1)
}

err := resolver.ReadPackagesFromFile(filename)
if err != nil {
fmt.Printf("Encountered an error while trying to read packages from file: %s\n", err)
Expand Down
16 changes: 8 additions & 8 deletions mvn.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ type MVNLookup struct {
}

type MVNPackage struct {
Group string
Group string
Artifact string
Version string
Version string
}

// NewNPMLookup constructs an `MVNLookup` struct and returns it.
Expand Down Expand Up @@ -68,7 +68,7 @@ func (n *MVNLookup) PackagesNotInPublic() []string {
notavail := []string{}
for _, pkg := range n.Packages {
if !n.isAvailableInPublic(pkg, 0) {
notavail = append(notavail, pkg.Group + "/" + pkg.Artifact)
notavail = append(notavail, pkg.Group+"/"+pkg.Artifact)
}
}
return notavail
Expand All @@ -86,11 +86,11 @@ func (n *MVNLookup) isAvailableInPublic(pkg MVNPackage, retry int) bool {
return true
}

group := strings.Replace(pkg.Group, ".", "/",-1)
group := strings.Replace(pkg.Group, ".", "/", -1)
if n.Verbose {
fmt.Print("Checking: https://repo1.maven.org/maven2/"+group+"/ ")
fmt.Print("Checking: https://repo1.maven.org/maven2/" + group + "/ ")
}
resp, err := http.Get("https://repo1.maven.org/maven2/"+group+"/")
resp, err := http.Get("https://repo1.maven.org/maven2/" + group + "/")
if err != nil {
fmt.Printf(" [W] Error when trying to request https://repo1.maven.org/maven2/"+group+"/ : %s\n", err)
return false
Expand All @@ -114,7 +114,7 @@ func (n *MVNLookup) isAvailableInPublic(pkg MVNPackage, retry int) bool {
fmt.Printf(" [!] Server responded with 429 (Too many requests), throttling and retrying...\n")
time.Sleep(10 * time.Second)
retry = retry + 1
n.isAvailableInPublic(pkg, retry)
return n.isAvailableInPublic(pkg, retry)
}
return false
}
}
24 changes: 12 additions & 12 deletions npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ type NpmResponse struct {
}

type NpmResponseUnpublished struct {
Maintainers []struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"maintainers"`
Name string `json:"name"`
Tags struct {
Latest string `json:"latest"`
} `json:"tags"`
Time time.Time `json:"time"`
Versions []string `json:"versions"`
Maintainers []struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"maintainers"`
Name string `json:"name"`
Tags struct {
Latest string `json:"latest"`
} `json:"tags"`
Time time.Time `json:"time"`
Versions []string `json:"versions"`
}

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

type NPMPackage struct {
Name string
Name string
Version string
}

Expand Down Expand Up @@ -155,7 +155,7 @@ func (n *NPMLookup) isAvailableInPublic(pkgname string, retry int) bool {
fmt.Printf(" [!] Server responded with 429 (Too many requests), throttling and retrying...\n")
time.Sleep(10 * time.Second)
retry = retry + 1
n.isAvailableInPublic(pkgname, retry)
return n.isAvailableInPublic(pkgname, retry)
}
return false
}
Expand Down
149 changes: 149 additions & 0 deletions rubygems.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package main

import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)

type Gem struct {
Remote string
IsLocal bool
IsRubyGems bool
IsTransitive bool
Name string
Version string
}

type RubyGemsResponse struct {
Name string `json:"name"`
Downloads int64 `json:"downloads"`
Version string `json:"version"`
}

// RubyGemsLookup represents a collection of rubygems packages to be tested for dependency confusion.
type RubyGemsLookup struct {
Packages []Gem
Verbose bool
}

// NewRubyGemsLookup constructs an `RubyGemsLookup` struct and returns it.
func NewRubyGemsLookup(verbose bool) PackageResolver {
return &RubyGemsLookup{Packages: []Gem{}, Verbose: verbose}
}

// ReadPackagesFromFile reads package information from a Gemfile.lock file
//
// Returns any errors encountered
func (r *RubyGemsLookup) ReadPackagesFromFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var remote string
for scanner.Scan() {
line := scanner.Text()
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "remote:") {
remote = strings.TrimSpace(strings.SplitN(trimmedLine, ":", 2)[1])
} else if trimmedLine == "revision:" {
continue
} else if trimmedLine == "branch:" {
continue
} else if trimmedLine == "GIT" {
continue
} else if trimmedLine == "GEM" {
continue
} else if trimmedLine == "PATH" {
continue
} else if trimmedLine == "PLATFORMS" {
break
} else if trimmedLine == "specs:" {
continue
} else if len(trimmedLine) > 0 {
parts := strings.SplitN(trimmedLine, " ", 2)
name := strings.TrimSpace(parts[0])
var version string
if len(parts) > 1 {
version = strings.TrimRight(strings.TrimLeft(parts[1], "("), ")")
} else {
version = ""
}
r.Packages = append(r.Packages, Gem{
Remote: remote,
IsLocal: !strings.HasPrefix(remote, "http"),
IsRubyGems: strings.HasPrefix(remote, "https://rubygems.org"),
IsTransitive: countLeadingSpaces(line) == 6,
Name: name,
Version: version,
})
} else {
continue
}
}
return nil
}

// PackagesNotInPublic determines if a rubygems package does not exist in the public rubygems package repository.
//
// Returns a slice of strings with any rubygem packages not in the public rubygems package repository
func (r *RubyGemsLookup) PackagesNotInPublic() []string {
notavail := []string{}
for _, pkg := range r.Packages {
if pkg.IsLocal || !pkg.IsRubyGems {
continue
}
if !r.isAvailableInPublic(pkg.Name, 0) {
notavail = append(notavail, pkg.Name)
}
}
return notavail
}

// isAvailableInPublic determines if a rubygems package exists in the public rubygems.org package repository.
//
// Returns true if the package exists in the public rubygems package repository.
func (r *RubyGemsLookup) isAvailableInPublic(pkgname string, retry int) bool {
if retry > 3 {
fmt.Printf(" [W] Maximum number of retries exhausted for package: %s\n", pkgname)
return false
}
url := fmt.Sprintf("https://rubygems.org/api/v1/gems/%s.json", pkgname)
if r.Verbose {
fmt.Printf("Checking: %s : \n", url)
}
resp, err := http.Get(url)
if err != nil {
fmt.Printf(" [W] Error when trying to request %s: %s\n", url, err)
return false
}
defer resp.Body.Close()
if r.Verbose {
fmt.Printf("%s\n", resp.Status)
}
if resp.StatusCode == http.StatusOK {
rubygemsResp := RubyGemsResponse{}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &rubygemsResp)
if err != nil {
// This shouldn't ever happen because if it doesn't return JSON, it likely has returned
// a non-200 status code.
fmt.Printf(" [W] Error when trying to unmarshal response from %s: %s\n", url, err)
return false
}
return true
} else if resp.StatusCode == 429 {
fmt.Printf(" [!] Server responded with 429 (Too many requests), throttling and retrying...\n")
time.Sleep(10 * time.Second)
retry = retry + 1
return r.isAvailableInPublic(pkgname, retry)
}
return false
}
5 changes: 5 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package main

import "strings"

func inSlice(what rune, where []rune) bool {
for _, r := range where {
if r == what {
Expand All @@ -9,3 +11,6 @@ func inSlice(what rune, where []rune) bool {
return false
}

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

0 comments on commit 360169a

Please sign in to comment.