* test(115): add failing tests for secret-scan exclusion lint + strict mode
Adds tests/secret-scan-lint.test.cjs covering all 7 acceptance criteria
for issue #115 (secret-scan exclusion governance):
1. Lint exits 0 on fully-annotated .secretscanignore fixture
2. Lint exits 1 on fixture missing required key (reason/owner/expires)
3. Lint exits 1 on fixture with expires date in the past
4. Lint exits 1 on wildcard pattern without rule-id
5. Lint exits 0 on grandfathered entry (default mode), exits 1 under --strict
6. secret-scan --strict does not honour grandfathered exclusions
(temp workspace fixture: file with real AWS-key pattern excluded by a
grandfathered entry → default exits 0, strict exits 1)
7. secret-scan default mode behaviour unchanged for existing .secretscanignore
entries (regression test)
All 24 tests confirmed RED on origin/main before any implementation.
Test helpers use spawnSync throughout so both stdout and stderr are always
captured regardless of exit code (fixes the execFileSync/stderr gap from
the existing security-scan.test.cjs pattern).
Design references cited in test file:
- GitGuardian exclusion annotation convention:
https://docs.gitguardian.com/internal-repositories-monitoring/integrations/cli/secrets
- CNCF Security TAG threat-model exception lifecycle:
https://github.com/cncf/tag-security/blob/main/community/working-groups/threat-modeling/templates/threats.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(115): add secret-scan-lint.sh + --strict mode + annotation parser
Implements secret-scan exclusion governance for issue #115.
## secret-scan-lint.sh (new script)
Exit codes (match secret-scan.sh convention):
0 = all exclusions valid (or grandfathered with warning)
1 = annotation violation: missing key, expired date, wildcard without rule-id,
or (under --strict) any grandfathered entry
2 = config error (file not found, bad args)
Annotation format (sidecar comment, immediately preceding the path):
# allow: <pattern> reason="..." owner="..." expires="YYYY-MM-DD" [rule-id="..."]
<pattern>
Required keys: reason, owner, expires
Optional key: rule-id — required when pattern contains * wildcards
Grandfathered entries (plain comment, no structured keys):
- Default mode: exit 0 + deprecation warning to stderr
- --strict mode: exit 1
## secret-scan.sh (modified: --strict flag)
--strict flag for release/security-review CI lanes:
- Grandfathered entries are NOT applied (file is scanned, not skipped)
- Exclusions whose expires date is past are NOT applied
- Default mode behaviour is fully preserved
load_ignorelist() now parses annotations:
- Reads prev_comment to determine annotation status per entry
- Uses date comparison (YYYY-MM-DD lexicographic) for expires checks
- Emits DEPRECATION WARNING to stderr for grandfathered entries in default mode
- Emits WARNING under --strict when skipping a grandfathered entry
Design references:
- GitGuardian exclusion annotation convention:
https://docs.gitguardian.com/internal-repositories-monitoring/integrations/cli/secrets
- CNCF Security TAG threat-model exception lifecycle:
https://github.com/cncf/tag-security/blob/main/community/working-groups/threat-modeling/templates/threats.md
- TruffleHog / GitLeaks wildcard-exclusion risk informed the rule-id requirement
for wildcard entries (unguarded wildcards can accidentally suppress real findings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(115): annotate existing .secretscanignore entries + wire CI lint step
## .secretscanignore migration
Existing entry `get-shit-done/workflows/plan-phase.md` has been migrated
from a bare plain comment to a fully-structured annotation:
# allow: get-shit-done/workflows/plan-phase.md
# reason="contains illustrative DATABASE_URL/REDIS_URL example strings
# used as documentation placeholders — not real credentials"
# owner="@open-gsd/maintainers"
# expires="2027-06-30"
This entry now passes lint (exit 0) in both default and --strict modes.
The expiration date of 2027-06-30 gives the team ~13 months to review
whether the file still needs to be excluded before the entry expires.
## CI workflow change (.github/workflows/security-scan.yml)
Added step "Secret scan exclusion lint" immediately before the existing
"Planning directory check" step:
- name: Secret scan exclusion lint
run: |
chmod +x scripts/secret-scan-lint.sh
scripts/secret-scan-lint.sh --file .secretscanignore
The step has no ${{ }} context interpolation in its run block (no
injection surface). It runs on every PR targeting main, release/**, hotfix/**.
This implements CI acceptance criterion from issue #115:
"CI lint fails for unmanaged wildcard exclusions"
"CI enforces policy format"
## Header added to .secretscanignore
Added governance documentation block explaining annotation format,
required/optional keys, and references to design sources:
- GitGuardian exclusion annotation convention:
https://docs.gitguardian.com/internal-repositories-monitoring/integrations/cli/secrets
- CNCF Security TAG threat-model exception lifecycle:
https://github.com/cncf/tag-security/blob/main/community/working-groups/threat-modeling/templates/threats.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(115): document exclusion governance + periodic reduced-scan procedure
Updates SECURITY.md with a new section "Secret-Scan Exclusion Governance"
covering:
1. Annotation format (required/optional keys, wildcard rule)
2. Local lint command
3. Periodic reduced-exclusion scan procedure using --strict mode
The procedure section explicitly states when to run (every release +
scheduled security review), what --strict does differently, and what to do
when --strict finds findings that default mode does not.
No runbooks/security-audit*.md exists in this repo. SECURITY.md is the
correct location as it is what secret-scan.sh references in its header
docstring (via the "See SECURITY.md" note pattern common in this codebase).
References cited:
- GitGuardian exclusion annotation convention:
https://docs.gitguardian.com/internal-repositories-monitoring/integrations/cli/secrets
- CNCF Security TAG threat-model exception lifecycle:
https://github.com/cncf/tag-security/blob/main/community/working-groups/threat-modeling/templates/threats.mdCloses#115 (together with feat and chore commits on this branch)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#115): exclude scanner's own test fixtures from diff-mode scan
Add */secret-scan-lint.test.cjs to should_skip_file(), consistent with
the existing exclusions for security-scan.test.cjs and
security-prompt-injection.test.cjs. The test fixture at line 465
contains a DATABASE_URL credential-shaped string that exercises the
Env Variable Leak detector — scanning it as live code is a false positive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>