diff --git a/.project-words.txt b/.project-words.txt index 0f90f3c1..3150bcc4 100644 --- a/.project-words.txt +++ b/.project-words.txt @@ -1,6 +1,5 @@ CCCS CISA -CSAG crosswalked devel DSIT @@ -42,6 +41,5 @@ Subprojects triaging unreviewable UKSSCOP -USCTM Updegrove webfonts diff --git a/baseline/lexicon.yaml b/baseline/lexicon.yaml index cefad5c0..67a6510f 100644 --- a/baseline/lexicon.yaml +++ b/baseline/lexicon.yaml @@ -36,14 +36,6 @@ An automated test suite must return an overall "pass" or "fail" result, and is often implemented using a test framework. Common ways to invoke automated tests include `make check`, `make test`, `npm test`, and `cargo test` manually or as part of a Continuous Integration workflow. -- term: Best Practices Badge - definition: | - The OpenSSF Best Practices Badge Identifies FLOSS best practices & implements a badging system for those practices. - synonyms: - - BPB - - OpenSSF Best Practices Badge - references: - - https://www.bestpractices.dev/en - term: Build and Release Pipeline definition: | A series of automated processes that compile @@ -114,26 +106,6 @@ - https://certcc.github.io/CERT-Guide-to-CVD/ - https://www.first.org/global/sigs/vulnerability-coordination/multiparty/guidelines-v1-1 - https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/about-coordinated-disclosure-of-security-vulnerabilities -- term: Cyber Resilience Act - definition: | - Regulation (EU) 2024/2847 (Cyber Resilience Act, CRA). - 2024 European cybersecurity law that goes into full effect - December 2027. Focuses on products sold within the European - Union and the cybersecurity and vulnerability management - practices used to create and support the product. - synonyms: - - CRA - references: - - https://eur-lex.europa.eu/eli/reg/2024/2847/oj -- term: Cybersecurity Framework - definition: | - The NIST Cyber Security Framework (CSF) helps organizations understand and improve their management of cybersecurity risk. - synonyms: - - CSF - - NIST Cybersecurity Framework - references: - - https://www.nist.gov/cyberframework - - https://doi.org/10.6028/NIST.CSWP.29 - term: Defect definition: | Errors or flaws in the software that cause it @@ -211,53 +183,6 @@ multiple forms of identification. synonyms: - MFA -- term: NIST Special Publication 800-161 - Cybersecurity Supply Chain Risk Management Practices for Systems and Organizations - definition: | - Provides guidance to organizations on identifying, - assessing, and mitigating cybersecurity risks throughout - the supply chain at all levels of their organizations. - synonyms: - - 800-161 - references: - - https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-161r1-upd1.pdf -- term: OpenChain - definition: | - A Linux Foundation project that oversee two ISO/IEC standards to better understand and manage software supply chains. - synonyms: - - "18974" - - ISO/IEC 5230 - - ISO/IEC 18974 - references: - - https://openchainproject.org/ - - https://openchainproject.org/license-compliance -- term: OpenCRE - definition: | - An OWASP project that converts cybersecurity requirements into a hierarchical, machine-readable format. - synonyms: - - OpenCRE - references: - - https://www.opencre.org/ - - https://zeljkoobrenovic.github.io/opencre-explorer/ -- term: OpenSSF Scorecard - definition: | - An OpenSSF project that helps users assesses open - source projects for security risks through a series - of automated checks. It was created by OSS developers - to help improve the health of critical projects - that the community depends on. - synonyms: - - ScrCrd - references: - - https://github.com/ossf/scorecard - - https://scorecard.dev/ -- term: Payment Card Industry Data Security Standard - definition: | - PCI DSS provides a baseline of technical and operational - requirements designed to protect payment account data. - synonyms: - - PCIDSS - references: - - https://docs-prv.pcisecuritystandards.org/PCI%20DSS/Standard/PCI-DSS-v4_0_1.pdf - term: Primary Branch definition: | The main development branch in the version @@ -281,19 +206,6 @@ - Private Security Vulnerability Reporting references: - https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability -- term: Proactive Software Supply Chain Risk Management Framework - definition: | - A holistic framework that an organization can use to - proactively mitigate software supply chain risk through - guided adoption of tasks; and that supports assessment, - scoring, and comparison against industry peers, - standards, and guidelines. The P-SSCRM contextualizes and - quantifies the tasks contained across multiple standards - and frameworks to those carried out by various kinds of organizations. - synonyms: - - P-SSCRM - references: - - https://arxiv.org/pdf/2404.12300 - term: Project definition: | A group of people and resources that coordinate to @@ -308,18 +220,6 @@ release time, this may include provenance information, licensing details, and other metadata. -- term: Proactive Software Supply Chain Risk Management Framework - definition: | - A maturity model for software assurance that provides an - effective and measurable way for all types of organizations - to analyze and improve their software security posture. - OWASP SAMM supports the complete software lifecycle, including - development and acquisition, and is technology and process agnostic. - It is intentionally built to be evolutive and risk-driven in nature. - synonyms: - - SAMM - references: - - https://owaspsamm.org/model/ - term: Sensitive Data definition: | Information that, if disclosed to unauthorized @@ -366,18 +266,6 @@ synonyms: - Repo - Repositories -- term: Secure Software Development Framework - definition: | - The NIST Secure Software Development Framework (SP 800-218) is a - broadly reviewed and collaborative set of fundamental secure software - development practices. - synonyms: - - SSDF - - NIST Secure Software Development Framework - - NIST SP 800-218 - references: - - https://csrc.nist.gov/projects/ssdf - - https://csrc.nist.gov/pubs/sp/800/218/final - term: Software Bill of Materials definition: | A list of all components that make up a given piece of software @@ -422,7 +310,6 @@ An OpenSSF project that sets guidelines for securing software supply chain infrastructure and artifact integrity. synonyms: - SLSA - - Supply-chain Levels for Software Artifacts references: - https://openssf.org/projects/slsa/ - https://slsa.dev/ @@ -480,7 +367,5 @@ maintainers, security teams, or the public, as well as tracking the resolution of these vulnerabilities. - synonyms: - - Coordinated Vulnerability Disclosure references: - https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability diff --git a/baseline/metadata.yaml b/baseline/metadata.yaml index ead751ed..9c4e514d 100644 --- a/baseline/metadata.yaml +++ b/baseline/metadata.yaml @@ -86,36 +86,6 @@ metadata: version: 2025-05-07 url: https://www.ncsc.gov.uk/guidance/software-security-code-of-practice-assurance-principles-claims description: "The Software Code of Practice has been created by DSIT and the National Cyber Security Centre (NCSC), the UK’s technical authority for cyber security, and is co-sealed by the Canadian Centre for Cyber Security (CCCS). The Code reflects the government’s ongoing focus on codifying minimum standards for technology providers to reduce cyber risk. It is aimed at professionals who are responsible for overseeing the development of ‘commodity’ software, including technical, compliance, and risk experts. For those organisations that require a higher level of assurance in the resilience of their connected products and technology, consider using the NCSC’s Cyber Resilience Testing scheme." - - id: DORA - title: EU Digital Operational Resilience Act (DORA) - version: 2022-12-14 - url: https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32022R2554&from=FR - description: "On digital operational resilience for the financial sector and amending Regulations (EC) No 1060/2009, (EU) No 648/2012, (EU) No 600/2014, (EU) No 909/2014 and (EU) 2016/1011." - - id: NIS2 - title: EU Network and Information Security Directive 2 - version: 2024-10-17 - url: https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ:L_202402690#tit_1 - description: "Laying down rules for the application of Directive (EU) 2022/2555 as regards technical and methodological requirements of cybersecurity risk-management measures and further specification of the cases in which an incident is considered to be significant with regard to DNS service providers, TLD name registries, cloud computing service providers, data centre service providers, content delivery network providers, managed service providers, managed security service providers, providers of online market places, of online search engines and of social networking services platforms, and trust service providers." - - id: CSbDP - title: CISA Secure by Design Pledge - version: 2024-05-08 - url: https://www.cisa.gov/sites/default/files/2024-05/CISA%20Secure%20by%20Design%20Pledge_508c.pdf - description: "A voluntary pledge focused on seven goals to work towards, in addition to context and example approaches to achieve the goal and demonstrate measurable progress within enterprise software products and services." - - id: CSAG - title: CISA Software Acquisition Guide - version: 2024-08-01 - url: https://www.cisa.gov/resources-tools/resources/software-acquisition-guide-government-enterprise-consumers-software-assurance-cyber-supply-chain - description: "The Software Acquisition Guide for Government Enterprise Consumers: Software Assurance in the Cyber-Supply Chain Risk Management (C-SCRM) Lifecycle product was developed in response to the core challenges of software assurance and cybersecurity transparency in the acquisition process, focusing primarily on software lifecycle activities." - - id: USCTM - title: US Cyber Trust Mark - version: 2023-07-18 - url: https://www.fcc.gov/CyberTrustMark - description: "A voluntary cybersecurity labeling program for wireless consumer IoT products. " - - id: MAF - title: MITRE ATT&CK Framework - version: v18 - url: https://attack.mitre.org/ - description: "A globally-accessible knowledge base of adversary tactics and techniques based on real-world observations." - id: BSI-TR-03185-2 title: BSI TR-03185-2 Secure Software Lifecycle for Open Source Software version: v1.1.0 diff --git a/cmd/pkg/baseline/validator.go b/cmd/pkg/baseline/validator.go index 63abb7c7..373206f4 100644 --- a/cmd/pkg/baseline/validator.go +++ b/cmd/pkg/baseline/validator.go @@ -84,15 +84,65 @@ func (v *Validator) Check(b *types.Baseline) error { } } - // The mapping documents live outside the catalog, so nothing but this check - // keeps a mapping's source pointing at a control that actually exists. + referenceIDs := make([]string, 0, len(b.Catalog.Metadata.MappingReferences)) + for _, ref := range b.Catalog.Metadata.MappingReferences { + referenceIDs = append(referenceIDs, ref.Id) + } + + targetedIDs := make([]string, 0, len(b.Mappings)) for i := range b.Mappings { doc := &b.Mappings[i] + // The mapping documents live outside the catalog, so nothing but this + // check keeps a mapping's source pointing at a control that actually + // exists. for _, m := range doc.Mappings { if !slices.Contains(entryIDs, m.Source) { errs = append(errs, fmt.Errorf("mapping %s targets unknown control %q", m.Id, m.Source)) } } + // The rendered document links each framework relation to the row for + // this ID in the External Frameworks table, so an ID that is not + // declared in the catalog metadata renders as a dead anchor. The empty + // ID is deliberately not exempt: the renderer skips it in silence, + // dropping the document from both the relations and the crosswalk + // without failing anything. + fw := doc.TargetReference.ReferenceId + if !slices.Contains(referenceIDs, fw) { + errs = append(errs, fmt.Errorf("mapping document %q targets reference %q, which is not declared in metadata mapping-references", doc.Metadata.Id, fw)) + } + targetedIDs = append(targetedIDs, fw) + } + + // The reverse of the check above: a reference declared in metadata with no + // mapping document behind it renders a table row nothing can link to. + for _, id := range referenceIDs { + if !slices.Contains(targetedIDs, id) { + errs = append(errs, fmt.Errorf("mapping-reference %q is declared in metadata but no mapping document targets it", id)) + } + } + + // addLinks resolves a name to the first lexicon entry declaring it, and + // asLink folds a term into its anchor, so two names differing only by case + // silently share one destination. Synonyms take part in that resolution + // too, so comparing terms alone misses the collisions that actually + // mislink. A synonym repeating its own entry's term is harmless -- addLinks + // skips already-wrapped text -- so only cross-entry collisions are errors. + declaredBy := make(map[string]int, len(b.Lexicon)) + for i, entry := range b.Lexicon { + names := append([]string{entry.Term}, entry.Synonyms...) + for _, name := range names { + key := strings.ToLower(strings.TrimSpace(name)) + if key == "" { + continue + } + // Ownership is tracked by entry index, not by term: two entries + // sharing a term must still collide with each other. + if owner, ok := declaredBy[key]; ok && owner != i { + errs = append(errs, fmt.Errorf("lexicon name %q in entry %q collides with entry %q", name, entry.Term, b.Lexicon[owner].Term)) + continue + } + declaredBy[key] = i + } } return errors.Join(errs...) diff --git a/cmd/pkg/baseline/validator_test.go b/cmd/pkg/baseline/validator_test.go new file mode 100644 index 00000000..a8c65904 --- /dev/null +++ b/cmd/pkg/baseline/validator_test.go @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: Copyright 2026 The OSPS Authors +// SPDX-License-Identifier: Apache-2.0 + +package baseline + +import ( + "strings" + "testing" + + "github.com/gemaraproj/go-gemara" + + "github.com/ossf/security-baseline/pkg/types" +) + +const ( + testControlID = "OSPS-AC-01" + testSourceRef = "osps-baseline" + testTerm = "Repository" + wantCollision = `collides with entry "Repository"` +) + +// validBaseline is the smallest baseline that passes Check: one control, one +// declared mapping-reference, and one mapping document targeting it. +func validBaseline() *types.Baseline { + return &types.Baseline{ + Catalog: gemara.ControlCatalog{ + Metadata: gemara.Metadata{ + MappingReferences: []gemara.MappingReference{ + {Id: "CSF", Title: "Cybersecurity Framework", Version: "2.0"}, + }, + }, + Controls: []gemara.Control{ + {Id: testControlID, Title: "A control"}, + }, + }, + Mappings: []gemara.MappingDocument{ + { + Metadata: gemara.Metadata{Id: "osps-to-csf"}, + SourceReference: gemara.TypedMapping{ReferenceId: testSourceRef, EntryType: gemara.EntryTypeControl}, + TargetReference: gemara.TypedMapping{ReferenceId: "CSF", EntryType: gemara.EntryTypeGuideline}, + Mappings: []gemara.Mapping{ + {Id: "M-1", Source: testControlID, Relationship: gemara.RelRelatesTo}, + }, + }, + }, + Lexicon: []types.LexiconEntry{ + {Term: testTerm, Synonyms: []string{"Repo"}}, + }, + } +} + +func TestCheck(t *testing.T) { + tests := []struct { + name string + mutate func(*types.Baseline) + wantErr string // substring; empty means Check must pass + }{ + { + name: "valid baseline passes", + mutate: func(*types.Baseline) {}, + }, + { + name: "mapping document targeting an undeclared reference", + mutate: func(b *types.Baseline) { + b.Mappings[0].TargetReference.ReferenceId = "NOPE" + }, + wantErr: `targets reference "NOPE"`, + }, + { + // Regression guard: an empty reference-id used to be exempt, and + // the renderer drops such a document without a word. + name: "mapping document with an empty reference id", + mutate: func(b *types.Baseline) { + b.Mappings[0].TargetReference.ReferenceId = "" + }, + wantErr: `targets reference ""`, + }, + { + name: "declared reference with no mapping document", + mutate: func(b *types.Baseline) { + b.Catalog.Metadata.MappingReferences = append(b.Catalog.Metadata.MappingReferences, + gemara.MappingReference{Id: "ORPHAN", Title: "Orphan", Version: "1"}) + }, + wantErr: `mapping-reference "ORPHAN" is declared in metadata but no mapping document targets it`, + }, + { + name: "duplicate lexicon term", + mutate: func(b *types.Baseline) { + b.Lexicon = append(b.Lexicon, types.LexiconEntry{Term: testTerm}) + }, + wantErr: wantCollision, + }, + { + // asLink case-folds a term into its anchor, so these two share one + // destination even though the strings differ. + name: "lexicon terms differing only by case", + mutate: func(b *types.Baseline) { + b.Lexicon = append(b.Lexicon, types.LexiconEntry{Term: "repository"}) + }, + wantErr: wantCollision, + }, + { + name: "synonym colliding with another entry's term", + mutate: func(b *types.Baseline) { + b.Lexicon = append(b.Lexicon, types.LexiconEntry{ + Term: "Source Control", + Synonyms: []string{testTerm}, + }) + }, + wantErr: wantCollision, + }, + { + // Harmless: addLinks skips text it has already wrapped. + name: "synonym repeating its own term is allowed", + mutate: func(b *types.Baseline) { + b.Lexicon[0].Synonyms = append(b.Lexicon[0].Synonyms, testTerm) + }, + }, + { + name: "mapping targeting an unknown control", + mutate: func(b *types.Baseline) { + b.Mappings[0].Mappings[0].Source = "OSPS-XX-99" + }, + wantErr: `targets unknown control "OSPS-XX-99"`, + }, + } + + v := NewValidator() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := validBaseline() + tt.mutate(b) + + err := v.Check(b) + if tt.wantErr == "" { + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + return + } + if err == nil { + t.Fatalf("expected an error containing %q, got none", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected an error containing %q, got: %v", tt.wantErr, err) + } + }) + } +} diff --git a/cmd/template.md b/cmd/template.md index 26504244..f681cf91 100644 --- a/cmd/template.md +++ b/cmd/template.md @@ -117,7 +117,7 @@ For more information on the project and to make contributions, visit the [GitHub {{- $relations := relationsForControl .Id }} {{ if $relations }} {{ range $relations }} - - **{{ .Framework | addLinks }}**: {{ range $index, $entry := .Entries }}{{ if $index }}, {{ end }}{{ $entry }}{{ end }} + - **[{{ .Framework }}](#fw-{{ .Framework }})**: {{ range $index, $entry := .Entries }}{{ if $index }}, {{ end }}{{ $entry }}{{ end }} {{- end }} {{ end }} @@ -133,7 +133,7 @@ Controls within this document may relate to the following external frameworks: | ID | Title | Version | Description | |----|-------|---------|-------------| {{ range .Catalog.Metadata.MappingReferences -}} -| {{ .Id }} | [{{ .Title }}]({{ .Url }}) | {{ .Version }} | {{ .Description }} | +| {{ .Id }} | [{{ .Title }}]({{ .Url }}) | {{ .Version }} | {{ .Description }} | {{ end }} --- diff --git a/schema/osps.cue b/schema/osps.cue index ddf8cecd..4eea6bb5 100644 --- a/schema/osps.cue +++ b/schema/osps.cue @@ -4,11 +4,19 @@ package schema import "github.com/gemaraproj/gemara@v1" +// A mapping-reference id is interpolated verbatim into the anchor that links a +// framework relation to its row in the External Frameworks table, so it has to +// be usable as one. A space is the trap: it renders a broken link and nothing +// else complains. The empty string is excluded by the leading character class, +// which also stops a mapping document from silently dropping out of the render. +_refID: =~"^[A-Za-z0-9][A-Za-z0-9._-]*$" + // #OSPSBaseline layers OSPS-specific constraints on top of the Gemara #ControlCatalog. #OSPSBaseline: gemara.#ControlCatalog & { let _agID = =~"^maturity-" metadata: "applicability-groups": [{id: _agID}, ...{id: _agID}] + metadata: "mapping-references": [...{id: _refID}] } // #OSPSMapping layers OSPS-specific constraints on top of the Gemara #MappingDocument. @@ -16,4 +24,5 @@ import "github.com/gemaraproj/gemara@v1" // external framework, so the source reference is pinned to "osps-baseline". #OSPSMapping: gemara.#MappingDocument & { "source-reference": "reference-id": "osps-baseline" + "target-reference": "reference-id": _refID }