@sasjs/lint 4.1.3 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Audit Watch
|
|
2
|
+
|
|
3
|
+
# The build gate runs `npm audit --omit=dev`, because the dev tree carries one
|
|
4
|
+
# advisory that cannot be fixed from here (GHSA-hp3w-g68c-fv3c, sprintf-js - see
|
|
5
|
+
# the Security Audit step in build.yml). Nothing in build.yml re-checks the dev
|
|
6
|
+
# tree, so that exemption would otherwise be open-ended. This runs the full audit
|
|
7
|
+
# weekly and fails when the exemption stops matching reality: a new dev-tree
|
|
8
|
+
# advisory appears, an exempted one disappears, or a package named in the
|
|
9
|
+
# exemption publishes a newer release. Any of those files an issue - and only
|
|
10
|
+
# those, because a scheduled job that is red every week trains people to ignore
|
|
11
|
+
# red.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
schedule:
|
|
15
|
+
- cron: '0 8 * * 1' # Mondays, 08:00 UTC
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
watch:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
issues: write
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Use Node.js
|
|
29
|
+
uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: 22
|
|
32
|
+
|
|
33
|
+
- name: Install Dependencies
|
|
34
|
+
run: npm ci
|
|
35
|
+
|
|
36
|
+
- name: Re-check the dev-tree audit exemption
|
|
37
|
+
id: watch
|
|
38
|
+
env:
|
|
39
|
+
AUDIT_WATCH_REPORT: ${{ runner.temp }}/audit-watch-report.md
|
|
40
|
+
run: node scripts/audit-watch.mjs
|
|
41
|
+
|
|
42
|
+
- name: File an issue
|
|
43
|
+
# `always()` because the watched step has already failed, which would
|
|
44
|
+
# otherwise skip this one.
|
|
45
|
+
if: always() && steps.watch.outcome == 'failure'
|
|
46
|
+
env:
|
|
47
|
+
GH_TOKEN: ${{ github.token }}
|
|
48
|
+
REPORT: ${{ runner.temp }}/audit-watch-report.md
|
|
49
|
+
run: |
|
|
50
|
+
title='Audit watch: the dev-tree exemption needs re-evaluating'
|
|
51
|
+
existing=$(gh issue list --state open --search "\"$title\" in:title" \
|
|
52
|
+
--json number --jq '.[0].number // empty')
|
|
53
|
+
|
|
54
|
+
if [ -n "$existing" ]; then
|
|
55
|
+
echo "already tracked in #$existing"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [ -f "$REPORT" ]; then
|
|
60
|
+
gh issue create --title "$title" --body-file "$REPORT"
|
|
61
|
+
else
|
|
62
|
+
# The script writes a report even when the watch itself breaks, so
|
|
63
|
+
# this means it never started. File the issue anyway rather than
|
|
64
|
+
# failing on a missing path and filing nothing.
|
|
65
|
+
gh issue create --title "$title" --body \
|
|
66
|
+
"The audit watch step failed before producing a report. Check the run:
|
|
67
|
+
$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
68
|
+
fi
|
|
@@ -36,11 +36,26 @@ jobs:
|
|
|
36
36
|
# gitleaks-action requires a license key for organization repos.
|
|
37
37
|
run: node_modules/.bin/gitleaks detect --source . --redact --no-banner -v
|
|
38
38
|
- name: Security Audit
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
39
|
+
# Prod-scoped on purpose: this gate covers what actually ships. The
|
|
40
|
+
# release toolchain is not a project dependency (see publish.yml), so
|
|
41
|
+
# the `npm` package bundled by @semantic-release/npm never reaches this
|
|
42
|
+
# tree - which is what made the old exemption-based audit script
|
|
43
|
+
# necessary. The dev tree carries one advisory that cannot be fixed
|
|
44
|
+
# from here: GHSA-hp3w-g68c-fv3c, sprintf-js reached through
|
|
45
|
+
# @jest/transform -> babel-plugin-istanbul ->
|
|
46
|
+
# @istanbuljs/load-nyc-config -> js-yaml 3 -> argparse. Every package in
|
|
47
|
+
# that chain is at its latest published version, and the advisory lists
|
|
48
|
+
# no patched release at all (sprintf-js 1.1.3 is the newest and is
|
|
49
|
+
# still in range), so gating on it would mean a permanently red build.
|
|
50
|
+
# `npm audit fix --force` does not fix it either - it downgrades jest
|
|
51
|
+
# 30 -> 25 and takes the tree from 21 advisories to 45.
|
|
52
|
+
#
|
|
53
|
+
# The exemption is not open-ended: audit-watch.yml runs the full audit
|
|
54
|
+
# weekly and fails when this comment stops matching the tree - a new
|
|
55
|
+
# dev-tree advisory, this one disappearing, or sprintf-js publishing a
|
|
56
|
+
# newer release. Keep the GHSA id named here and in the EXEMPTIONS list
|
|
57
|
+
# of scripts/audit-watch.mjs.
|
|
58
|
+
run: npm audit --omit=dev --audit-level=low
|
|
44
59
|
- name: Check Code Style
|
|
45
60
|
run: npm run lint
|
|
46
61
|
- name: Run Unit Tests
|
package/package.json
CHANGED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watches the dev-tree audit exemption declared in `.github/workflows/build.yml`.
|
|
3
|
+
*
|
|
4
|
+
* That workflow gates on `npm audit --omit=dev` because the dev tree carries one
|
|
5
|
+
* advisory nothing in this repo can fix: GHSA-hp3w-g68c-fv3c (sprintf-js,
|
|
6
|
+
* reached through @jest/transform -> babel-plugin-istanbul ->
|
|
7
|
+
* @istanbuljs/load-nyc-config -> js-yaml 3 -> argparse). Every package in that
|
|
8
|
+
* chain is at its latest published version and the advisory lists no patched
|
|
9
|
+
* release, so gating on it would mean a permanently red build.
|
|
10
|
+
*
|
|
11
|
+
* An exemption with no expiry is how a scoped gate becomes a permanent one, so
|
|
12
|
+
* this script re-checks it weekly and reports when it is out of date:
|
|
13
|
+
*
|
|
14
|
+
* - a dev-tree advisory outside EXEMPTIONS appears - the exemption is too narrow
|
|
15
|
+
* - an EXEMPTIONS advisory no longer appears in the tree - the exemption is dead
|
|
16
|
+
* - a WATCHED_FOR_RELEASE package publishes a version newer than the one
|
|
17
|
+
* recorded here - a fix may now exist, so re-evaluate and drop the exemption
|
|
18
|
+
* - an EXEMPTIONS id is missing from the build.yml comment - the gate and this
|
|
19
|
+
* script disagree about what is exempt
|
|
20
|
+
*
|
|
21
|
+
* Any of those exits 1, failing the scheduled run and making the workflow file an
|
|
22
|
+
* issue. The known advisory on its own does NOT exit 1: it is expected, and a job
|
|
23
|
+
* that is red every week trains people to ignore red.
|
|
24
|
+
*
|
|
25
|
+
* Run with `node scripts/audit-watch.mjs`.
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process'
|
|
28
|
+
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
|
|
29
|
+
import path from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
33
|
+
const BUILD_WORKFLOW = path.join('.github', 'workflows', 'build.yml')
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Advisories the dev-scoped gate in build.yml is allowed to leave unaddressed.
|
|
37
|
+
* Each one must also be named in that workflow's comment - the check below
|
|
38
|
+
* enforces it, so the gate and this list cannot drift apart.
|
|
39
|
+
*/
|
|
40
|
+
const EXEMPTIONS = ['GHSA-hp3w-g68c-fv3c']
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Packages whose published version decides whether an exemption is still
|
|
44
|
+
* justified. `newestKnown` is the newest release at the time the exemption was
|
|
45
|
+
* written; a newer one on the registry means a fix may exist and the exemption
|
|
46
|
+
* deserves a fresh look rather than another year of silence.
|
|
47
|
+
*/
|
|
48
|
+
const WATCHED_FOR_RELEASE = [{ name: 'sprintf-js', newestKnown: '1.1.3' }]
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `npm audit --json` exits 1 whenever it reports anything, so the exit code says
|
|
52
|
+
* nothing about whether the command worked - parse the report instead.
|
|
53
|
+
*/
|
|
54
|
+
const runAudit = () => {
|
|
55
|
+
let stdout
|
|
56
|
+
try {
|
|
57
|
+
stdout = execFileSync('npm', ['audit', '--json'], {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
60
|
+
})
|
|
61
|
+
} catch (error) {
|
|
62
|
+
stdout = error.stdout
|
|
63
|
+
}
|
|
64
|
+
if (!stdout || !stdout.trim()) {
|
|
65
|
+
throw new Error('npm audit produced no JSON - run `npm ci` first')
|
|
66
|
+
}
|
|
67
|
+
return JSON.parse(stdout)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The advisories the report names, keyed by GHSA id. A `via` entry is a bare
|
|
72
|
+
* package name when the advisory is inherited from a dependency, and an object
|
|
73
|
+
* carrying the GHSA url when it is the root of the chain - only the latter are
|
|
74
|
+
* advisories in their own right.
|
|
75
|
+
*/
|
|
76
|
+
const collectAdvisories = (report) => {
|
|
77
|
+
const advisories = new Map()
|
|
78
|
+
|
|
79
|
+
for (const [packageName, info] of Object.entries(report?.vulnerabilities ?? {})) {
|
|
80
|
+
for (const via of info?.via ?? []) {
|
|
81
|
+
if (typeof via !== 'object' || via === null) continue
|
|
82
|
+
const id = String(via.url ?? '').split('/').pop()
|
|
83
|
+
if (!id?.startsWith('GHSA-')) continue
|
|
84
|
+
|
|
85
|
+
const entry = advisories.get(id) ?? {
|
|
86
|
+
severity: via.severity,
|
|
87
|
+
title: via.title,
|
|
88
|
+
packages: []
|
|
89
|
+
}
|
|
90
|
+
entry.packages.push(packageName)
|
|
91
|
+
advisories.set(id, entry)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return advisories
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The version the registry currently resolves `latest` to, or null if offline. */
|
|
99
|
+
const latestPublished = (name) => {
|
|
100
|
+
try {
|
|
101
|
+
return execFileSync('npm', ['view', name, 'version'], {
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
104
|
+
}).trim()
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Numeric dotted comparison, so `1.1.10` sorts above `1.1.9`. A prerelease
|
|
112
|
+
* suffix is dropped from both sides first: `1.1.3-beta.1` is not newer than
|
|
113
|
+
* `1.1.3`, and a prerelease that happens to be tagged `latest` must not raise
|
|
114
|
+
* the alarm by itself.
|
|
115
|
+
*/
|
|
116
|
+
const isNewer = (candidate, baseline) => {
|
|
117
|
+
const a = candidate.split('-')[0].split('.')
|
|
118
|
+
const b = baseline.split('-')[0].split('.')
|
|
119
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
120
|
+
const left = a[i] ?? '0'
|
|
121
|
+
const right = b[i] ?? '0'
|
|
122
|
+
if (left === right) continue
|
|
123
|
+
const leftNumber = Number(left)
|
|
124
|
+
const rightNumber = Number(right)
|
|
125
|
+
if (Number.isNaN(leftNumber) || Number.isNaN(rightNumber)) {
|
|
126
|
+
return left > right
|
|
127
|
+
}
|
|
128
|
+
return leftNumber > rightNumber
|
|
129
|
+
}
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Write the report to the step summary and to the file the workflow reads.
|
|
135
|
+
* Wrapped so a failure to report cannot mask the failure being reported - and
|
|
136
|
+
* so the workflow's `gh issue create --body-file` always has a file, including
|
|
137
|
+
* on the path below where the watch itself broke.
|
|
138
|
+
*/
|
|
139
|
+
const writeReport = (summary) => {
|
|
140
|
+
try {
|
|
141
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
142
|
+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
|
|
143
|
+
}
|
|
144
|
+
if (process.env.AUDIT_WATCH_REPORT) {
|
|
145
|
+
writeFileSync(process.env.AUDIT_WATCH_REPORT, summary)
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`could not write the report: ${error.message}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const main = () => {
|
|
153
|
+
const report = runAudit()
|
|
154
|
+
const advisories = collectAdvisories(report)
|
|
155
|
+
const problems = []
|
|
156
|
+
|
|
157
|
+
for (const [id, advisory] of advisories) {
|
|
158
|
+
if (EXEMPTIONS.includes(id)) continue
|
|
159
|
+
problems.push(
|
|
160
|
+
`new dev-tree advisory ${id} (${advisory.severity}): ${advisory.title} - ` +
|
|
161
|
+
`packages: ${[...new Set(advisory.packages)].sort().join(', ')}. ` +
|
|
162
|
+
'Fix it, or add it to EXEMPTIONS and name it in the build.yml comment.'
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const id of EXEMPTIONS) {
|
|
167
|
+
if (advisories.has(id)) continue
|
|
168
|
+
problems.push(
|
|
169
|
+
`exempted advisory ${id} no longer appears in the tree - the exemption is ` +
|
|
170
|
+
'dead weight. Remove it from EXEMPTIONS and from the build.yml comment, ' +
|
|
171
|
+
'and restore the full `npm audit` gate.'
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const { name, newestKnown } of WATCHED_FOR_RELEASE) {
|
|
176
|
+
const latest = latestPublished(name)
|
|
177
|
+
if (latest === null) {
|
|
178
|
+
console.log(`skipped the registry check for ${name} - npm view failed`)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (isNewer(latest, newestKnown)) {
|
|
182
|
+
problems.push(
|
|
183
|
+
`${name} has published ${latest}, newer than the ${newestKnown} recorded ` +
|
|
184
|
+
'with the exemption - a fix may exist. Re-evaluate the exemption, bump ' +
|
|
185
|
+
'the chain if it clears, and drop the exemption when it does.'
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const buildWorkflow = readFileSync(path.join(ROOT, BUILD_WORKFLOW), 'utf8')
|
|
191
|
+
for (const id of EXEMPTIONS) {
|
|
192
|
+
if (buildWorkflow.includes(id)) continue
|
|
193
|
+
problems.push(
|
|
194
|
+
`exemption ${id} is not named in ${BUILD_WORKFLOW} - the gate and this ` +
|
|
195
|
+
'script disagree about what is exempt.'
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { moderate = 0, high = 0, critical = 0, total = 0 } =
|
|
200
|
+
report?.metadata?.vulnerabilities ?? {}
|
|
201
|
+
|
|
202
|
+
const summary = [
|
|
203
|
+
'## Dev-tree audit watch',
|
|
204
|
+
'',
|
|
205
|
+
`Full audit: ${total} advisories (${critical} critical, ${high} high, ${moderate} moderate).`,
|
|
206
|
+
'',
|
|
207
|
+
'| advisory | severity | packages | exempt |',
|
|
208
|
+
'| --- | --- | --- | --- |',
|
|
209
|
+
...[...advisories].map(
|
|
210
|
+
([id, advisory]) =>
|
|
211
|
+
`| ${id} | ${advisory.severity} | ${[...new Set(advisory.packages)].sort().join(', ')} | ` +
|
|
212
|
+
`${EXEMPTIONS.includes(id) ? 'yes' : '**no**'} |`
|
|
213
|
+
),
|
|
214
|
+
'',
|
|
215
|
+
problems.length
|
|
216
|
+
? ['### The exemption needs attention', '', ...problems.map((p) => `- ${p}`), ''].join('\n')
|
|
217
|
+
: 'The exemption still matches the tree - nothing to do.',
|
|
218
|
+
''
|
|
219
|
+
].join('\n')
|
|
220
|
+
|
|
221
|
+
console.log(summary)
|
|
222
|
+
writeReport(summary)
|
|
223
|
+
|
|
224
|
+
process.exitCode = problems.length ? 1 : 0
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
main()
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// The watch itself broke, so the exemption went unchecked. Say so in the
|
|
231
|
+
// report rather than leaving the workflow to fail on a missing file.
|
|
232
|
+
const summary = [
|
|
233
|
+
'## Dev-tree audit watch',
|
|
234
|
+
'',
|
|
235
|
+
'The watch did not complete, so the dev-tree audit exemption was **not**',
|
|
236
|
+
'checked against the tree. Nothing is known about the advisories right now.',
|
|
237
|
+
'',
|
|
238
|
+
'```',
|
|
239
|
+
error.message,
|
|
240
|
+
'```',
|
|
241
|
+
'',
|
|
242
|
+
'Investigate the failing step, then re-run the workflow.',
|
|
243
|
+
''
|
|
244
|
+
].join('\n')
|
|
245
|
+
|
|
246
|
+
console.error(`audit watch failed: ${error.message}`)
|
|
247
|
+
writeReport(summary)
|
|
248
|
+
process.exit(1)
|
|
249
|
+
}
|
package/utils/isIgnored.js
CHANGED
|
@@ -13,7 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.isIgnored = void 0;
|
|
16
|
-
const
|
|
16
|
+
const file_1 = require("@sasjs/utils/file");
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const ignore_1 = __importDefault(require("ignore"));
|
|
19
19
|
const _1 = require(".");
|
|
@@ -32,8 +32,8 @@ const isIgnored = (fPath, configuration) => __awaiter(void 0, void 0, void 0, fu
|
|
|
32
32
|
if (fPath === projectRoot)
|
|
33
33
|
return false;
|
|
34
34
|
let gitIgnoreFileContent = '';
|
|
35
|
-
if (yield (0,
|
|
36
|
-
gitIgnoreFileContent = yield (0,
|
|
35
|
+
if (yield (0, file_1.fileExists)(gitIgnoreFilePath))
|
|
36
|
+
gitIgnoreFileContent = yield (0, file_1.readFile)(gitIgnoreFilePath);
|
|
37
37
|
return (0, ignore_1.default)()
|
|
38
38
|
.add(gitIgnoreFileContent)
|
|
39
39
|
.add(config.ignoreList)
|
package/utils/isIgnored.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"isIgnored.js","sourceRoot":"","sources":["../../src/utils/isIgnored.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,
|
|
1
|
+
{"version":3,"file":"isIgnored.js","sourceRoot":"","sources":["../../src/utils/isIgnored.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,4CAAwD;AACxD,gDAAuB;AACvB,oDAA2B;AAC3B,wBAAiD;AAGjD;;;;;GAKG;AACI,MAAM,SAAS,GAAG,CACvB,KAAa,EACb,aAA0B,EACR,EAAE;IACpB,MAAM,MAAM,GAAG,aAAa,IAAI,CAAC,MAAM,IAAA,gBAAa,GAAE,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,MAAM,IAAA,iBAAc,GAAE,CAAA;IAC1C,MAAM,iBAAiB,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC9D,MAAM,QAAQ,GAAG,WAAW,GAAG,cAAI,CAAC,GAAG,CAAA;IACvC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAEhD,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,KAAK,CAAA;IAEvC,IAAI,oBAAoB,GAAG,EAAE,CAAA;IAE7B,IAAI,MAAM,IAAA,iBAAU,EAAC,iBAAiB,CAAC;QACrC,oBAAoB,GAAG,MAAM,IAAA,eAAQ,EAAC,iBAAiB,CAAC,CAAA;IAE1D,OAAO,IAAA,gBAAM,GAAE;SACZ,GAAG,CAAC,oBAAoB,CAAC;SACzB,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC;SACtB,OAAO,CAAC,YAAY,CAAC,CAAA;AAC1B,CAAC,CAAA,CAAA;AArBY,QAAA,SAAS,aAqBrB"}
|