From aad880a743fd2ed4a9da91dfef609b06dc8e5d35 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 26 Apr 2026 02:21:38 +0000 Subject: [PATCH] chore(security): land OpenSSF Scorecard hardening recipe (RAN-51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .bestpractices.json | 35 ++++++ .github/workflows/scorecard.yml | 60 +++++++---- .github/workflows/security.yml | 185 ++++++++++++++++++++++++++++++++ CLAUDE.md | 24 +++++ SECURITY.md | 22 ++++ 5 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/security.yml diff --git a/.bestpractices.json b/.bestpractices.json index 6103aef..d2c70db 100644 --- a/.bestpractices.json +++ b/.bestpractices.json @@ -1,4 +1,39 @@ { + "$schema": "https://bestpractices.coreinfrastructure.org/projects.schema.json", + "_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.", + "project_id": 12628, + "name": "docsiq", + "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.", + "homepage_url": "https://github.com/RandomCodeSpace/docsiq", + "repo_url": "https://github.com/RandomCodeSpace/docsiq", + "license": "MIT", + "level": "passing", + "badge_url": "https://www.bestpractices.dev/projects/12628/badge", + "project_page_url": "https://www.bestpractices.dev/en/projects/12628", + "evidence": { + "vulnerability_report_process": "SECURITY.md", + "license_file": "LICENSE", + "code_of_conduct": "CODE_OF_CONDUCT.md", + "contributing_guide": "CONTRIBUTING.md", + "governance": "GOVERNANCE.md", + "build_reproducible": "go build -tags sqlite_fts5 ./...", + "ci_workflow": ".github/workflows/ci.yml", + "code_scanning": ".github/workflows/codeql.yml", + "supply_chain_scorecard": ".github/workflows/scorecard.yml", + "oss_cli_security_stack": ".github/workflows/security.yml", + "fuzz_testing": ".github/workflows/fuzz.yml", + "dependency_updates": ".github/dependabot.yml", + "secret_scanning": "GitHub repo setting (secret_scanning + push_protection enabled)", + "private_vulnerability_reporting": "GitHub repo setting (security advisories enabled)", + "signed_releases": "cosign keyless signing via .github/workflows/release.yml + Sigstore Rekor" + }, + "audit": { + "self_assessment_date": "2026-04-26", + "self_assessment_author": "TechLead (RAN-51)", + "ran_50_lane": "RAN-51 (recipe validation; replicates to otelcontext, snipIT, vigil)", + "scorecard_dashboard": "https://scorecard.dev/viewer/?uri=github.com/RandomCodeSpace/docsiq" + }, + "description_good_status": "Met", "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", diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b239ea4..929e7a7 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,54 +1,68 @@ -name: scorecard - -# Triggers: -# - workflow_run on completed 'release' runs → scan fresh release assets -# - weekly schedule (Mondays, 06:00 UTC) → backstop against drift -# - branch_protection_rule changes → re-score when policy moves -# - manual workflow_dispatch → on-demand -# Not on every main push — most commits don't change release/scorecard-visible -# state, so we were burning runner time publishing stale results. +# OpenSSF Scorecard supply-chain analysis. +# RAN-51 hardening lane (mirrors codeiq RAN-46 recipe). +# Best-effort target — Scorecard does not gate merge; the OpenSSF Best +# Practices badge is the only hard gate per board. +# Docs: https://github.com/ossf/scorecard-action + +name: Scorecard supply-chain security + on: - workflow_run: - workflows: [release] - types: [completed] - branch_protection_rule: + push: + branches: [main] schedule: + # Mondays 06:00 UTC - cron: '0 6 * * 1' workflow_dispatch: +# Restrict the default GITHUB_TOKEN to read-only; the steps below request +# the narrow scopes they actually need. permissions: read-all jobs: analysis: - name: scorecard analysis + name: Scorecard analysis runs-on: ubuntu-latest permissions: + # Required to upload to the code-scanning Security tab. security-events: write + # Required to read the OIDC token for publish_results. id-token: write + # Default scopes for actions/checkout. contents: read actions: read steps: - - name: checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Harden runner egress + # step-security/harden-runner v2.19.0 + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 + with: + egress-policy: audit + + - name: Checkout code + # actions/checkout v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - - name: run scorecard - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + - name: Run Scorecard analysis + # ossf/scorecard-action v2.4.3 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a with: results_file: results.sarif results_format: sarif + # Publish so results appear on the public Scorecard dashboard. publish_results: true - - name: upload artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + - name: Upload Scorecard SARIF (artifact) + # actions/upload-artifact v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: - name: scorecard-results + name: scorecard-sarif path: results.sarif retention-days: 5 - - name: upload sarif to code scanning - uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + - name: Upload SARIF to GitHub code-scanning + # github/codeql-action/upload-sarif v4.35.2 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 with: sarif_file: results.sarif diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..2f6b020 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,185 @@ +name: Security (OSS-CLI) +# OSS-CLI security stack — RAN-51 hardening lane. +# Mirrors the codeiq RAN-46 (B) OSS-CLI stack +# (Semgrep + osv-scanner + Trivy + Gitleaks + jscpd + sbom-action), +# adapted for docsiq's Go + React/TS shape. +# +# Six independent jobs — fail-fast off so every signal surfaces on a +# single run. All actions SHA-pinned per Scorecard `Pinned-Dependencies`. +# Top-level `permissions: read-all` per Scorecard `Token-Permissions`; +# jobs scope up only when needed (gitleaks needs full git history, +# the sbom job uploads artifacts). +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '21 4 * * 1' # Mondays 04:21 UTC — catch newly-disclosed CVEs + +permissions: read-all + +jobs: + osv-scanner: + name: OSV-Scanner (SCA) + runs-on: ubuntu-latest + permissions: + contents: read + env: + OSV_SCANNER_VERSION: 2.3.5 + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + # Install osv-scanner from the official GitHub release (binary, not + # the action — google/osv-scanner-action's `action.yml` is composite + # and fails when invoked as a job step). Using the preinstalled + # `gh` CLI avoids any external `curl`/`wget`. + - name: Install osv-scanner + run: | + gh release download "v${OSV_SCANNER_VERSION}" \ + --repo google/osv-scanner \ + --pattern 'osv-scanner_linux_amd64' \ + --clobber + mv osv-scanner_linux_amd64 osv-scanner + chmod +x osv-scanner + ./osv-scanner --version + # Scan the Go module graph and the embedded React UI's npm lockfile. + # `--recursive` would also find ui/package-lock.json, but being + # explicit keeps the AC §3 ("Zero High/Critical CVEs") evidence + # trail clear: Go covered by go.mod, frontend by ui/package-lock.json, + # cross-ecosystem reactive coverage by Dependabot security updates. + - name: Run osv-scanner (Go modules) + run: ./osv-scanner --lockfile=go.mod + - name: Run osv-scanner (UI npm lockfile) + run: ./osv-scanner --lockfile=ui/package-lock.json + + trivy: + name: Trivy (filesystem scan) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: . + severity: HIGH,CRITICAL + exit-code: '1' + ignore-unfixed: true + + semgrep: + name: Semgrep (SAST) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + - name: Install semgrep + run: python -m pip install --quiet --upgrade pip semgrep + - name: Run semgrep (security-audit + owasp-top-ten + golang + typescript) + # `p/golang` covers Go-specific patterns (unsafe.Pointer, errcheck + # families, taint flows). `p/typescript` covers the embedded React + # UI under ui/. `p/security-audit` and `p/owasp-top-ten` are + # language-agnostic. `--metrics off` keeps runs offline-friendly. + run: | + semgrep scan \ + --error \ + --config p/security-audit \ + --config p/owasp-top-ten \ + --config p/golang \ + --config p/typescript \ + --severity ERROR \ + --metrics off + + gitleaks: + name: Gitleaks (secret scan) + runs-on: ubuntu-latest + permissions: + contents: read + env: + GITLEAKS_VERSION: 8.30.1 + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + # The official `gitleaks/gitleaks-action` requires a paid licence for + # GitHub organisations. The underlying CLI is MIT-licensed; install + # it directly from the upstream release using the preinstalled `gh`. + - name: Install gitleaks + run: | + gh release download "v${GITLEAKS_VERSION}" \ + --repo gitleaks/gitleaks \ + --pattern "gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + --output gitleaks.tar.gz + tar -xzf gitleaks.tar.gz gitleaks + chmod +x gitleaks + - name: Run gitleaks (full git history) + run: ./gitleaks detect --source . --redact --no-banner --exit-code 1 + + jscpd: + name: jscpd (duplication < 3% on production code) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '20' + - name: Run jscpd + # Scope to production code only: cmd/ + internal/ (Go) and ui/src/ + # (React/TS). Tests share fixture/assertion shape by design — that + # parallelism catches contract regressions and is not a refactor + # target. `--min-tokens 200` matches a meaningful method body, not + # language scaffolding (package + imports + struct headers). + run: | + npx --yes jscpd@4 \ + --threshold 3 \ + --min-tokens 200 \ + --reporters consoleFull \ + --format "go,javascript,typescript" \ + --ignore "**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/testdata/**,**/*_test.go,**/*.test.ts,**/*.spec.ts,**/e2e/**,**/.next/**,**/vendor/**" \ + cmd internal ui/src + + sbom: + name: SBOM (SPDX + CycloneDX) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - name: Generate SPDX SBOM + uses: anchore/sbom-action@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 + with: + format: spdx-json + output-file: sbom.spdx.json + upload-artifact: false + - name: Generate CycloneDX SBOM + uses: anchore/sbom-action@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 + with: + format: cyclonedx-json + output-file: sbom.cdx.json + upload-artifact: false + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.6.2 + with: + name: sbom + path: | + sbom.spdx.json + sbom.cdx.json + retention-days: 90 diff --git a/CLAUDE.md b/CLAUDE.md index 69998f9..2affb95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,3 +55,27 @@ internal/ - Error wrapping: `fmt.Errorf("context: %w", err)` - Concurrency: use semaphore channels (`make(chan struct{}, N)`) for limiting parallelism - Config: Viper with `mapstructure` tags, env prefix `DOCSIQ_` + +## Security & Supply-Chain + +docsiq is registered with the OpenSSF Best Practices programme as project +[12628](https://www.bestpractices.dev/en/projects/12628) and runs the OpenSSF +Scorecard alongside the OSS-CLI security stack on every push to `main` plus +weekly. + +| Control | Source | Gate | +|---|---|---| +| OpenSSF Best Practices | [`.bestpractices.json`](.bestpractices.json) + project 12628 | **passing** badge — hard gate | +| 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 | +| 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` | +| CodeQL | [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml) | High/Critical findings = block merge | +| Signed commits on `main` | Branch protection (GitHub repo setting) | Verify required | +| Dependency updates | [`.github/dependabot.yml`](.github/dependabot.yml) (gomod + npm + github-actions, weekly) | Reactive | + +Per the RAN-50 board ruling, the **OpenSSF Best Practices passing badge is the +only hard supply-chain gate**; Scorecard is best-effort and tracked but does +not block merge. Drops in the published Scorecard number trigger an +investigation issue rather than a build failure. + +For the disclosure policy, fix SLAs, and the full hardening reference list +see [`SECURITY.md`](SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md index 6ae4c50..04d8af8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -107,3 +107,25 @@ publicly as [GitHub Issues](https://github.com/RandomCodeSpace/docsiq/issues). Security reports are archived as [GitHub Security Advisories](https://github.com/RandomCodeSpace/docsiq/security/advisories) after coordinated disclosure. + +## Hardening references + +The supply-chain and code-quality controls backing this policy: + +- [`.github/workflows/scorecard.yml`](.github/workflows/scorecard.yml) — + OpenSSF Scorecard analysis (push to `main` + weekly cron, SARIF + uploaded to the GitHub Security tab). +- [`.github/workflows/security.yml`](.github/workflows/security.yml) — + OSS-CLI security stack: Semgrep (SAST), osv-scanner (SCA), Trivy + (filesystem CVE), Gitleaks (secrets), jscpd (duplication), + `anchore/sbom-action` (SPDX + CycloneDX SBOMs). +- [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml) — + GitHub CodeQL code scanning, SARIF in the Security tab. +- [`.github/dependabot.yml`](.github/dependabot.yml) — automated + dependency, GitHub Actions, and npm bumps (weekly). +- [`.bestpractices.json`](.bestpractices.json) — OpenSSF Best Practices + self-assessment (project + [12628](https://www.bestpractices.dev/en/projects/12628), badge + status: passing). +- GitHub repo settings — secret scanning, push protection, and private + vulnerability reporting are enabled at the repo level.