Add supply-chain controls, audit policy, and release checklist - #701
Conversation
|
@ChaoLing140 is attempting to deploy a commit to the ritik4ever's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@floxxih Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
📝 WalkthroughWalkthroughThis pull request implements four interconnected operational improvements: GitHub Actions supply-chain artifact generation with SBOMs and attestations, npm audit baseline enforcement with CI gating, a release checklist template, and supporting documentation for repeatable maintainer workflows across the repository. ChangesSupply-chain operations and release automation
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/build.yml:
- Around line 47-51: The workflow step named "Install Rust SBOM tooling"
currently installs the floating `stable` toolchain and installs `cargo-sbom`
without a fixed version; change that step to pin Rust to a specific toolchain
release (e.g., use a rust-toolchain.toml or replace `rustup toolchain install
stable`/`rustup default stable` with a concrete toolchain like `1.72.1`), and
install cargo-sbom with an explicit version by adding `--version <x.y.z>` to the
`cargo install cargo-sbom --locked` command so SBOM builds are reproducible.
- Around line 64-90: The workflow currently references actions/attest@v4 and
actions/upload-artifact@v4 by tag; update each uses: entry (all occurrences of
"uses: actions/attest@v4" and "uses: actions/upload-artifact@v4") to the
corresponding full commit SHA (e.g., actions/attest@<sha> and
actions/upload-artifact@<sha>) to pin to an immutable revision; fetch the
canonical commit SHAs from the respective GitHub action repos/releases, replace
the tag references for the four attest steps and the upload step, and keep the
existing with: inputs unchanged.
In @.github/workflows/npm-audit-policy.yml:
- Around line 34-38: Replace floating action tags actions/checkout@v4 and
actions/setup-node@v4 with their respective pinned commit SHAs or digests (e.g.,
actions/checkout@<full-sha> and actions/setup-node@<full-sha>) and add
persist-credentials: false to the checkout step (the actions/checkout
invocation) to disable credential persistence; keep the node-version: 20 input
on the setup-node step when pinning.
In `@docs/OPERATIONS.md`:
- Around line 117-120: Update the "Verification" section to include an exact,
maintainer-ready command and the identity checks to run; add a sample invocation
using GitHub CLI's attestation verify (e.g., "gh attestation verify <artifact>
--attestation <attestation-file> --source <repo>@<workflow>@<commit>") and list
the specific checks to perform: confirm artifact name and version match, verify
the attestation subject matches the artifact digest, and validate the
attestation provenance (repository, workflow name, and commit SHA) against the
expected repo/workflow/commit values produced by CI so a maintainer can
copy-paste and run the verification reproducibly.
In `@scripts/check-npm-audit-baseline.mjs`:
- Around line 91-95: The current writeBaseline block unconditionally updates
security/npm-audit-baseline.json (using writeFileSync with baselinePath and
nextBaseline.generatedAt) even when some scope exceeded the allowed baseline,
producing a partial/incorrect baseline; change the logic so the file is written
only when the overall run succeeded (no scopes exceeded or no failures) by
gating the write behind the success condition (e.g., check the existing
boolean/flag that indicates "all scopes within baseline" or introduce one like
allScopesWithinBaseline) before calling writeFileSync and setting
nextBaseline.generatedAt; apply the same guarded-write change to the other write
site referenced (the lines around 97-99) so the baseline is never updated on
failed runs.
- Around line 34-57: The current audit parsing treats missing vulnerability
metadata as zeros; change this to fail-fast by validating the spawn result and
parsed report before computing counts: after calling spawnSync('npm'...) check
result.error and that result.status === 0 (or result.signal) and throw if the
subprocess failed or timed out; after JSON.parse, if report.error exists or
report.metadata?.vulnerabilities is falsy, throw an informative Error for
scopeName including the raw output; only then iterate severities to build
counts. Reference symbols: spawnSync call, result, output, report,
report.metadata?.vulnerabilities, severities, counts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 461e2272-8cd7-422e-a329-564e86e67ece
📒 Files selected for processing (11)
.github/workflows/build.yml.github/workflows/npm-audit-policy.ymlCHANGELOG.mdbackend/package.jsoncontracts/Makefiledocs/CONTRIBUTING.mddocs/OPERATIONS.mddocs/RELEASE_CHECKLIST.mdpackage.jsonscripts/check-npm-audit-baseline.mjssecurity/npm-audit-baseline.json
| - name: Install Rust SBOM tooling | ||
| run: | | ||
| rustup toolchain install stable --profile minimal | ||
| rustup default stable | ||
| cargo install cargo-sbom --locked |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the workflow section around the referenced lines
echo "=== build.yml lines 1-120 (for context) ==="
nl -ba .github/workflows/build.yml | sed -n '1,120p'
# Confirm exactly how cargo-sbom is installed
echo "=== Search for cargo-sbom install / usage ==="
rg -n --hidden --glob '!.git/*' "cargo-sbom|rustup toolchain install|rust-toolchain" .
# Check for any checked-in Rust toolchain files
echo "=== Look for rust-toolchain files ==="
ls -la rust-toolchain rust-toolchain.toml .tool-versions 2>/dev/null || trueRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 188
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the workflow section around the referenced lines (no nl available)
echo "=== build.yml lines 1-120 (for context) ==="
awk 'NR<=120{printf "%6d %s\n", NR, $0}' .github/workflows/build.yml
echo "=== Search for cargo-sbom install / usage ==="
rg -n --hidden --glob '!.git/*' "cargo-sbom|rustup toolchain install|rust-toolchain|toolchain" .
echo "=== Look for rust-toolchain / tool-versions files ==="
ls -la rust-toolchain rust-toolchain.toml .tool-versions 2>/dev/null || trueRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 4341
Pin Rust SBOM tooling versions for reproducible builds.
Lines 47-51 install the moving stable Rust toolchain and run cargo install cargo-sbom --locked without pinning a specific cargo-sbom version, so SBOM output can drift across runs. Pin Rust to an exact release (e.g., rust-toolchain.toml or a fixed toolchain in the workflow) and install cargo-sbom with --version <...>.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build.yml around lines 47 - 51, The workflow step named
"Install Rust SBOM tooling" currently installs the floating `stable` toolchain
and installs `cargo-sbom` without a fixed version; change that step to pin Rust
to a specific toolchain release (e.g., use a rust-toolchain.toml or replace
`rustup toolchain install stable`/`rustup default stable` with a concrete
toolchain like `1.72.1`), and install cargo-sbom with an explicit version by
adding `--version <x.y.z>` to the `cargo install cargo-sbom --locked` command so
SBOM builds are reproducible.
| - name: Attest frontend bundle provenance | ||
| uses: actions/attest@v4 | ||
| with: | ||
| subject-path: artifacts/frontend-dist.tar.gz | ||
|
|
||
| - name: Attest backend bundle provenance | ||
| uses: actions/attest@v4 | ||
| with: | ||
| subject-path: artifacts/backend-dist.tar.gz | ||
|
|
||
| - name: Attest frontend SBOM | ||
| uses: actions/attest@v4 | ||
| with: | ||
| sbom-path: artifacts/sbom/frontend.cdx.json | ||
|
|
||
| - name: Attest backend SBOM | ||
| uses: actions/attest@v4 | ||
| with: | ||
| sbom-path: artifacts/sbom/backend.cdx.json | ||
|
|
||
| - name: Attest contracts SBOM | ||
| uses: actions/attest@v4 | ||
| with: | ||
| sbom-path: artifacts/sbom/contracts.cdx.json | ||
|
|
||
| - name: Upload build and supply-chain artifacts | ||
| uses: actions/upload-artifact@v4 |
There was a problem hiding this comment.
Pin actions/attest and actions/upload-artifact to full commit SHAs in this workflow.
In .github/workflows/build.yml (lines 64-90), the uses: steps for actions/attest@v4 and actions/upload-artifact@v4 are only tag-pinned; pin them to the exact commit SHA(s) to prevent supply-chain trust drift.
🧰 Tools
🪛 zizmor (1.25.2)
[error] 65-65: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 70-70: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 75-75: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 80-80: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 85-85: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 90-90: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build.yml around lines 64 - 90, The workflow currently
references actions/attest@v4 and actions/upload-artifact@v4 by tag; update each
uses: entry (all occurrences of "uses: actions/attest@v4" and "uses:
actions/upload-artifact@v4") to the corresponding full commit SHA (e.g.,
actions/attest@<sha> and actions/upload-artifact@<sha>) to pin to an immutable
revision; fetch the canonical commit SHAs from the respective GitHub action
repos/releases, replace the tag references for the four attest steps and the
upload step, and keep the existing with: inputs unchanged.
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
sed -n '33,39p' .github/workflows/npm-audit-policy.ymlRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 207
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/npm-audit-policy.yml"
nl -ba "$FILE" | sed -n '1,200p'
echo "---- grep persist-credentials ----"
rg -n "persist-credentials|checkout@|setup-node@" "$FILE" || trueRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 126
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/npm-audit-policy.yml"
echo "---- file (numbered, first 160 lines) ----"
cat -n "$FILE" | sed -n '1,160p'
echo "---- grep patterns ----"
rg -n "persist-credentials|checkout@|setup-node@" "$FILE" || trueRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 1485
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "^\\s*- uses:" .github/workflows/npm-audit-policy.ymlRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 159
Pin and harden the workflow actions (checkout).
This workflow uses floating tags (actions/checkout@v4, actions/setup-node@v4) and the checkout step does not set persist-credentials: false. Pin both actions to full commit SHAs/digests and disable credential persistence on checkout.
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 34-34: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 34-34: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 36-36: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/npm-audit-policy.yml around lines 34 - 38, Replace
floating action tags actions/checkout@v4 and actions/setup-node@v4 with their
respective pinned commit SHAs or digests (e.g., actions/checkout@<full-sha> and
actions/setup-node@<full-sha>) and add persist-credentials: false to the
checkout step (the actions/checkout invocation) to disable credential
persistence; keep the node-version: 20 input on the setup-node step when
pinning.
| ### Verification | ||
|
|
||
| Use GitHub's attestation tooling to verify a downloaded artifact against the repository's published attestations. The workflow stores the attestation on the run; verification is a maintainer task, not a manual build step. | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== File preview: docs/OPERATIONS.md (around lines 90-150) ==="
sed -n '90,150p' docs/OPERATIONS.md | cat -n
echo
echo "=== Search: attestation/gh attestation/verify in docs ==="
rg -n --hidden --no-ignore -S "attestation|gh attestation|attestations|verify" docs || trueRepository: ritik4ever/stellar-portfolio-rebalancer
Length of output: 9416
Document the exact artifact attestation verification command and checks
In docs/OPERATIONS.md (Verification section), add the specific maintainer-ready invocation (e.g., gh attestation verify ..., matching what the CI generates) plus the required identity/subject checks to validate (artifact name/version, and the expected repo/workflow/commit identity). [This] keeps the verification step executable and repeatable from the handbook.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/OPERATIONS.md` around lines 117 - 120, Update the "Verification" section
to include an exact, maintainer-ready command and the identity checks to run;
add a sample invocation using GitHub CLI's attestation verify (e.g., "gh
attestation verify <artifact> --attestation <attestation-file> --source
<repo>@<workflow>@<commit>") and list the specific checks to perform: confirm
artifact name and version match, verify the attestation subject matches the
artifact digest, and validate the attestation provenance (repository, workflow
name, and commit SHA) against the expected repo/workflow/commit values produced
by CI so a maintainer can copy-paste and run the verification reproducibly.
| const result = spawnSync('npm', ['audit', '--json', '--omit=dev'], { | ||
| cwd: resolve(repoRoot, scope.path), | ||
| encoding: 'utf8', | ||
| shell: process.platform === 'win32', | ||
| }) | ||
|
|
||
| const output = (result.stdout || result.stderr || '').trim() | ||
| if (!output) { | ||
| throw new Error(`npm audit produced no output for scope "${scopeName}"`) | ||
| } | ||
|
|
||
| let report | ||
| try { | ||
| report = JSON.parse(output) | ||
| } catch (error) { | ||
| throw new Error(`Failed to parse npm audit output for scope "${scopeName}": ${error.message}\n${output}`) | ||
| } | ||
|
|
||
| const counts = {} | ||
| for (const severity of severities) { | ||
| counts[severity] = report.metadata?.vulnerabilities?.[severity] ?? 0 | ||
| } | ||
|
|
||
| return counts |
There was a problem hiding this comment.
Fail closed when npm audit itself fails.
Lines 34-54 treat missing vulnerability metadata as zero counts. That lets registry/auth/lockfile failures pass the policy gate if npm audit returns an error payload or the subprocess fails before producing the expected JSON shape. This check should hard-fail on result.error, report.error, missing metadata.vulnerabilities, and ideally on a bounded timeout as well.
Suggested fix
function runAudit(scopeName) {
const scope = baseline.scopes[scopeName]
if (!scope) {
throw new Error(`Unknown audit scope "${scopeName}"`)
}
const result = spawnSync('npm', ['audit', '--json', '--omit=dev'], {
cwd: resolve(repoRoot, scope.path),
encoding: 'utf8',
shell: process.platform === 'win32',
+ timeout: 120_000,
})
+
+ if (result.error) {
+ throw new Error(
+ `Failed to run npm audit for scope "${scopeName}": ${result.error.message}`
+ )
+ }
const output = (result.stdout || result.stderr || '').trim()
if (!output) {
throw new Error(`npm audit produced no output for scope "${scopeName}"`)
}
@@
try {
report = JSON.parse(output)
} catch (error) {
throw new Error(`Failed to parse npm audit output for scope "${scopeName}": ${error.message}\n${output}`)
}
+
+ if (report.error) {
+ throw new Error(
+ `npm audit failed for scope "${scopeName}": ${report.error.summary || report.error.code || 'unknown error'}`
+ )
+ }
+
+ if (!report.metadata?.vulnerabilities) {
+ throw new Error(`npm audit did not return vulnerability metadata for scope "${scopeName}"`)
+ }
const counts = {}
for (const severity of severities) {
- counts[severity] = report.metadata?.vulnerabilities?.[severity] ?? 0
+ counts[severity] = report.metadata.vulnerabilities[severity] ?? 0
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const result = spawnSync('npm', ['audit', '--json', '--omit=dev'], { | |
| cwd: resolve(repoRoot, scope.path), | |
| encoding: 'utf8', | |
| shell: process.platform === 'win32', | |
| }) | |
| const output = (result.stdout || result.stderr || '').trim() | |
| if (!output) { | |
| throw new Error(`npm audit produced no output for scope "${scopeName}"`) | |
| } | |
| let report | |
| try { | |
| report = JSON.parse(output) | |
| } catch (error) { | |
| throw new Error(`Failed to parse npm audit output for scope "${scopeName}": ${error.message}\n${output}`) | |
| } | |
| const counts = {} | |
| for (const severity of severities) { | |
| counts[severity] = report.metadata?.vulnerabilities?.[severity] ?? 0 | |
| } | |
| return counts | |
| const result = spawnSync('npm', ['audit', '--json', '--omit=dev'], { | |
| cwd: resolve(repoRoot, scope.path), | |
| encoding: 'utf8', | |
| shell: process.platform === 'win32', | |
| timeout: 120_000, | |
| }) | |
| if (result.error) { | |
| throw new Error( | |
| `Failed to run npm audit for scope "${scopeName}": ${result.error.message}` | |
| ) | |
| } | |
| const output = (result.stdout || result.stderr || '').trim() | |
| if (!output) { | |
| throw new Error(`npm audit produced no output for scope "${scopeName}"`) | |
| } | |
| let report | |
| try { | |
| report = JSON.parse(output) | |
| } catch (error) { | |
| throw new Error(`Failed to parse npm audit output for scope "${scopeName}": ${error.message}\n${output}`) | |
| } | |
| if (report.error) { | |
| throw new Error( | |
| `npm audit failed for scope "${scopeName}": ${report.error.summary || report.error.code || 'unknown error'}` | |
| ) | |
| } | |
| if (!report.metadata?.vulnerabilities) { | |
| throw new Error(`npm audit did not return vulnerability metadata for scope "${scopeName}"`) | |
| } | |
| const counts = {} | |
| for (const severity of severities) { | |
| counts[severity] = report.metadata.vulnerabilities[severity] ?? 0 | |
| } | |
| return counts |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/check-npm-audit-baseline.mjs` around lines 34 - 57, The current audit
parsing treats missing vulnerability metadata as zeros; change this to fail-fast
by validating the spawn result and parsed report before computing counts: after
calling spawnSync('npm'...) check result.error and that result.status === 0 (or
result.signal) and throw if the subprocess failed or timed out; after
JSON.parse, if report.error exists or report.metadata?.vulnerabilities is falsy,
throw an informative Error for scopeName including the raw output; only then
iterate severities to build counts. Reference symbols: spawnSync call, result,
output, report, report.metadata?.vulnerabilities, severities, counts.
| if (writeBaseline) { | ||
| nextBaseline.generatedAt = new Date().toISOString() | ||
| writeFileSync(baselinePath, `${JSON.stringify(nextBaseline, null, 2)}\n`) | ||
| console.log(`[audit-policy] Baseline refreshed at ${baselinePath}`) | ||
| } |
There was a problem hiding this comment.
Do not persist a partial baseline on failed runs.
With --write-baseline, Lines 91-95 still rewrite the JSON even when another scope exceeded the baseline. A failed update run should leave security/npm-audit-baseline.json untouched; otherwise maintainers can accidentally commit partial baseline drift with a fresh generatedAt.
Suggested fix
-if (writeBaseline) {
- nextBaseline.generatedAt = new Date().toISOString()
- writeFileSync(baselinePath, `${JSON.stringify(nextBaseline, null, 2)}\n`)
- console.log(`[audit-policy] Baseline refreshed at ${baselinePath}`)
-}
-
if (hasFailure) {
console.error('[audit-policy] Update the baseline only after the findings are reviewed and accepted.')
process.exit(1)
}
+
+if (writeBaseline) {
+ nextBaseline.generatedAt = new Date().toISOString()
+ writeFileSync(baselinePath, `${JSON.stringify(nextBaseline, null, 2)}\n`)
+ console.log(`[audit-policy] Baseline refreshed at ${baselinePath}`)
+}Also applies to: 97-99
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/check-npm-audit-baseline.mjs` around lines 91 - 95, The current
writeBaseline block unconditionally updates security/npm-audit-baseline.json
(using writeFileSync with baselinePath and nextBaseline.generatedAt) even when
some scope exceeded the allowed baseline, producing a partial/incorrect
baseline; change the logic so the file is written only when the overall run
succeeded (no scopes exceeded or no failures) by gating the write behind the
success condition (e.g., check the existing boolean/flag that indicates "all
scopes within baseline" or introduce one like allScopesWithinBaseline) before
calling writeFileSync and setting nextBaseline.generatedAt; apply the same
guarded-write change to the other write site referenced (the lines around 97-99)
so the baseline is never updated on failed runs.
Summary:
Verification:
npm run audit:policyCloses #545
Closes #546
Closes #544
Closes #547
Summary by CodeRabbit
New Features
Documentation