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

feat: Add new data structures for an abstract representation of projects/findings #75

Merged
merged 11 commits into from
Aug 1, 2023
Merged
1 change: 1 addition & 0 deletions querying/datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package querying
20 changes: 20 additions & 0 deletions querying/ecosystems.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package querying

type FindingEcosystemType string

const (
FindingEcosystemApt FindingEcosystemType = "apt"
FindingEcosystemCSharp FindingEcosystemType = "csharp"
FindingEcosystemDart FindingEcosystemType = "dart"
FindingEcosystemErlang FindingEcosystemType = "erlang"
FindingEcosystemGHA FindingEcosystemType = "gha" // GitHub Actions
FindingEcosystemGo FindingEcosystemType = "go"
FindingEcosystemJava FindingEcosystemType = "java"
FindingEcosystemJS FindingEcosystemType = "js" // Includes TypeScript
FindingEcosystemPHP FindingEcosystemType = "php"
FindingEcosystemPython FindingEcosystemType = "python"
FindingEcosystemRPM FindingEcosystemType = "rpm"
FindingEcosystemRuby FindingEcosystemType = "ruby"
FindingEcosystemRust FindingEcosystemType = "rust"
FindingEcosystemSwift FindingEcosystemType = "swift"
)
20 changes: 20 additions & 0 deletions querying/finding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package querying

import "sync"

type FindingIdentifierType string
type FindingIdentifierMap map[FindingIdentifierType]string

const (
FindingIdentifierCVE FindingIdentifierType = "cve"
FindingIdentifierGHSA FindingIdentifierType = "ghsa"
)

type Finding struct {
Identifiers FindingIdentifierMap
Ecosystem FindingEcosystemType
Severity FindingSeverityType
Description string
PackageName string
mu sync.Mutex
}
112 changes: 112 additions & 0 deletions querying/project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package querying

import (
"regexp"
"strings"
"sync"

"golang.org/x/exp/maps"
)

type ProjectCollection struct {
Projects []*Project
mu sync.Mutex
}

type Project struct {
Name string
Findings []*Finding
Links map[string]string
mu sync.Mutex
}

// NewProject returns a new, empty project with no links or findings.
func NewProject(name string) *Project {
return &Project{
Name: name,
Findings: []*Finding{},
Links: map[string]string{},
}
}

// NewProjectCollection returns a new, empty ProjectCollection object.
func NewProjectCollection() *ProjectCollection {
return &ProjectCollection{
Projects: []*Project{},
}
}

// normalizeProjectName converts a project name to a standard normalized format.
// This means stripping out all characters which are not: Letters, numbers,
// spaces, hyphens, or underscores. This is done via a regex, which includes the
// unicode character classes (\p) of `{L}` to represent all letters, and `{N}`
// to represent all numbers.
// Once all undesirable characters have been stripped, both spaces and hyphens
// are converted to underscores, and the resulting string is lower-cased.
tarkatronic marked this conversation as resolved.
Show resolved Hide resolved
//
// For example:
//
// $$$ This Project is MONEY! $$$
//
// will be normalized to
//
// this_project_is_money
func normalizeProjectName(name string) string {
unacceptableChars := regexp.MustCompile(`[^\p{L}\p{N} \-\_]+`)
developerDemetri marked this conversation as resolved.
Show resolved Hide resolved
replacer := strings.NewReplacer(
" ", "_",
"-", "_",
)
return replacer.Replace(
strings.TrimSpace(
unacceptableChars.ReplaceAllString(
strings.ToLower(name), "",
),
),
)
}

// GetProject returns the project with the specified name from the collection.
// If such a project does not yet exist, it is created and added to the collection.
func (c *ProjectCollection) GetProject(name string) *Project {
c.mu.Lock()
defer c.mu.Unlock()
name = normalizeProjectName(name)
for _, proj := range c.Projects {
if normalizeProjectName(proj.Name) == name {
return proj
}
}
// If we make it past the loop, no existing project was found with this name
newProj := NewProject(name)
c.Projects = append(c.Projects, newProj)
return newProj
}

// GetFinding returns the specified finding from the project, based on the identifiers.
// If such a finding does not yet exist, it is created and added to the project.
func (p *Project) GetFinding(identifiers FindingIdentifierMap) *Finding {
var result *Finding
p.mu.Lock()
defer p.mu.Unlock()
for _, finding := range p.Findings {
for idType, id := range finding.Identifiers {
val, ok := identifiers[idType]
if ok && val == id {
result = finding
break
}
}
}
if result == nil {
result = &Finding{
Identifiers: identifiers,
}
p.Findings = append(p.Findings, result)
} else {
result.mu.Lock()
defer result.mu.Unlock()
maps.Copy(result.Identifiers, identifiers)
}
return result
}
70 changes: 70 additions & 0 deletions querying/project_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package querying_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/underdog-tech/vulnbot/querying"
)

func TestGetProjectAddsToCollection(t *testing.T) {
projects := querying.NewProjectCollection()
assert.Len(t, projects.Projects, 0)
proj := projects.GetProject("Improbability Drive")
assert.Len(t, projects.Projects, 1)
assert.Equal(t, proj, projects.Projects[0])
}

func TestProjectNameIsNormalized(t *testing.T) {
projects := querying.NewProjectCollection()
proj := projects.GetProject("Heart-of-Gold: Improbability Drive!")
assert.Equal(t, "heart_of_gold_improbability_drive", proj.Name)
}

func TestProjectsAreNotDuplicated(t *testing.T) {
projects := querying.NewProjectCollection()
proj1 := projects.GetProject("Improbability Drive")
proj2 := projects.GetProject("Improbability Drive")
assert.Len(t, projects.Projects, 1)
assert.Equal(t, proj1, proj2)
}

func TestGetFindingAddsToProject(t *testing.T) {
project := querying.NewProject("Improbability Drive")
assert.Len(t, project.Findings, 0)
finding := project.GetFinding(
querying.FindingIdentifierMap{
querying.FindingIdentifierCVE: "CVE-42",
},
)
assert.Len(t, project.Findings, 1)
assert.Equal(t, finding, project.Findings[0])
}

func TestFindingsAreNotDuplicated(t *testing.T) {
project := querying.NewProject("Improbability Drive")
identifiers := querying.FindingIdentifierMap{
querying.FindingIdentifierCVE: "CVE-42",
}
finding1 := project.GetFinding(identifiers)
finding2 := project.GetFinding(identifiers)
assert.Len(t, project.Findings, 1)
assert.Equal(t, finding1, finding2)
}

func TestFindingIdentifiersAreMerged(t *testing.T) {
project := querying.NewProject("Improbability Drive")
id_single := querying.FindingIdentifierMap{
querying.FindingIdentifierCVE: "CVE-42",
}
id_multi := querying.FindingIdentifierMap{
querying.FindingIdentifierCVE: "CVE-42",
querying.FindingIdentifierGHSA: "GHSA-4242",
}
finding1 := project.GetFinding(id_single)
finding2 := project.GetFinding(id_multi)
assert.Len(t, project.Findings, 1)
assert.Equal(t, finding1, finding2)
assert.Equal(t, finding1.Identifiers, id_multi)
assert.Equal(t, project.Findings[0].Identifiers, id_multi)
}
12 changes: 12 additions & 0 deletions querying/severities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package querying

type FindingSeverityType uint8

const (
FindingSeverityCritical FindingSeverityType = iota
FindingSeverityHigh
FindingSeverityModerate
FindingSeverityLow
FindingSeverityInfo
FindingSeverityUndefined
)