-
Notifications
You must be signed in to change notification settings - Fork 0
168 lines (159 loc) · 6.82 KB
/
Copy pathsecurity-ci.yml
File metadata and controls
168 lines (159 loc) · 6.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# security-ci — enforce the APP-01 static security foundation.
#
# Evidence is bound to both the source head and the actual checkout SHA. For
# pull_request events the latter is GitHub's ephemeral merge commit.
name: security-ci
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions: {}
concurrency:
group: security-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
csp-enforcement:
name: hardened policy gate + security tests
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
env:
CI: "true"
SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
TESTED_SHA: ${{ github.sha }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.23.1"
cache: npm
- name: verify exact Node, npm, registry and lifecycle policy
run: npm run toolchain:check
- name: verify frozen lockfile invariants
run: npm run lockfile:check
- name: deterministic install with lifecycle scripts disabled
run: npm ci --ignore-scripts --audit=false --fund=false
- name: dependency vulnerability gate and snapshot
id: audit
run: npm run audit:ci
- name: preserve dependency audit snapshot even when the gate fails
if: ${{ !cancelled() && steps.audit.conclusion != 'skipped' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: app-01-dependency-audit-${{ env.SOURCE_SHA }}
path: npm-audit.json
if-no-files-found: error
retention-days: 90
- name: CSP, endpoint and security-header fail-closed gate
run: npm run csp:check
- name: serializer, served-header and regression tests
run: npm test
- name: generate SPDX SBOM and evidence manifest
run: |
set -euo pipefail
npm sbom --sbom-format=spdx > sbom.spdx.json
node <<'NODE'
const { execFileSync } = require('node:child_process');
const { createHash } = require('node:crypto');
const { readFileSync, writeFileSync } = require('node:fs');
const hash = (path) => {
const bytes = readFileSync(path);
return {
path,
bytes,
sha256: createHash('sha256').update(bytes).digest('hex'),
};
};
const sbomFile = hash('sbom.spdx.json');
const auditFile = hash('npm-audit.json');
const packageFile = hash('package.json');
const lockFile = hash('package-lock.json');
const npmrcFile = hash('.npmrc');
const sbom = JSON.parse(sbomFile.bytes.toString('utf8'));
const audit = JSON.parse(auditFile.bytes.toString('utf8'));
const vulnerabilities = audit.metadata?.vulnerabilities;
const evidence = {
schema_version: '1.2',
repository: process.env.GITHUB_REPOSITORY,
event_name: process.env.GITHUB_EVENT_NAME,
source_sha: process.env.SOURCE_SHA,
tested_sha: process.env.TESTED_SHA,
workflow: process.env.GITHUB_WORKFLOW,
run_id: process.env.GITHUB_RUN_ID,
run_attempt: process.env.GITHUB_RUN_ATTEMPT,
generated_at: new Date().toISOString(),
runner: {
os: process.env.RUNNER_OS,
arch: process.env.RUNNER_ARCH,
image_os: process.env.ImageOS || null,
image_version: process.env.ImageVersion || null,
kernel: execFileSync('uname', ['-srmo'], { encoding: 'utf8' }).trim(),
git: execFileSync('git', ['--version'], { encoding: 'utf8' }).trim(),
},
tools: {
node: process.version,
npm: execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim(),
registry: execFileSync('npm', ['config', 'get', 'registry'], { encoding: 'utf8' }).trim(),
},
inputs: {
package_json_sha256: packageFile.sha256,
package_lock_sha256: lockFile.sha256,
npmrc_sha256: npmrcFile.sha256,
},
audit: {
path: auditFile.path,
sha256: auditFile.sha256,
report_version: audit.auditReportVersion,
vulnerabilities,
},
sbom: {
path: sbomFile.path,
sha256: sbomFile.sha256,
spdx_version: sbom.spdxVersion,
document_name: sbom.name,
package_count: Array.isArray(sbom.packages) ? sbom.packages.length : null,
},
};
if (!evidence.source_sha || !evidence.tested_sha) {
throw new Error('source/tested commit identity is missing');
}
if (evidence.tools.node !== 'v22.23.1' || evidence.tools.npm !== '10.9.8') {
throw new Error(`unexpected evidence toolchain: ${JSON.stringify(evidence.tools)}`);
}
if (evidence.tools.registry !== 'https://registry.npmjs.org/') {
throw new Error(`unexpected npm registry: ${evidence.tools.registry}`);
}
if (evidence.runner.os !== 'Linux' || evidence.runner.arch !== 'X64') {
throw new Error(`unexpected runner platform: ${JSON.stringify(evidence.runner)}`);
}
if (audit.auditReportVersion !== 2 || !vulnerabilities) {
throw new Error('npm audit snapshot is missing expected schema metadata');
}
if ((vulnerabilities.high ?? 0) > 0 || (vulnerabilities.critical ?? 0) > 0) {
throw new Error(`high/critical vulnerabilities present: ${JSON.stringify(vulnerabilities)}`);
}
if (evidence.sbom.spdx_version !== 'SPDX-2.3') {
throw new Error(`unexpected SPDX version: ${evidence.sbom.spdx_version}`);
}
if (!Number.isInteger(evidence.sbom.package_count) || evidence.sbom.package_count < 1) {
throw new Error('SBOM package inventory is empty or malformed');
}
for (const file of [sbomFile, auditFile, packageFile, lockFile, npmrcFile]) {
delete file.bytes;
}
writeFileSync('security-evidence.json', `${JSON.stringify(evidence, null, 2)}\n`);
NODE
- name: upload immutable security evidence
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: app-01-security-evidence-${{ env.SOURCE_SHA }}
path: |
npm-audit.json
sbom.spdx.json
security-evidence.json
if-no-files-found: error
retention-days: 90