From d7ca0ecd0deb876bc95aa6f07431ce9d9d8a3381 Mon Sep 17 00:00:00 2001 From: Daniel-park10 Date: Sat, 7 Nov 2026 04:43:26 +0530 Subject: [PATCH] fix(baseline): recognize lowercase license and security policy file names (fixes #717) - Add lowercase file variants to discover in OSPS-LE-01.01 and OSPS-LE-03.01 - Add lowercase and uppercase globs to OSPS-LE-02.01 regex pass - Add lowercase and directory variants to OSPS-VM-01.01, OSPS-VM-02.01, OSPS-VM-03.01, OSPS-VM-04.02, OSPS-VM-05.01, OSPS-VM-06.01, OSPS-BR-07.02, OSPS-SA-03.01, STAGE1-REF-SECURITY-01, and context.security_contact - Update detect_license_type() in auto_detect to recognize lowercase license files - Update FIELD_FILE_LOCATIONS in test_project_reference.py - Add regression tests covering all casing and directory variants, including sindresorhus/slash repository layout Signed-off-by: Daniel-park10 --- .../src/darnit_baseline/openssf-baseline.toml | 89 ++++++-- .../darnit/src/darnit/context/auto_detect.py | 39 +++- tests/darnit/context/test_auto_detect.py | 17 ++ .../controls/test_license_discovery.py | 48 +++- .../controls/test_security_md_reference.py | 10 +- .../test_security_policy_discovery.py | 206 ++++++++++++++++++ .../darnit_baseline/test_project_reference.py | 37 +++- 7 files changed, 414 insertions(+), 32 deletions(-) create mode 100644 tests/darnit_baseline/controls/test_security_policy_discovery.py diff --git a/packages/darnit-baseline/src/darnit_baseline/openssf-baseline.toml b/packages/darnit-baseline/src/darnit_baseline/openssf-baseline.toml index 6918b7fc8..2c194e8d5 100644 --- a/packages/darnit-baseline/src/darnit_baseline/openssf-baseline.toml +++ b/packages/darnit-baseline/src/darnit_baseline/openssf-baseline.toml @@ -259,7 +259,11 @@ store_as = "security.contact" # A user-judgment key (feature 042, FR-005); see maintainers. auto_detect = false required = false -hint_sources = ["SECURITY.md", ".github/SECURITY.md"] +hint_sources = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] allow_sieve_hints = true # Rejects detected values containing "example.com" or "example.org" before # they can be stored or offered for confirmation (feature 039). @@ -1688,6 +1692,9 @@ discover = [ "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "LICENCE.txt", "COPYING", "COPYING.md", "COPYING.txt", + "license", "license.md", "license.txt", + "licence", "licence.md", "licence.txt", + "copying", "copying.md", "copying.txt", ] kind = "file" @@ -1749,7 +1756,11 @@ help_md = """Use an OSI-approved open source license. # Local check: Look for SPDX identifier in project files (no API needed) [[controls."OSPS-LE-02.01".passes]] handler = "regex" -files = ["LICENSE*", "COPYING*", "NOTICE*", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"] +files = [ + "LICENSE*", "LICENCE*", "COPYING*", "NOTICE*", + "license*", "licence*", "copying*", "notice*", + "package.json", "pyproject.toml", "Cargo.toml", "go.mod", +] pattern = '(?i)(SPDX-License-Identifier\s*:\s*\S+|"license"\s*:\s*"[^"]+"|license\s*=\s*"[^"]+"|License\s*::\s*OSI)' pass_if_any = true expr = 'output.any_match' @@ -1868,6 +1879,9 @@ discover = [ "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "LICENCE.txt", "COPYING", "COPYING.md", "COPYING.txt", + "license", "license.md", "license.txt", + "licence", "licence.md", "licence.txt", + "copying", "copying.md", "copying.txt", ] kind = "file" @@ -2286,7 +2300,11 @@ help_md = """Add security contacts to SECURITY.md. [controls."OSPS-VM-02.01".locator] project_path = "security.policy" -discover = ["SECURITY.md", ".github/SECURITY.md", "docs/SECURITY.md"] +discover = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] kind = "file" [controls."OSPS-VM-02.01".locator.llm_hints] @@ -2728,7 +2746,11 @@ help_md = """Create CVD policy in SECURITY.md. [controls."OSPS-VM-01.01".locator] project_path = "security.policy" -discover = ["SECURITY.md", ".github/SECURITY.md", "docs/SECURITY.md"] +discover = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] kind = "file" [controls."OSPS-VM-01.01".locator.llm_hints] @@ -2738,7 +2760,11 @@ look_for_urls = true [[controls."OSPS-VM-01.01".passes]] handler = "pattern" -files = ["SECURITY.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] [controls."OSPS-VM-01.01".passes.pattern.patterns] disclosure = "(disclos|report|contact)" @@ -2796,7 +2822,11 @@ help_md = """Enable private vulnerability reporting. [controls."OSPS-VM-03.01".locator] project_path = "security.policy" -discover = ["SECURITY.md", ".github/SECURITY.md"] +discover = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] kind = "file" [controls."OSPS-VM-03.01".locator.llm_hints] @@ -2816,7 +2846,11 @@ concludes = ["pass"] # PatternPass: Check for private contact methods in SECURITY.md [[controls."OSPS-VM-03.01".passes]] handler = "pattern" -files = ["SECURITY.md", ".github/SECURITY.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] [controls."OSPS-VM-03.01".passes.pattern.patterns] private_contact = "(private|confidential|security advisory|PGP|GPG|security@|email)" @@ -3074,7 +3108,12 @@ help_md = """Document secrets management policy. [[controls."OSPS-BR-07.02".passes]] handler = "pattern" -files = ["SECURITY.md", "docs/security.md", "docs/SECRETS-POLICY.md", "CONTRIBUTING.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", + "docs/SECRETS-POLICY.md", "CONTRIBUTING.md", +] [controls."OSPS-BR-07.02".passes.pattern.patterns] secrets_policy = "(secret|credential|token|key).*(management|policy|handling|rotation)" @@ -3436,7 +3475,12 @@ Run: `create_security_policy()` or `ensure_vex_policy()` [[controls."OSPS-VM-04.02".passes]] handler = "pattern" -files = ["SECURITY.md", ".github/SECURITY.md", "docs/VEX-POLICY.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", + "docs/VEX-POLICY.md", +] [controls."OSPS-VM-04.02".passes.pattern.patterns] vex_policy = "(vex|vulnerability.exploitability|exploitability.exchange)" @@ -3500,7 +3544,12 @@ help_md = """Define SCA remediation policy. [[controls."OSPS-VM-05.01".passes]] handler = "pattern" -files = ["SECURITY.md", "docs/security.md", "docs/SCA-POLICY.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", + "docs/SCA-POLICY.md", +] [controls."OSPS-VM-05.01".passes.pattern.patterns] sca_policy = "(dependency|SCA|software composition|remediat)" @@ -3717,7 +3766,12 @@ help_md = """Define SAST remediation policy. [[controls."OSPS-VM-06.01".passes]] handler = "pattern" -files = ["SECURITY.md", "docs/SAST-POLICY.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", + "docs/SAST-POLICY.md", +] [controls."OSPS-VM-06.01".passes.pattern.patterns] sast_policy = "(SAST|static analysis|code scan|remediat)" @@ -4336,7 +4390,12 @@ help_md = """Perform and document security assessment. [[controls."OSPS-SA-03.01".passes]] handler = "pattern" -files = ["SECURITY.md", "docs/security.md", "CONTRIBUTING.md", "docs/SECURITY-ASSESSMENT.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", + "CONTRIBUTING.md", "docs/SECURITY-ASSESSMENT.md", +] [controls."OSPS-SA-03.01".passes.pattern.patterns] security_assessment = "(security.*(assessment|review|audit)|pre-release.*security)" @@ -4489,7 +4548,11 @@ tags = { level = 1, domain = "VM", "stage1-ref" = true } # orchestrator so both properties hold; documented for follow-up. [[controls."STAGE1-REF-SECURITY-01".passes]] handler = "file_exists" -files = ["SECURITY.md", "docs/SECURITY.md", ".github/SECURITY.md"] +files = [ + "SECURITY.md", "security.md", + ".github/SECURITY.md", ".github/security.md", + "docs/SECURITY.md", "docs/security.md", +] authority = "dispositive" [[controls."STAGE1-REF-SECURITY-01".passes]] diff --git a/packages/darnit/src/darnit/context/auto_detect.py b/packages/darnit/src/darnit/context/auto_detect.py index 46b8b8660..a99169829 100644 --- a/packages/darnit/src/darnit/context/auto_detect.py +++ b/packages/darnit/src/darnit/context/auto_detect.py @@ -20,6 +20,7 @@ from darnit.core.logging import get_logger from darnit.core.repo_files import read_repository_text +from darnit.core.utils import resolve_case_exact_path if TYPE_CHECKING: from darnit.config.context_schema import ContextDefinition @@ -183,16 +184,34 @@ def detect_license_type(local_path: str) -> str | None: "apache-2.0", "mit", "bsd-3-clause", "gpl", "isc", "mpl-2.0", "lgpl", "unlicense", or None if unknown. """ - license_path = Path(local_path) / "LICENSE" - if not license_path.is_file(): - # Also check LICENSE.md, LICENSE.txt - for alt in ("LICENSE.md", "LICENSE.txt"): - alt_path = Path(local_path) / alt - if alt_path.is_file(): - license_path = alt_path - break - else: - return None + candidates = ( + "LICENSE", + "LICENSE.md", + "LICENSE.txt", + "LICENCE", + "LICENCE.md", + "LICENCE.txt", + "COPYING", + "COPYING.md", + "COPYING.txt", + "license", + "license.md", + "license.txt", + "licence", + "licence.md", + "licence.txt", + "copying", + "copying.md", + "copying.txt", + ) + license_path: Path | None = None + for candidate in candidates: + exact = resolve_case_exact_path(candidate, local_path) + if exact and os.path.isfile(exact): + license_path = Path(exact) + break + if license_path is None: + return None try: content = read_repository_text(local_path, license_path, errors="replace")[:1000] diff --git a/tests/darnit/context/test_auto_detect.py b/tests/darnit/context/test_auto_detect.py index 0abff4e35..d9e5a8f01 100644 --- a/tests/darnit/context/test_auto_detect.py +++ b/tests/darnit/context/test_auto_detect.py @@ -232,6 +232,23 @@ def test_prefers_license_over_license_md(self, tmp_path): (tmp_path / "LICENSE.md").write_text("# MIT License") assert detect_license_type(str(tmp_path)) == "apache-2.0" + def test_lowercase_license_detected(self, tmp_path): + """Issue #717: detect_license_type recognizes lowercase license file.""" + (tmp_path / "license").write_text("MIT License\n\nCopyright (c) 2026") + assert detect_license_type(str(tmp_path)) == "mit" + + def test_lowercase_license_md_detected(self, tmp_path): + (tmp_path / "license.md").write_text("Apache License\nVersion 2.0") + assert detect_license_type(str(tmp_path)) == "apache-2.0" + + def test_licence_detected(self, tmp_path): + (tmp_path / "licence").write_text("MIT License\n\nCopyright (c) 2026") + assert detect_license_type(str(tmp_path)) == "mit" + + def test_copying_detected(self, tmp_path): + (tmp_path / "copying").write_text("GNU General Public License\nVersion 3") + assert detect_license_type(str(tmp_path)) == "gpl" + class TestCollectAutoContext: def test_collects_all(self, tmp_path): diff --git a/tests/darnit_baseline/controls/test_license_discovery.py b/tests/darnit_baseline/controls/test_license_discovery.py index c6618b8c3..681b3d5af 100644 --- a/tests/darnit_baseline/controls/test_license_discovery.py +++ b/tests/darnit_baseline/controls/test_license_discovery.py @@ -47,14 +47,33 @@ def _make_ctx(local_path: Path, control_id: str) -> CheckContext: @pytest.mark.parametrize("control_id", LICENSE_CONTROLS) @pytest.mark.parametrize( "filename", - ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "LICENCE.txt", "COPYING"], + [ + "LICENSE", + "LICENSE.md", + "LICENSE.txt", + "LICENCE", + "LICENCE.md", + "LICENCE.txt", + "COPYING", + "COPYING.md", + "COPYING.txt", + "license", + "license.md", + "license.txt", + "licence", + "licence.md", + "licence.txt", + "copying", + "copying.md", + "copying.txt", + ], ) def test_license_filename_variants_found( tmp_path: Path, control_id: str, filename: str, ) -> None: - """Both controls accept US and British spellings and common suffixes. + """Both controls accept US and British spellings, lowercase names, and common suffixes. Feature 041: finding the file is evidence; whether it concludes PASS depends on the control's step declarations, so this asserts discovery @@ -65,8 +84,8 @@ def test_license_filename_variants_found( control = _load_control(control_id) result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, control_id)) - assert result.status != "FAIL", f"{control_id} failed on {filename}: {result.details}" - assert result.evidence["relative_path"] == filename + assert result.status != "FAIL", f"{control_id} failed on {filename}: {result.message}" + assert result.evidence["relative_path"].lower() == filename.lower() @pytest.mark.parametrize("control_id", LICENSE_CONTROLS) @@ -89,3 +108,24 @@ def test_both_controls_share_one_discover_list() -> None: lists = [config.controls[c].locator.discover for c in LICENSE_CONTROLS] assert lists[0] == lists[1] assert "LICENCE" in lists[0] + assert "license" in lists[0] + + +@pytest.mark.parametrize( + "filename", + ["license", "license.md", "licence", "copying"], +) +def test_le_02_01_matches_lowercase_license_files(tmp_path: Path, filename: str) -> None: + """Issue #717: OSPS-LE-02.01 regex pass detects SPDX identifiers in lowercase license files.""" + (tmp_path / filename).write_text( + "// SPDX-License-Identifier: MIT\n" + MIT_TEXT, + encoding="utf-8", + ) + + control = _load_control("OSPS-LE-02.01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "OSPS-LE-02.01")) + + assert result.pass_history, f"No pass history for OSPS-LE-02.01 on {filename}" + pattern_attempt = result.pass_history[0] + assert pattern_attempt.result.outcome.value == "pass" + assert pattern_attempt.result.evidence.get("any_match") is True diff --git a/tests/darnit_baseline/controls/test_security_md_reference.py b/tests/darnit_baseline/controls/test_security_md_reference.py index 1435fc430..8580cb698 100644 --- a/tests/darnit_baseline/controls/test_security_md_reference.py +++ b/tests/darnit_baseline/controls/test_security_md_reference.py @@ -16,6 +16,8 @@ from pathlib import Path +import pytest + from darnit.sieve.handler_registry import ( HandlerContext, ) @@ -70,16 +72,20 @@ def test_first_run_reports_fail_no_security_md(self, tmp_path: Path) -> None: # file_exists ran and recorded the paths it checked. assert "files_checked" in (result.evidence or {}) + @pytest.mark.parametrize("filename", ["SECURITY.md", "security.md", ".github/security.md", "docs/security.md"]) def test_second_run_does_not_pass_on_presence_alone( self, tmp_path: Path, + filename: str, ) -> None: """Feature 041 (was US4 acceptance #3 of feature 025): with SECURITY.md present, file_exists may not conclude PASS for a content control; the control is WARN with the found file recorded as evidence. """ (tmp_path / "README.md").write_text("# proj\n") - (tmp_path / "SECURITY.md").write_text( + target_path = tmp_path / filename + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_text( "# Security Policy\n\nReport issues to sec@example.com\n", ) @@ -96,7 +102,7 @@ def test_second_run_does_not_pass_on_presence_alone( assert result.status == "WARN" assert result.authority == "suggestive" assert result.concluded_by == "none" - assert result.evidence["relative_path"] == "SECURITY.md" + assert result.evidence["relative_path"].lower() == filename.lower() def test_cli_and_direct_produce_equal_authority(self, tmp_path: Path) -> None: """US4 acceptance #4 + SC-004 (partial): the same control run two diff --git a/tests/darnit_baseline/controls/test_security_policy_discovery.py b/tests/darnit_baseline/controls/test_security_policy_discovery.py new file mode 100644 index 000000000..e269ec9cd --- /dev/null +++ b/tests/darnit_baseline/controls/test_security_policy_discovery.py @@ -0,0 +1,206 @@ +"""Regression tests for security policy file discovery (issue #717). + +OSPS-VM-01.01, OSPS-VM-02.01, OSPS-VM-03.01, and STAGE1-REF-SECURITY-01 +previously only looked for uppercase variants (SECURITY.md, .github/SECURITY.md, +docs/SECURITY.md), missing lowercase file names that GitHub recognizes (such +as security.md, .github/security.md, and docs/security.md). +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from darnit.sieve.models import CheckContext, ControlSpec +from darnit.sieve.orchestrator import SieveOrchestrator + +SECURITY_POLICY_CONTROLS = ( + "OSPS-VM-01.01", + "OSPS-VM-02.01", + "OSPS-VM-03.01", + "STAGE1-REF-SECURITY-01", +) + +SECURITY_FILENAMES = ( + "SECURITY.md", + "security.md", + ".github/SECURITY.md", + ".github/security.md", + "docs/SECURITY.md", + "docs/security.md", +) + +SECURITY_POLICY_TEXT = """# Security Policy + +## Reporting a Vulnerability + +Please report vulnerabilities by email to security@example.com. +We will acknowledge receipt within 48 hours and provide a timeline for fixes. +We follow a 90-day coordinated disclosure process. +""" + +MIT_LICENSE_TEXT = """MIT License + +Copyright (c) 2026 Example Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction. +""" + + +def _load_control(control_id: str) -> ControlSpec: + from darnit.config.control_loader import control_from_framework + from darnit.config.merger import load_framework_by_name + + config = load_framework_by_name("openssf-baseline") + return control_from_framework(control_id, config.controls[control_id]) + + +def _make_ctx(local_path: Path, control_id: str) -> CheckContext: + return CheckContext( + owner="test", + repo="test-repo", + local_path=str(local_path), + default_branch="main", + control_id=control_id, + ) + + +@pytest.mark.parametrize("filename", SECURITY_FILENAMES) +def test_vm_02_01_discovers_security_policy_variants(tmp_path: Path, filename: str) -> None: + """Issue #717: OSPS-VM-02.01 discovers all casing and location variants.""" + file_path = tmp_path / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(SECURITY_POLICY_TEXT, encoding="utf-8") + + control = _load_control("OSPS-VM-02.01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "OSPS-VM-02.01")) + + assert "None of the required files found" not in (result.message or "") + assert result.status != "FAIL", f"OSPS-VM-02.01 failed on {filename}: {result.message}" + assert result.evidence.get("relative_path") or result.evidence.get("found_file") + + +@pytest.mark.parametrize("filename", SECURITY_FILENAMES) +def test_stage1_ref_security_01_discovers_security_policy_variants(tmp_path: Path, filename: str) -> None: + """Issue #717: STAGE1-REF-SECURITY-01 dispositive file_exists finds all variants.""" + file_path = tmp_path / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(SECURITY_POLICY_TEXT, encoding="utf-8") + + control = _load_control("STAGE1-REF-SECURITY-01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "STAGE1-REF-SECURITY-01")) + + assert "None of the required files found" not in (result.message or "") + assert result.status != "FAIL", f"STAGE1-REF-SECURITY-01 failed on {filename}: {result.message}" + + +@pytest.mark.parametrize("filename", SECURITY_FILENAMES) +def test_vm_01_01_pattern_and_discover_variants(tmp_path: Path, filename: str) -> None: + """Issue #717: OSPS-VM-01.01 locator and pattern passes find all variants.""" + file_path = tmp_path / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(SECURITY_POLICY_TEXT, encoding="utf-8") + + control = _load_control("OSPS-VM-01.01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "OSPS-VM-01.01")) + + assert "None of the required files found" not in (result.message or "") + # The pattern pass matches disclosure & process keywords + assert result.status != "FAIL", f"OSPS-VM-01.01 failed on {filename}: {result.message}" + + +@pytest.mark.parametrize("filename", SECURITY_FILENAMES) +def test_vm_03_01_pattern_and_discover_variants(tmp_path: Path, filename: str) -> None: + """Issue #717: OSPS-VM-03.01 locator and pattern passes find all variants.""" + file_path = tmp_path / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(SECURITY_POLICY_TEXT, encoding="utf-8") + + control = _load_control("OSPS-VM-03.01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "OSPS-VM-03.01")) + + assert "None of the required files found" not in (result.message or "") + assert result.status != "FAIL", f"OSPS-VM-03.01 failed on {filename}: {result.message}" + + +def test_missing_security_policy_fails(tmp_path: Path) -> None: + """A repository without a security policy must still fail.""" + (tmp_path / "README.md").write_text("# Test Repo\n", encoding="utf-8") + + for control_id in ("OSPS-VM-02.01", "STAGE1-REF-SECURITY-01"): + control = _load_control(control_id) + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, control_id)) + assert result.status == "FAIL", f"{control_id} should fail when no security policy exists" + assert "None of the required files found" in (result.message or "") + + +def test_sindresorhus_slash_regression(tmp_path: Path) -> None: + """Issue #717 regression: sindresorhus/slash has root 'license' and '.github/security.md'. + + Neither should fail with 'None of the required files found'. + """ + (tmp_path / "license").write_text( + "// SPDX-License-Identifier: MIT\n" + MIT_LICENSE_TEXT, + encoding="utf-8", + ) + gh_dir = tmp_path / ".github" + gh_dir.mkdir(parents=True, exist_ok=True) + (gh_dir / "security.md").write_text(SECURITY_POLICY_TEXT, encoding="utf-8") + + orchestrator = SieveOrchestrator(stop_on_llm=False) + + for control_id in ("OSPS-LE-01.01", "OSPS-LE-03.01", "OSPS-VM-02.01", "STAGE1-REF-SECURITY-01"): + control = _load_control(control_id) + result = orchestrator.verify(control, _make_ctx(tmp_path, control_id)) + assert "None of the required files found" not in (result.message or ""), ( + f"{control_id} failed to discover file on sindresorhus/slash layout: {result.message}" + ) + assert result.status != "FAIL", ( + f"{control_id} unexpectedly failed on sindresorhus/slash layout: {result.message}" + ) + + +def test_security_contact_hint_sources_include_lowercase() -> None: + """Issue #717: context.security_contact hint_sources includes lowercase variants.""" + from darnit.config.merger import load_framework_by_name + + config = load_framework_by_name("openssf-baseline") + hint_sources = config.context.definitions["security_contact"].hint_sources + assert "security.md" in hint_sources + assert ".github/security.md" in hint_sources + assert "docs/security.md" in hint_sources + + +@pytest.mark.parametrize("filename", SECURITY_FILENAMES) +def test_vm_05_01_pattern_matches_security_policy_variants(tmp_path: Path, filename: str) -> None: + """OSPS-VM-05.01 pattern pass finds SCA policy across casing and directory variants.""" + file_path = tmp_path / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text( + "# Security Policy\n\nDependencies and SCA findings are remediated promptly.\n", + encoding="utf-8", + ) + + control = _load_control("OSPS-VM-05.01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "OSPS-VM-05.01")) + assert result.pass_history + assert result.pass_history[0].result.outcome.value == "pass" + + +@pytest.mark.parametrize("filename", SECURITY_FILENAMES) +def test_vm_06_01_pattern_matches_security_policy_variants(tmp_path: Path, filename: str) -> None: + """OSPS-VM-06.01 pattern pass finds SAST policy across casing and directory variants.""" + file_path = tmp_path / filename + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text( + "# Security Policy\n\nSAST findings are remediated promptly.\n", + encoding="utf-8", + ) + + control = _load_control("OSPS-VM-06.01") + result = SieveOrchestrator(stop_on_llm=False).verify(control, _make_ctx(tmp_path, "OSPS-VM-06.01")) + assert result.pass_history + assert result.pass_history[0].result.outcome.value == "pass" diff --git a/tests/darnit_baseline/test_project_reference.py b/tests/darnit_baseline/test_project_reference.py index 54a9e78df..407184ca5 100644 --- a/tests/darnit_baseline/test_project_reference.py +++ b/tests/darnit_baseline/test_project_reference.py @@ -51,8 +51,20 @@ # Where each referenced field's file conventionally lives (FR-016): a declared # reference must name a file of that kind. FIELD_FILE_LOCATIONS = { - "security.policy": ["SECURITY.md", "security.md", ".github/SECURITY.md", "docs/SECURITY.md", "docs/security.md"], - "governance.contributing": ["CONTRIBUTING.md", "contributing.md", ".github/CONTRIBUTING.md", "docs/CONTRIBUTING.md"], + "security.policy": [ + "SECURITY.md", + "security.md", + ".github/SECURITY.md", + ".github/security.md", + "docs/SECURITY.md", + "docs/security.md", + ], + "governance.contributing": [ + "CONTRIBUTING.md", + "contributing.md", + ".github/CONTRIBUTING.md", + "docs/CONTRIBUTING.md", + ], "governance.codeowners": ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"], "governance.governance_doc": ["GOVERNANCE.md", "governance.md", "docs/GOVERNANCE.md", ".github/GOVERNANCE.md"], "governance.maintainers": [ @@ -62,7 +74,26 @@ "docs/MAINTAINERS.md", ".github/MAINTAINERS.md", ], - "legal.license": ["LICENSE", "LICENSE.md", "LICENSE.txt", "license", "COPYING"], + "legal.license": [ + "LICENSE", + "LICENSE.md", + "LICENSE.txt", + "LICENCE", + "LICENCE.md", + "LICENCE.txt", + "COPYING", + "COPYING.md", + "COPYING.txt", + "license", + "license.md", + "license.txt", + "licence", + "licence.md", + "licence.txt", + "copying", + "copying.md", + "copying.txt", + ], "documentation.readme": ["README.md", "README", "readme.md", "README.rst"], "documentation.support": ["SUPPORT.md", "support.md", ".github/SUPPORT.md", "docs/SUPPORT.md"], "documentation.architecture": [