Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RubyGems #40

Merged
merged 9 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, " "))
}