From 2d4371ba37a20d528011630bf63fc1b4aded2025 Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 8 Oct 2026 02:09:53 -0500 Subject: [PATCH 1/2] fix: load in-scope and out-of-scope as string lists Fixes #68 What/Why The schema declares in-scope and out-of-scope as string lists, but the generated types made them *URL. The strict loader then rejected every file that set either field. Both are now []string. Proof it works TestLoadV220Fields now loads a fixture with both fields. Without the type change it fails with "cannot unmarshal []interface {} into ... si.URL", the error in the issue. go test, golangci-lint, and make covcheck (76.2%) pass. Risk Low. The field type change is an API break on paper, but no file that set these fields could load before, and no known consumer reads them. AI role Drafted by Claude Opus 5.5 and reviewed by the maintainer. Review focus generated_types.go is hand-edited for these two fields only. A full regeneration from the current spec also changes Project and Repository from pointers to values, which needs its own change. Signed-off-by: jmeridth --- v2/si/generated_types.go | 4 ++-- v2/si/security_insights_test.go | 2 ++ v2/si/test_data/minimal-v2.2.0.yml | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) 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/security_insights_test.go b/v2/si/security_insights_test.go index 8719e71..e40b292 100644 --- a/v2/si/security_insights_test.go +++ b/v2/si/security_insights_test.go @@ -98,6 +98,8 @@ func TestLoadV220Fields(t *testing.T) { require.NotNil(t, &si.Project.VulnerabilityReporting) assert.NotNil(t, si.Project.VulnerabilityReporting.Policy, "vulnerability-reporting.policy should be set") assert.Equal(t, "https://example.com/SECURITY.md", si.Project.VulnerabilityReporting.Policy.String()) + assert.Equal(t, []string{"remote code execution in the server"}, si.Project.VulnerabilityReporting.InScope) + assert.Equal(t, []string{"denial of service"}, si.Project.VulnerabilityReporting.OutOfScope) } func TestNewURL(t *testing.T) { diff --git a/v2/si/test_data/minimal-v2.2.0.yml b/v2/si/test_data/minimal-v2.2.0.yml index efbc8f0..dd74f24 100644 --- a/v2/si/test_data/minimal-v2.2.0.yml +++ b/v2/si/test_data/minimal-v2.2.0.yml @@ -4,7 +4,7 @@ header: last-reviewed: '2025-03-01' url: https://example.com/kubernetes/kubernetes comment: | - Minimal v2.2.0 example with project.documentation.design and vulnerability-reporting.policy + Minimal v2.2.0 example with project.documentation.design, vulnerability-reporting.policy, in-scope, and out-of-scope project: name: FooBar @@ -29,6 +29,10 @@ project: reports-accepted: true bug-bounty-available: true policy: https://example.com/SECURITY.md + in-scope: + - remote code execution in the server + out-of-scope: + - denial of service repository: url: https://vcs.example.com/foobar/foo From 8a97be72d25f0d6d345c537281c4da6f644e103e Mon Sep 17 00:00:00 2001 From: jmeridth Date: Thu, 8 Oct 2026 09:35:35 -0500 Subject: [PATCH 2/2] test: load every Security Insights v2.2.0 spec example Closes #72 What/Why The tests only loaded "minimal" fixtures, so the spec's own example-full.yml failed to load (#68) without any test noticing. The four v2.2.0 spec examples now live unmodified in test_data/spec-v2.2.0, and TestLoadSpecExamples loads each one. The in-scope fields added to minimal-v2.2.0.yml earlier in this branch are reverted, since that fixture is meant to stay minimal. Proof it works TestLoadSpecExamples passes. With the old *URL types, the full example fails with the #68 unmarshal error. The reuse example's parent is served by httptest, so no test touches the network. make check-spec-examples confirms the fixtures match the v2.2.0 tag. go test, golangci-lint, and make covcheck (76.2%) pass. Risk Low. Tests and a Makefile target only. AI role Drafted by Claude Opus 5.5 and reviewed by the maintainer. Review focus Whether check-spec-examples should also run in CI. Signed-off-by: jmeridth --- Makefile | 12 +- v2/si/security_insights_test.go | 2 - v2/si/spec_examples_test.go | 79 +++++++++ v2/si/test_data/minimal-v2.2.0.yml | 6 +- v2/si/test_data/spec-v2.2.0/example-full.yml | 156 ++++++++++++++++++ .../test_data/spec-v2.2.0/example-minimum.yml | 46 ++++++ ...example-multi-repository-project-reuse.yml | 28 ++++ .../example-multi-repository-project.yml | 53 ++++++ 8 files changed, 374 insertions(+), 8 deletions(-) create mode 100644 v2/si/spec_examples_test.go create mode 100644 v2/si/test_data/spec-v2.2.0/example-full.yml create mode 100644 v2/si/test_data/spec-v2.2.0/example-minimum.yml create mode 100644 v2/si/test_data/spec-v2.2.0/example-multi-repository-project-reuse.yml create mode 100644 v2/si/test_data/spec-v2.2.0/example-multi-repository-project.yml 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/security_insights_test.go b/v2/si/security_insights_test.go index e40b292..8719e71 100644 --- a/v2/si/security_insights_test.go +++ b/v2/si/security_insights_test.go @@ -98,8 +98,6 @@ func TestLoadV220Fields(t *testing.T) { require.NotNil(t, &si.Project.VulnerabilityReporting) assert.NotNil(t, si.Project.VulnerabilityReporting.Policy, "vulnerability-reporting.policy should be set") assert.Equal(t, "https://example.com/SECURITY.md", si.Project.VulnerabilityReporting.Policy.String()) - assert.Equal(t, []string{"remote code execution in the server"}, si.Project.VulnerabilityReporting.InScope) - assert.Equal(t, []string{"denial of service"}, si.Project.VulnerabilityReporting.OutOfScope) } func TestNewURL(t *testing.T) { 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/minimal-v2.2.0.yml b/v2/si/test_data/minimal-v2.2.0.yml index dd74f24..efbc8f0 100644 --- a/v2/si/test_data/minimal-v2.2.0.yml +++ b/v2/si/test_data/minimal-v2.2.0.yml @@ -4,7 +4,7 @@ header: last-reviewed: '2025-03-01' url: https://example.com/kubernetes/kubernetes comment: | - Minimal v2.2.0 example with project.documentation.design, vulnerability-reporting.policy, in-scope, and out-of-scope + Minimal v2.2.0 example with project.documentation.design and vulnerability-reporting.policy project: name: FooBar @@ -29,10 +29,6 @@ project: reports-accepted: true bug-bounty-available: true policy: https://example.com/SECURITY.md - in-scope: - - remote code execution in the server - out-of-scope: - - denial of service repository: url: https://vcs.example.com/foobar/foo 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.