Skip to content

Commit a36996e

Browse files
chore(security): land OpenSSF Scorecard hardening recipe (RAN-51) (#82)
- Add .github/workflows/security.yml — OSS-CLI stack (Semgrep, osv-scanner, Trivy, Gitleaks, jscpd, anchore/sbom-action), language-adapted for Go + React/TS, all actions SHA-pinned. - Realign scorecard.yml with the codeiq RAN-46 recipe: push-to-main + weekly cron + workflow_dispatch, step-security/harden-runner audit, SARIF -> Security tab, all actions SHA-pinned. - SECURITY.md gains a "Hardening references" section linking the scorecard, security, codeql, and dependabot configs plus .bestpractices.json (project 12628 — passing). - CLAUDE.md gains a "Security & Supply-Chain" section documenting the Scorecard baseline + target (Best Practices passing = hard gate; Scorecard observational with stretch >= 8.0/10 per board ruling). - .bestpractices.json gains the OpenSSF schema header (project_id 12628, level=passing, evidence map, audit metadata) on top of the existing per-criterion answers. Out-of-band repo settings flipped to satisfy the AC: - Branch protection on main: required_signatures enabled. - Repo-level Dependabot security updates: enabled. Recipe-validation lane for RAN-50 — replicates next to otelcontext, snipIT, vigil. Per-language deltas vs codeiq: - osv-scanner runs against go.mod + ui/package-lock.json. - semgrep adds p/golang and p/typescript; drops p/java. - jscpd targets cmd/ + internal/ + ui/src; format go,javascript,typescript. Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 0bd76ea commit a36996e

5 files changed

Lines changed: 303 additions & 23 deletions

File tree

.bestpractices.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,39 @@
11
{
2+
"$schema": "https://bestpractices.coreinfrastructure.org/projects.schema.json",
3+
"_comment": "OpenSSF Best Practices self-assessment for RandomCodeSpace/docsiq. Project is registered (id 12628) and the badge currently displays 'passing' — see https://www.bestpractices.dev/en/projects/12628. The audit_* fields below mirror the per-criterion answers submitted to the OpenSSF Best Practices site; refresh them whenever criteria scoring changes.",
4+
"project_id": 12628,
5+
"name": "docsiq",
6+
"description": "GraphRAG-powered documentation search tool — indexes PDF/DOCX/TXT/MD/web content into a knowledge graph with entity extraction, community detection, and vector embeddings, then answers queries via graph + vector search.",
7+
"homepage_url": "https://github.com/RandomCodeSpace/docsiq",
8+
"repo_url": "https://github.com/RandomCodeSpace/docsiq",
9+
"license": "MIT",
10+
"level": "passing",
11+
"badge_url": "https://www.bestpractices.dev/projects/12628/badge",
12+
"project_page_url": "https://www.bestpractices.dev/en/projects/12628",
13+
"evidence": {
14+
"vulnerability_report_process": "SECURITY.md",
15+
"license_file": "LICENSE",
16+
"code_of_conduct": "CODE_OF_CONDUCT.md",
17+
"contributing_guide": "CONTRIBUTING.md",
18+
"governance": "GOVERNANCE.md",
19+
"build_reproducible": "go build -tags sqlite_fts5 ./...",
20+
"ci_workflow": ".github/workflows/ci.yml",
21+
"code_scanning": ".github/workflows/codeql.yml",
22+
"supply_chain_scorecard": ".github/workflows/scorecard.yml",
23+
"oss_cli_security_stack": ".github/workflows/security.yml",
24+
"fuzz_testing": ".github/workflows/fuzz.yml",
25+
"dependency_updates": ".github/dependabot.yml",
26+
"secret_scanning": "GitHub repo setting (secret_scanning + push_protection enabled)",
27+
"private_vulnerability_reporting": "GitHub repo setting (security advisories enabled)",
28+
"signed_releases": "cosign keyless signing via .github/workflows/release.yml + Sigstore Rekor"
29+
},
30+
"audit": {
31+
"self_assessment_date": "2026-04-26",
32+
"self_assessment_author": "TechLead (RAN-51)",
33+
"ran_50_lane": "RAN-51 (recipe validation; replicates to otelcontext, snipIT, vigil)",
34+
"scorecard_dashboard": "https://scorecard.dev/viewer/?uri=github.com/RandomCodeSpace/docsiq"
35+
},
36+
237
"description_good_status": "Met",
338
"description_good_justification": "See README.md. docsiq is a GraphRAG-powered documentation search tool written in Go that indexes PDF/DOCX/TXT/MD/web content into a knowledge graph with entity extraction, community detection, and vector embeddings, then answers queries via graph + vector search. https://github.com/RandomCodeSpace/docsiq/blob/main/README.md",
439

.github/workflows/scorecard.yml

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,68 @@
1-
name: scorecard
2-
3-
# Triggers:
4-
# - workflow_run on completed 'release' runs → scan fresh release assets
5-
# - weekly schedule (Mondays, 06:00 UTC) → backstop against drift
6-
# - branch_protection_rule changes → re-score when policy moves
7-
# - manual workflow_dispatch → on-demand
8-
# Not on every main push — most commits don't change release/scorecard-visible
9-
# state, so we were burning runner time publishing stale results.
1+
# OpenSSF Scorecard supply-chain analysis.
2+
# RAN-51 hardening lane (mirrors codeiq RAN-46 recipe).
3+
# Best-effort target — Scorecard does not gate merge; the OpenSSF Best
4+
# Practices badge is the only hard gate per board.
5+
# Docs: https://github.com/ossf/scorecard-action
6+
7+
name: Scorecard supply-chain security
8+
109
on:
11-
workflow_run:
12-
workflows: [release]
13-
types: [completed]
14-
branch_protection_rule:
10+
push:
11+
branches: [main]
1512
schedule:
13+
# Mondays 06:00 UTC
1614
- cron: '0 6 * * 1'
1715
workflow_dispatch:
1816

17+
# Restrict the default GITHUB_TOKEN to read-only; the steps below request
18+
# the narrow scopes they actually need.
1919
permissions: read-all
2020

2121
jobs:
2222
analysis:
23-
name: scorecard analysis
23+
name: Scorecard analysis
2424
runs-on: ubuntu-latest
2525
permissions:
26+
# Required to upload to the code-scanning Security tab.
2627
security-events: write
28+
# Required to read the OIDC token for publish_results.
2729
id-token: write
30+
# Default scopes for actions/checkout.
2831
contents: read
2932
actions: read
3033

3134
steps:
32-
- name: checkout
33-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
35+
- name: Harden runner egress
36+
# step-security/harden-runner v2.19.0
37+
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
38+
with:
39+
egress-policy: audit
40+
41+
- name: Checkout code
42+
# actions/checkout v6
43+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
3444
with:
3545
persist-credentials: false
3646

37-
- name: run scorecard
38-
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
47+
- name: Run Scorecard analysis
48+
# ossf/scorecard-action v2.4.3
49+
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a
3950
with:
4051
results_file: results.sarif
4152
results_format: sarif
53+
# Publish so results appear on the public Scorecard dashboard.
4254
publish_results: true
4355

44-
- name: upload artifact
45-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
56+
- name: Upload Scorecard SARIF (artifact)
57+
# actions/upload-artifact v4.6.2
58+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
4659
with:
47-
name: scorecard-results
60+
name: scorecard-sarif
4861
path: results.sarif
4962
retention-days: 5
5063

51-
- name: upload sarif to code scanning
52-
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
64+
- name: Upload SARIF to GitHub code-scanning
65+
# github/codeql-action/upload-sarif v4.35.2
66+
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225
5367
with:
5468
sarif_file: results.sarif

.github/workflows/security.yml

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
name: Security (OSS-CLI)
2+
# OSS-CLI security stack — RAN-51 hardening lane.
3+
# Mirrors the codeiq RAN-46 (B) OSS-CLI stack
4+
# (Semgrep + osv-scanner + Trivy + Gitleaks + jscpd + sbom-action),
5+
# adapted for docsiq's Go + React/TS shape.
6+
#
7+
# Six independent jobs — fail-fast off so every signal surfaces on a
8+
# single run. All actions SHA-pinned per Scorecard `Pinned-Dependencies`.
9+
# Top-level `permissions: read-all` per Scorecard `Token-Permissions`;
10+
# jobs scope up only when needed (gitleaks needs full git history,
11+
# the sbom job uploads artifacts).
12+
on:
13+
push:
14+
branches: [main]
15+
pull_request:
16+
branches: [main]
17+
schedule:
18+
- cron: '21 4 * * 1' # Mondays 04:21 UTC — catch newly-disclosed CVEs
19+
20+
permissions: read-all
21+
22+
jobs:
23+
osv-scanner:
24+
name: OSV-Scanner (SCA)
25+
runs-on: ubuntu-latest
26+
permissions:
27+
contents: read
28+
env:
29+
OSV_SCANNER_VERSION: 2.3.5
30+
GH_TOKEN: ${{ github.token }}
31+
steps:
32+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
33+
with:
34+
persist-credentials: false
35+
# Install osv-scanner from the official GitHub release (binary, not
36+
# the action — google/osv-scanner-action's `action.yml` is composite
37+
# and fails when invoked as a job step). Using the preinstalled
38+
# `gh` CLI avoids any external `curl`/`wget`.
39+
- name: Install osv-scanner
40+
run: |
41+
gh release download "v${OSV_SCANNER_VERSION}" \
42+
--repo google/osv-scanner \
43+
--pattern 'osv-scanner_linux_amd64' \
44+
--clobber
45+
mv osv-scanner_linux_amd64 osv-scanner
46+
chmod +x osv-scanner
47+
./osv-scanner --version
48+
# Scan the Go module graph and the embedded React UI's npm lockfile.
49+
# `--recursive` would also find ui/package-lock.json, but being
50+
# explicit keeps the AC §3 ("Zero High/Critical CVEs") evidence
51+
# trail clear: Go covered by go.mod, frontend by ui/package-lock.json,
52+
# cross-ecosystem reactive coverage by Dependabot security updates.
53+
- name: Run osv-scanner (Go modules)
54+
run: ./osv-scanner --lockfile=go.mod
55+
- name: Run osv-scanner (UI npm lockfile)
56+
run: ./osv-scanner --lockfile=ui/package-lock.json
57+
58+
trivy:
59+
name: Trivy (filesystem scan)
60+
runs-on: ubuntu-latest
61+
permissions:
62+
contents: read
63+
steps:
64+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
65+
with:
66+
persist-credentials: false
67+
- uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
68+
with:
69+
scan-type: fs
70+
scan-ref: .
71+
severity: HIGH,CRITICAL
72+
exit-code: '1'
73+
ignore-unfixed: true
74+
75+
semgrep:
76+
name: Semgrep (SAST)
77+
runs-on: ubuntu-latest
78+
permissions:
79+
contents: read
80+
steps:
81+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
82+
with:
83+
persist-credentials: false
84+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
85+
with:
86+
python-version: '3.12'
87+
- name: Install semgrep
88+
run: python -m pip install --quiet --upgrade pip semgrep
89+
- name: Run semgrep (security-audit + owasp-top-ten + golang + typescript)
90+
# `p/golang` covers Go-specific patterns (unsafe.Pointer, errcheck
91+
# families, taint flows). `p/typescript` covers the embedded React
92+
# UI under ui/. `p/security-audit` and `p/owasp-top-ten` are
93+
# language-agnostic. `--metrics off` keeps runs offline-friendly.
94+
run: |
95+
semgrep scan \
96+
--error \
97+
--config p/security-audit \
98+
--config p/owasp-top-ten \
99+
--config p/golang \
100+
--config p/typescript \
101+
--severity ERROR \
102+
--metrics off
103+
104+
gitleaks:
105+
name: Gitleaks (secret scan)
106+
runs-on: ubuntu-latest
107+
permissions:
108+
contents: read
109+
env:
110+
GITLEAKS_VERSION: 8.30.1
111+
GH_TOKEN: ${{ github.token }}
112+
steps:
113+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
114+
with:
115+
fetch-depth: 0
116+
persist-credentials: false
117+
# The official `gitleaks/gitleaks-action` requires a paid licence for
118+
# GitHub organisations. The underlying CLI is MIT-licensed; install
119+
# it directly from the upstream release using the preinstalled `gh`.
120+
- name: Install gitleaks
121+
run: |
122+
gh release download "v${GITLEAKS_VERSION}" \
123+
--repo gitleaks/gitleaks \
124+
--pattern "gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
125+
--output gitleaks.tar.gz
126+
tar -xzf gitleaks.tar.gz gitleaks
127+
chmod +x gitleaks
128+
- name: Run gitleaks (full git history)
129+
run: ./gitleaks detect --source . --redact --no-banner --exit-code 1
130+
131+
jscpd:
132+
name: jscpd (duplication < 3% on production code)
133+
runs-on: ubuntu-latest
134+
permissions:
135+
contents: read
136+
steps:
137+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
138+
with:
139+
persist-credentials: false
140+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
141+
with:
142+
node-version: '20'
143+
- name: Run jscpd
144+
# Scope to production code only: cmd/ + internal/ (Go) and ui/src/
145+
# (React/TS). Tests share fixture/assertion shape by design — that
146+
# parallelism catches contract regressions and is not a refactor
147+
# target. `--min-tokens 200` matches a meaningful method body, not
148+
# language scaffolding (package + imports + struct headers).
149+
run: |
150+
npx --yes jscpd@4 \
151+
--threshold 3 \
152+
--min-tokens 200 \
153+
--reporters consoleFull \
154+
--format "go,javascript,typescript" \
155+
--ignore "**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/testdata/**,**/*_test.go,**/*.test.ts,**/*.spec.ts,**/e2e/**,**/.next/**,**/vendor/**" \
156+
cmd internal ui/src
157+
158+
sbom:
159+
name: SBOM (SPDX + CycloneDX)
160+
runs-on: ubuntu-latest
161+
permissions:
162+
contents: read
163+
steps:
164+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
165+
with:
166+
persist-credentials: false
167+
- name: Generate SPDX SBOM
168+
uses: anchore/sbom-action@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7
169+
with:
170+
format: spdx-json
171+
output-file: sbom.spdx.json
172+
upload-artifact: false
173+
- name: Generate CycloneDX SBOM
174+
uses: anchore/sbom-action@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7
175+
with:
176+
format: cyclonedx-json
177+
output-file: sbom.cdx.json
178+
upload-artifact: false
179+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.6.2
180+
with:
181+
name: sbom
182+
path: |
183+
sbom.spdx.json
184+
sbom.cdx.json
185+
retention-days: 90

CLAUDE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,27 @@ internal/
5555
- Error wrapping: `fmt.Errorf("context: %w", err)`
5656
- Concurrency: use semaphore channels (`make(chan struct{}, N)`) for limiting parallelism
5757
- Config: Viper with `mapstructure` tags, env prefix `DOCSIQ_`
58+
59+
## Security & Supply-Chain
60+
61+
docsiq is registered with the OpenSSF Best Practices programme as project
62+
[12628](https://www.bestpractices.dev/en/projects/12628) and runs the OpenSSF
63+
Scorecard alongside the OSS-CLI security stack on every push to `main` plus
64+
weekly.
65+
66+
| Control | Source | Gate |
67+
|---|---|---|
68+
| OpenSSF Best Practices | [`.bestpractices.json`](.bestpractices.json) + project 12628 | **passing** badge — hard gate |
69+
| OpenSSF Scorecard | [`.github/workflows/scorecard.yml`](.github/workflows/scorecard.yml) (push + weekly cron) | Observational, baseline ≥ current published score, stretch ≥ 8.0/10 — does **not** block merge |
70+
| Semgrep / osv-scanner / Trivy / Gitleaks / jscpd / SBOM | [`.github/workflows/security.yml`](.github/workflows/security.yml) | High/Critical findings = block merge per `~/.claude/rules/security.md` |
71+
| CodeQL | [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml) | High/Critical findings = block merge |
72+
| Signed commits on `main` | Branch protection (GitHub repo setting) | Verify required |
73+
| Dependency updates | [`.github/dependabot.yml`](.github/dependabot.yml) (gomod + npm + github-actions, weekly) | Reactive |
74+
75+
Per the RAN-50 board ruling, the **OpenSSF Best Practices passing badge is the
76+
only hard supply-chain gate**; Scorecard is best-effort and tracked but does
77+
not block merge. Drops in the published Scorecard number trigger an
78+
investigation issue rather than a build failure.
79+
80+
For the disclosure policy, fix SLAs, and the full hardening reference list
81+
see [`SECURITY.md`](SECURITY.md).

SECURITY.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,25 @@ publicly as [GitHub Issues](https://github.com/RandomCodeSpace/docsiq/issues).
107107
Security reports are archived as
108108
[GitHub Security Advisories](https://github.com/RandomCodeSpace/docsiq/security/advisories)
109109
after coordinated disclosure.
110+
111+
## Hardening references
112+
113+
The supply-chain and code-quality controls backing this policy:
114+
115+
- [`.github/workflows/scorecard.yml`](.github/workflows/scorecard.yml)
116+
OpenSSF Scorecard analysis (push to `main` + weekly cron, SARIF
117+
uploaded to the GitHub Security tab).
118+
- [`.github/workflows/security.yml`](.github/workflows/security.yml)
119+
OSS-CLI security stack: Semgrep (SAST), osv-scanner (SCA), Trivy
120+
(filesystem CVE), Gitleaks (secrets), jscpd (duplication),
121+
`anchore/sbom-action` (SPDX + CycloneDX SBOMs).
122+
- [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml)
123+
GitHub CodeQL code scanning, SARIF in the Security tab.
124+
- [`.github/dependabot.yml`](.github/dependabot.yml) — automated
125+
dependency, GitHub Actions, and npm bumps (weekly).
126+
- [`.bestpractices.json`](.bestpractices.json) — OpenSSF Best Practices
127+
self-assessment (project
128+
[12628](https://www.bestpractices.dev/en/projects/12628), badge
129+
status: passing).
130+
- GitHub repo settings — secret scanning, push protection, and private
131+
vulnerability reporting are enabled at the repo level.

0 commit comments

Comments
 (0)