From d60ed1a7ebc0e33a17bde169baaebd707ef58d54 Mon Sep 17 00:00:00 2001 From: Eddie Knight Date: Wed, 1 Jan 2025 12:06:56 -0600 Subject: [PATCH] feat: Added go package to support v2 ingestion Signed-off-by: Eddie Knight --- go.mod | 5 ++ go.sum | 4 + {python => v1}/Dockerfile | 0 {python => v1}/README.md | 0 {python => v1}/requirements.txt | 0 {python => v1}/si-validator.py | 0 v2/README.md | 0 v2/si/import.go | 85 +++++++++++++++++++ v2/si/import_test.go | 28 ++++++ v2/si/security-insights.go | 146 ++++++++++++++++++++++++++++++++ 10 files changed, 268 insertions(+) create mode 100644 go.mod create mode 100644 go.sum rename {python => v1}/Dockerfile (100%) rename {python => v1}/README.md (100%) rename {python => v1}/requirements.txt (100%) rename {python => v1}/si-validator.py (100%) create mode 100644 v2/README.md create mode 100644 v2/si/import.go create mode 100644 v2/si/import_test.go create mode 100644 v2/si/security-insights.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b1ce33b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/ossf/si-tooling + +go 1.23 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/python/Dockerfile b/v1/Dockerfile similarity index 100% rename from python/Dockerfile rename to v1/Dockerfile diff --git a/python/README.md b/v1/README.md similarity index 100% rename from python/README.md rename to v1/README.md diff --git a/python/requirements.txt b/v1/requirements.txt similarity index 100% rename from python/requirements.txt rename to v1/requirements.txt diff --git a/python/si-validator.py b/v1/si-validator.py similarity index 100% rename from python/si-validator.py rename to v1/si-validator.py diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..e69de29 diff --git a/v2/si/import.go b/v2/si/import.go new file mode 100644 index 0000000..08340db --- /dev/null +++ b/v2/si/import.go @@ -0,0 +1,85 @@ +package si + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "gopkg.in/yaml.v3" +) + +type FileAPIResponse struct { + ByteContent []byte `json:"content"` + SHA string `json:"sha"` +} + +type SIBuilder struct { + TargetSI SecurityInsights + ParentSI SecurityInsights +} + +func makeApiCall(endpoint, token string) (bytes []byte, err error) { + request, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return + } + if token != "" { + request.Header.Set("Authorization", "Bearer "+token) + } + client := &http.Client{} + response, err := client.Do(request) + if err != nil { + err = fmt.Errorf("error making http call: %s", err.Error()) + return + } + if response.StatusCode != 200 { + err = fmt.Errorf("unexpected response: %s", response.Status) + return + } + return io.ReadAll(response.Body) +} + +func getGitHubSourceFile(endpoint string) (response FileAPIResponse, err error) { + responseData, err := makeApiCall("https://api.github.com/"+endpoint, "") + if err != nil { + return + } + err = json.Unmarshal(responseData, &response) + return +} + +func Read(owner, repo, path string) (si SecurityInsights, err error) { + var builder SIBuilder + // Get Target SI + response, err := getGitHubSourceFile(fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)) + if err != nil { + err = fmt.Errorf("error reading target SI: %s", err.Error()) + return + } + + err = yaml.Unmarshal(response.ByteContent, &builder.TargetSI) + if err != nil { + err = fmt.Errorf("error unmarshalling target SI: %s", err.Error()) + return + } + + // check for parent SI, read if exists + if builder.TargetSI.Header.ProjectSISource != "" { + response, err = getGitHubSourceFile(builder.TargetSI.Header.ProjectSISource) + if err != nil { + err = fmt.Errorf("error reading parent SI: %s", err.Error()) + return + } + err = yaml.Unmarshal(response.ByteContent, &builder.ParentSI) + if err != nil { + err = fmt.Errorf("error unmarshalling parent SI: %s", err.Error()) + return + } + } + + // Override target SI project data with contents of parent SI project data + builder.TargetSI.Project = builder.ParentSI.Project + + return builder.TargetSI, nil +} diff --git a/v2/si/import_test.go b/v2/si/import_test.go new file mode 100644 index 0000000..84e8d09 --- /dev/null +++ b/v2/si/import_test.go @@ -0,0 +1,28 @@ +package si + +import ( + "fmt" + "testing" +) + +func TestRead(t *testing.T) { + testData := []struct { + owner string + repo string + path string + }{ + {"ossf", "security-insights-spec", ".github/security-insights.yml"}, + } + + for _, tt := range testData { + t.Run(fmt.Sprintf("Read(%s, %s, %s)", tt.owner, tt.repo, tt.path), func(t *testing.T) { + // TODO: Add real test cases + out, err := Read(tt.owner, tt.repo, tt.path) + if err != nil { + t.Errorf("Read() error = %v", err) + return + } + fmt.Print(out) + }) + } +} diff --git a/v2/si/security-insights.go b/v2/si/security-insights.go new file mode 100644 index 0000000..040653f --- /dev/null +++ b/v2/si/security-insights.go @@ -0,0 +1,146 @@ +package si + +type SecurityInsights struct { + Header Header `yaml:"header"` + Project Project `yaml:"project"` + Repository Repository `yaml:"repository"` +} + +type Header struct { + LastReviewed string `yaml:"last-reviewed"` + LastUpdated string `yaml:"last-updated"` + SchemaVersion string `yaml:"schema-version"` + URL string `yaml:"url"` + Comment string `yaml:"comment"` + ProjectSISource string `yaml:"project-si-source"` +} + +type Assessment struct { + Comment string `yaml:"comment"` + Name string `yaml:"name"` + Evidence string `yaml:"evidence"` + Date string `yaml:"date"` +} + +type Attestation struct { + Name string `yaml:"name"` + Location string `yaml:"location"` + PredicateURI string `yaml:"predicate-uri"` + Comment string `yaml:"comment"` +} + +type Contact struct { + Name string `yaml:"name"` + Primary bool `yaml:"primary"` + Affiliation string `yaml:"affiliation"` + Email string `yaml:"email"` + Social string `yaml:"social"` +} + +type License struct { + URL string `yaml:"url"` + Expression string `yaml:"expression"` +} + +type Link struct { + URI string `yaml:"uri"` + Comment string `yaml:"comment"` +} + +type Project struct { + Name string `yaml:"name"` + Homepage string `yaml:"homepage"` + Roadmap string `yaml:"roadmap"` + Funding string `yaml:"funding"` + Administrators []Contact `yaml:"administrators"` + Repositories []Repo `yaml:"repositories"` + Vulnerability VulnReport `yaml:"vulnerability-reporting"` + Documentation Docs `yaml:"documentation"` +} + +type Repo struct { + Name string `yaml:"name"` + Comment string `yaml:"comment"` + URL string `yaml:"url"` +} + +type VulnReport struct { + ReportsAccepted bool `yaml:"reports-accepted"` + BugBountyAvailable bool `yaml:"bug-bounty-available"` + BugBountyProgram string `yaml:"bug-bounty-program"` + Contact Contact `yaml:"contact"` + Comment string `yaml:"comment"` + SecurityPolicy string `yaml:"security-policy"` + PGPKey string `yaml:"pgp-key"` + InScope []string `yaml:"in-scope"` + OutOfScope []string `yaml:"out-of-scope"` +} + +type Docs struct { + DetailedGuide string `yaml:"detailed-guide"` + CodeOfConduct string `yaml:"code-of-conduct"` + QuickstartGuide string `yaml:"quickstart-guide"` + ReleaseProcess string `yaml:"release-process"` + SignatureVerification string `yaml:"signature-verification"` +} + +type Repository struct { + Status string `yaml:"status"` + URL string `yaml:"url"` + AcceptsChangeRequest bool `yaml:"accepts-change-request"` + AcceptsAutomatedChangeRequest bool `yaml:"accepts-automated-change-request"` + BugFixesOnly bool `yaml:"bug-fixes-only"` + NoThirdPartyPackages bool `yaml:"no-third-party-packages"` + CoreTeam []Contact `yaml:"core-team"` + License License `yaml:"license"` + Security SecurityInfo `yaml:"security"` + Documentation Docs `yaml:"documentation"` + Release Release `yaml:"release"` +} + +type SecurityInfo struct { + Assessments Assessments `yaml:"assessments"` + Champions []Contact `yaml:"champions"` + Tools []Tool `yaml:"tools"` +} + +type Assessments struct { + Self Assessment `yaml:"self"` + ThirdParty []Assessment `yaml:"third-party"` +} + +type Tool struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Version string `yaml:"version"` + Comment string `yaml:"comment"` + Rulesets []string `yaml:"rulesets"` + Integration Integration `yaml:"integration"` + Results Results `yaml:"results"` +} + +type Integration struct { + Adhoc bool `yaml:"adhoc"` + CI bool `yaml:"ci"` + Release bool `yaml:"release"` +} + +type Results struct { + Adhoc Attestation `yaml:"adhoc"` + CI Attestation `yaml:"ci"` + Release Attestation `yaml:"release"` +} + +type Release struct { + AutomatedPipeline bool `yaml:"automated-pipeline"` + DistributionPoints []Link `yaml:"distribution-points"` + Changelog string `yaml:"changelog"` + License License `yaml:"license"` + Attestations []Attestation `yaml:"attestations"` +} + +type SIHeader struct { + SchemaVersion string `yaml:"schema-version"` + ChangeLogURL string `yaml:"changelog"` + LicenseURL string `yaml:"license"` +}