diff --git a/Makefile b/Makefile index c640973..afc3eff 100644 --- a/Makefile +++ b/Makefile @@ -23,4 +23,14 @@ covcheck: test-cov exit 0; \ fi -PHONY: test-cov covcheck pkgdocs +PHONY: test-cov covcheck pkgdocs check-spec-examples + +# Confirm the spec examples in test_data match the published Security Insights release +SPEC_VERSION ?= v2.2.0 +check-spec-examples: + @echo "Comparing test_data/spec-$(SPEC_VERSION) with the Security Insights $(SPEC_VERSION) examples ..." + @for f in v2/si/test_data/spec-$(SPEC_VERSION)/*.yml; do \ + curl -sfL "https://raw.githubusercontent.com/ossf/security-insights/$(SPEC_VERSION)/examples/$$(basename $$f)" | diff -q - "$$f" >/dev/null \ + || { echo "$$f differs from the $(SPEC_VERSION) example"; exit 1; }; \ + done + @echo "All spec examples match." diff --git a/v2/si/generated_types.go b/v2/si/generated_types.go index c52a285..233b8bf 100644 --- a/v2/si/generated_types.go +++ b/v2/si/generated_types.go @@ -174,10 +174,10 @@ type VulnerabilityReporting struct { PGPKey *URL `json:"pgp-key,omitempty"` // A list of issues or components that are covered by the vulnerability reporting process. - InScope *URL `json:"in-scope,omitempty"` + InScope []string `json:"in-scope,omitempty"` // A list of issues or components not covered by the vulnerability reporting process. - OutOfScope *URL `json:"out-of-scope,omitempty"` + OutOfScope []string `json:"out-of-scope,omitempty"` } // Project describes the overall project, including basic info, documentation links, repositories, vulnerability reporting, and security details. diff --git a/v2/si/spec_examples_test.go b/v2/si/spec_examples_test.go new file mode 100644 index 0000000..3754131 --- /dev/null +++ b/v2/si/spec_examples_test.go @@ -0,0 +1,79 @@ +package si + +import ( + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The files in test_data/spec-v2.2.0 are unmodified copies of the examples +// published with Security Insights v2.2.0. Run `make check-spec-examples` to +// confirm they still match. +const specExamplesDir = "test_data/spec-v2.2.0/" + +// specParentURL is the project-si-source in the reuse example. +const specParentURL = "https://raw.githubusercontent.com/example/repo/refs/heads/main/security-insights.yml" + +func specExample(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(specExamplesDir + name) + require.NoError(t, err) + return data +} + +func TestLoadSpecExamples(t *testing.T) { + t.Run("minimum", func(t *testing.T) { + si, err := Load(specExample(t, "example-minimum.yml")) + require.NoError(t, err) + require.NotNil(t, si.Project) + require.NotNil(t, si.Repository) + assert.Equal(t, "FooBar", si.Project.Name) + assert.Len(t, si.Project.Repositories, 1) + assert.Equal(t, "active", si.Repository.Status) + }) + + t.Run("full", func(t *testing.T) { + si, err := Load(specExample(t, "example-full.yml")) + require.NoError(t, err) + require.NotNil(t, si.Project) + require.NotNil(t, si.Repository) + assert.Equal(t, "FooBar", si.Project.Name) + assert.Len(t, si.Project.Repositories, 2) + assert.Equal(t, []string{"broken access control", "other"}, si.Project.VulnerabilityReporting.InScope) + assert.Equal(t, []string{"other"}, si.Project.VulnerabilityReporting.OutOfScope) + assert.Equal(t, "https://github.com/kubernetes/kubernetes", si.Repository.Url.String()) + }) + + t.Run("multi-repository project", func(t *testing.T) { + si, err := Load(specExample(t, "example-multi-repository-project.yml")) + require.NoError(t, err) + require.NotNil(t, si.Project) + assert.Equal(t, "FooBar", si.Project.Name) + assert.Len(t, si.Project.Repositories, 2) + }) + + t.Run("multi-repository project reuse", func(t *testing.T) { + parent := specExample(t, "example-multi-repository-project.yml") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/yaml") + _, _ = w.Write(parent) + })) + defer server.Close() + + child := string(specExample(t, "example-multi-repository-project-reuse.yml")) + require.Contains(t, child, specParentURL) + child = strings.Replace(child, specParentURL, server.URL, 1) + + si, err := Load([]byte(child)) + require.NoError(t, err) + require.NotNil(t, si.Project, "project should be inherited from project-si-source") + require.NotNil(t, si.Repository) + assert.Equal(t, "FooBar", si.Project.Name) + assert.Equal(t, "https://example.com/foobar/bar", si.Repository.Url.String()) + }) +} diff --git a/v2/si/test_data/spec-v2.2.0/example-full.yml b/v2/si/test_data/spec-v2.2.0/example-full.yml new file mode 100644 index 0000000..d59efa4 --- /dev/null +++ b/v2/si/test_data/spec-v2.2.0/example-full.yml @@ -0,0 +1,156 @@ +header: + schema-version: 2.0.0 + last-updated: '2025-03-01' + last-reviewed: '2025-04-01' + url: https://example.com/foo/bar/raw/branch/main/security-insights.yml + comment: | + This file contains all possible information for both project and repository, + though it is not required to include all of this information every time. + Nor is it required to include both a project and repository section if the project + section is intended to be inherited by repositories via header.project-si-source + +project: + name: FooBar + homepage: https://example.com + funding: https://example.com/FUNDING.yml + roadmap: https://example.com/roadmap.html + steward: + uri: https://example.com + comment: | + Some description of the relationship between this project and its steward. + administrators: + - name: Joe Dohn + affiliation: Foo + email: joe.bob@email.com + social: https://social.example.com/joebob + primary: true + documentation: + quickstart-guide: https://example.com/quickstart + detailed-guide: https://example.com/user-guide + code-of-conduct: https://example.com/code-of-conduct.html + release-process: https://example.com/release-process + support-policy: https://example.com/support-policy + signature-verification: https://example.com/signature-verification + repositories: + - name: Foo + url: https://example.com/foobar/foo + comment: | + Foo is the core repo for FooBar. + - name: Bar + url: https://example.com/foobar/bar + comment: | + Bar is a subproject repo. + vulnerability-reporting: + reports-accepted: true + bug-bounty-available: true + bug-bounty-program: https://example.com/bugs.html + contact: + name: The security team at FooBar Enterprise provides security support for this project. + email: security@something.com + primary: true + policy: https://example.com/reporting.html + in-scope: + - broken access control + - other + out-of-scope: + - other + pgp-key: | + your-key-here + comment: | + Lorum ipsum... + +repository: + url: https://github.com/kubernetes/kubernetes + status: active + bug-fixes-only: false + accepts-change-request: true + accepts-automated-change-request: true + no-third-party-packages: false + core-team: + - name: Alice White + affiliation: Foo Bar + email: alicewhite@example.com + social: https://social.example.com/alicewhite + primary: true + documentation: + contributing-guide: https://example.com/contributing-guide + review-policy: https://example.com/review-policy + security-policy: https://example.com/security-policy.html + governance: https://example.com/governance + dependency-management-policy: https://example.com/dependency-management-policy + license: + url: https://example.com/LICENSE + expression: MIT + release: + changelog: https://example.com/release/{version}#changelog + automated-pipeline: true + attestations: + - name: Release VEX + predicate-uri: https://intoto.VEX + location: https://example.com/release/{version}#vex + comment: Replace {version} with the actual version number for the release you want VEX data for. + - name: Release SBOM + predicate-uri: https://intoto.SPDX + location: https://example.com/release/{version}#spdx + comment: Replace {version} with the actual version number for the release you want an SBOM for. + - name: Maintainer Identity VSA + location: https://example.com/maintainer-identity + predicate-uri: https://slsa.dev/verification_summary/v1 + comment: | + This is a VSA that details how trust identities were established for maintainers of the project. + - name: SCA Scan Results + location: https://example.com/test-results#{version} + predicate-uri: https://slsa.dev/test_results/{version} + comment: Results from SCA scan for a specific version + distribution-points: + - uri: https://example.com/foo + comment: GitHub Release Page + - uri: pkg:npm/foobar + comment: NPM Package + license: + url: https://example.com/release/{version}#license + expression: MIT AND Apache-2.0 + security: + assessments: + self: + evidence: https://example.com/assessment.html + date: '2021-09-01' + comment: | + foo bar + third-party: + - evidence: https://example.com/artifact.html + date: '2021-09-01' + comment: | + foo bar + champions: + - name: Joe Bob + email: joe.bob@example.com + primary: true + tools: + - name: Dependabot + type: SCA + version: 1.2.3 + rulesets: + - built-in + results: + adhoc: + name: Scheduled SCA Scan Results + predicate-uri: https://intoto.SCA + location: https://example.com/release/{version}#SCA + comment: Replace {version} with the actual version number for the release you want VEX data for. + ci: + name: PR SCA Scan Results + predicate-uri: https://intoto.SCA + location: https://example.com/release/{version}#SCA + comment: Replace {version} with the actual version number for the release you want VEX data for. + release: + name: Build & Release SCA Scan Results + predicate-uri: https://intoto.SCA + location: https://example.com/release/{version}#SCA + comment: Replace {version} with the actual version number for the release you want VEX data for. + integration: + adhoc: true + ci: true + release: true + comment: | + foo bar diff --git a/v2/si/test_data/spec-v2.2.0/example-minimum.yml b/v2/si/test_data/spec-v2.2.0/example-minimum.yml new file mode 100644 index 0000000..0f4d066 --- /dev/null +++ b/v2/si/test_data/spec-v2.2.0/example-minimum.yml @@ -0,0 +1,46 @@ +header: + schema-version: 2.0.0 + last-updated: '2025-03-01' + last-reviewed: '2025-04-01' + url: https://example.com/foo/bar/raw/branch/main/security-insights.yml + comment: | + This file contains the minimum information for both project and repository. + It not required to include both a project and repository section if the project + section is intended to be inherited by repositories via header.project-si-source + +project: + name: FooBar + administrators: + - name: Joe Dohn + affiliation: Foo + email: joe.bob@example.com + social: https://social.example.com/joebob + primary: true + repositories: + - name: Foo + url: https://example.com/foobar/foo + comment: | + Foo is the core repo for FooBar. + vulnerability-reporting: + reports-accepted: true + bug-bounty-available: true + +repository: + url: https://example.com/foobar/foo + status: active + accepts-change-request: true + accepts-automated-change-request: true + core-team: + - name: Alice White + affiliation: Foo Bar + email: alicewhite@email.com + social: https://social.example.com/alicewhite + primary: true + license: + url: https://example.com/LICENSE + expression: MIT + security: + assessments: + self: + comment: | + Self assessment has not yet been completed. diff --git a/v2/si/test_data/spec-v2.2.0/example-multi-repository-project-reuse.yml b/v2/si/test_data/spec-v2.2.0/example-multi-repository-project-reuse.yml new file mode 100644 index 0000000..df4ab7a --- /dev/null +++ b/v2/si/test_data/spec-v2.2.0/example-multi-repository-project-reuse.yml @@ -0,0 +1,28 @@ +# Repository template for a multi-repository project +# This file would be stored in the https://example.com/foobar/bar repository +header: + schema-version: 2.0.0 + last-updated: '2025-03-01' + last-reviewed: '2025-04-01' + url: https://example.com/foo/bar/raw/branch/main/security-insights.yml + project-si-source: https://raw.githubusercontent.com/example/repo/refs/heads/main/security-insights.yml + +repository: + url: https://example.com/foobar/bar + status: active + accepts-change-request: true + accepts-automated-change-request: true + core-team: + - name: Alice White + affiliation: Foo Bar + email: alicewhite@email.com + social: https://social.example.com/alicewhite + primary: true + license: + url: https://example.com/LICENSE + expression: MIT + security: + assessments: + self: + comment: | + Self assessment has not yet been completed. diff --git a/v2/si/test_data/spec-v2.2.0/example-multi-repository-project.yml b/v2/si/test_data/spec-v2.2.0/example-multi-repository-project.yml new file mode 100644 index 0000000..1a358d4 --- /dev/null +++ b/v2/si/test_data/spec-v2.2.0/example-multi-repository-project.yml @@ -0,0 +1,53 @@ +# Project and repository template for a multi-repository project +# This file would be stored in the https://example.com/foobar/foo repository +# and addressable via https://example.com/foobar/foo/security-insights.yml +header: + schema-version: 2.0.0 + last-updated: '2025-03-01' + last-reviewed: '2025-04-01' + url: https://example.com/foo/bar/raw/branch/main/security-insights.yml + comment: | + This file contains the minimum information for both project and repository. + It not required to include both a project and repository section if the project + section is intended to be inherited by repositories via header.project-si-source + +project: + name: FooBar + administrators: + - name: Joe Dohn + affiliation: Foo + email: joe.bob@example.com + social: https://social.example.com/joebob + primary: true + repositories: + - name: Foo + url: https://example.com/foobar/foo + comment: | + Foo is the core repo for FooBar. + - name: Bar + url: https://example.com/foobar/bar + comment: | + Bar is also part of the FooBar project. + vulnerability-reporting: + reports-accepted: true + bug-bounty-available: true + +repository: + url: https://example.com/foobar/foo + status: active + accepts-change-request: true + accepts-automated-change-request: true + core-team: + - name: Alice White + affiliation: Foo Bar + email: alicewhite@email.com + social: https://social.example.com/alicewhite + primary: true + license: + url: https://example.com/LICENSE + expression: MIT + security: + assessments: + self: + comment: | + Self assessment has not yet been completed.