From 4b24fa2f4ec7ca472dccfc38de4c0cd07a1a7c12 Mon Sep 17 00:00:00 2001 From: Eddie Knight Date: Wed, 26 Aug 2026 15:58:45 -0500 Subject: [PATCH 1/3] fix: link framework relations to metadata, drop duplicate lexicon entries Each framework was defined twice: once in metadata.yaml as a mapping-reference (id, title, version, url, description) and again in lexicon.yaml as a term. The compiled document rendered both, with two independently maintained descriptions, and the two files were joined only by string coincidence -- template.md piped each control's framework ID through addLinks, which matched it against lexicon terms and synonyms and silently emitted bare text when nothing matched. Four of the fourteen mapped frameworks were already unlinked that way (Scorecard, ISO-18974, UKSSCOP, BSI-TR-03185-2), and lexicon.yaml carried two entries both titled "Proactive Software Supply Chain Risk Management Framework", the second holding OWASP SAMM's definition. Link framework relations directly to the External Frameworks table row instead, giving each row a stable anchor, and delete the eleven duplicated lexicon entries. All fourteen mapped frameworks now resolve. The entry for Supply-chain Levels for Software Artifacts stays: its SLSA synonym is a live auto-link target in OSPS-BR criteria text. Add two validator checks so this cannot drift again silently: a mapping document's target reference must be declared in the catalog metadata, and lexicon terms must be unique. Signed-off-by: Eddie Knight --- baseline/lexicon.yaml | 112 ---------------------------------- cmd/pkg/baseline/validator.go | 19 ++++++ cmd/template.md | 4 +- 3 files changed, 21 insertions(+), 114 deletions(-) diff --git a/baseline/lexicon.yaml b/baseline/lexicon.yaml index cefad5c0..cd754d33 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 diff --git a/cmd/pkg/baseline/validator.go b/cmd/pkg/baseline/validator.go index 63abb7c7..b82753f6 100644 --- a/cmd/pkg/baseline/validator.go +++ b/cmd/pkg/baseline/validator.go @@ -84,6 +84,11 @@ func (v *Validator) Check(b *types.Baseline) error { } } + referenceIDs := make([]string, 0, len(b.Catalog.Metadata.MappingReferences)) + for _, ref := range b.Catalog.Metadata.MappingReferences { + referenceIDs = append(referenceIDs, ref.Id) + } + // 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 i := range b.Mappings { @@ -93,6 +98,20 @@ func (v *Validator) Check(b *types.Baseline) error { 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. + if fw := doc.TargetReference.ReferenceId; fw != "" && !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)) + } + } + + lexiconTerms := make([]string, 0, len(b.Lexicon)) + for _, entry := range b.Lexicon { + if slices.Contains(lexiconTerms, entry.Term) { + errs = append(errs, fmt.Errorf("duplicate lexicon term %q", entry.Term)) + } + lexiconTerms = append(lexiconTerms, entry.Term) } return errors.Join(errs...) 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 }} --- From 6c720a122bb8fec6487d46124b14f69e6e7bd30e Mon Sep 17 00:00:00 2001 From: Eddie Knight Date: Wed, 26 Aug 2026 15:58:45 -0500 Subject: [PATCH 2/3] fix: drop mapping-references that have no mapping document DORA, NIS2, CSbDP, CSAG, USCTM and MAF were declared as mapping-references but no mapping document has ever targeted them. They came over in the Gemara v1 migration (#494) from the pre-migration frameworks.yaml, where they were declaration-only as well. The compiled catalog is published to grc.store on release, so each one would ship as a framework reference a consumer could resolve but never match against, and each rendered a row in the External Frameworks table that no control could link to. The source material is not lost. The crosswalk workbook in docs/ keeps a sheet per framework, and four of them are substantially filled in -- though none reference a single OSPS control ID, so the crosswalk itself was never started. Re-add each reference alongside its mapping document. Every remaining reference now has exactly one mapping document, and every row in the External Frameworks table is the target of at least one control relation. Signed-off-by: Eddie Knight --- baseline/metadata.yaml | 30 ------------------------------ 1 file changed, 30 deletions(-) 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 From a7b1a0369cdee268b18c283399e4c36bc629d66e Mon Sep 17 00:00:00 2001 From: Eddie Knight Date: Thu, 27 Aug 2026 07:59:06 -0500 Subject: [PATCH 3/3] fix: close the validation gaps around framework and lexicon IDs The mapping-reference check exempted the empty string, which is the one value that fails invisibly: both `validate` and `cue vet` pass while the renderer drops the document from the relations and the crosswalk without a word. Blanking one reference-id removed all 9 SLSA relations from the compiled document and left its table row orphaned, with no error. - Drop the `fw != ""` guard so an empty ID is an error like any other undeclared one. - Add the reverse check: a mapping-reference declared in metadata with no mapping document behind it renders a row nothing can link to. This is the state the previous commit cleaned out by hand. - Compare lexicon names case-folded and across synonyms. addLinks resolves a name to the first entry declaring it and asLink folds terms into anchors, so names differing only by case share one destination. Ownership is tracked per entry, so two entries sharing a term still collide while a synonym repeating its own term stays allowed. - Constrain the reference ID shape in CUE, on both the catalog metadata and each mapping document's target reference, so an ID with a space cannot render a broken anchor. - Drop the two lexicon synonyms the stricter check rejects: Vulnerability Reporting claimed Coordinated Vulnerability Disclosure, which is its own entry with a different definition, and the SLSA entry repeated its own term. - Cover both new checks with tests, and drop two spellcheck words left over from the removed references. Rendered output is byte-identical, and anchors remain 14/14. --- .project-words.txt | 2 - baseline/lexicon.yaml | 3 - cmd/pkg/baseline/validator.go | 49 ++++++++-- cmd/pkg/baseline/validator_test.go | 149 +++++++++++++++++++++++++++++ schema/osps.cue | 9 ++ 5 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 cmd/pkg/baseline/validator_test.go 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 cd754d33..67a6510f 100644 --- a/baseline/lexicon.yaml +++ b/baseline/lexicon.yaml @@ -310,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/ @@ -368,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/cmd/pkg/baseline/validator.go b/cmd/pkg/baseline/validator.go index b82753f6..373206f4 100644 --- a/cmd/pkg/baseline/validator.go +++ b/cmd/pkg/baseline/validator.go @@ -89,10 +89,12 @@ func (v *Validator) Check(b *types.Baseline) error { referenceIDs = append(referenceIDs, ref.Id) } - // The mapping documents live outside the catalog, so nothing but this check - // keeps a mapping's source pointing at a control that actually exists. + 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)) @@ -100,18 +102,47 @@ func (v *Validator) Check(b *types.Baseline) error { } // 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. - if fw := doc.TargetReference.ReferenceId; fw != "" && !slices.Contains(referenceIDs, fw) { + // 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) } - lexiconTerms := make([]string, 0, len(b.Lexicon)) - for _, entry := range b.Lexicon { - if slices.Contains(lexiconTerms, entry.Term) { - errs = append(errs, fmt.Errorf("duplicate lexicon term %q", entry.Term)) + // 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 } - lexiconTerms = append(lexiconTerms, entry.Term) } 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/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 }