v11y-check 0.0.0 → 0.7.0
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.
- package/LICENSE +7 -0
- package/README.md +148 -1
- package/bin/v11y.js +11 -0
- package/dist/esm/.cache +1 -0
- package/dist/esm/bin/route.d.ts +1 -0
- package/dist/esm/bin/route.js +170 -0
- package/dist/esm/bin/v11y.d.ts +1 -0
- package/dist/esm/bin/v11y.js +12 -0
- package/dist/esm/cli.d.ts +5 -0
- package/dist/esm/cli.js +231 -0
- package/dist/esm/config.d.ts +10 -0
- package/dist/esm/config.js +41 -0
- package/dist/esm/format-actions.d.ts +3 -0
- package/dist/esm/format-actions.js +34 -0
- package/dist/esm/format-check.d.ts +38 -0
- package/dist/esm/format-check.js +134 -0
- package/dist/esm/format-summary.d.ts +8 -0
- package/dist/esm/format-summary.js +12 -0
- package/dist/esm/format-time.d.ts +1 -0
- package/dist/esm/format-time.js +36 -0
- package/dist/esm/format-verbose.d.ts +3 -0
- package/dist/esm/format-verbose.js +144 -0
- package/dist/esm/generate.d.ts +9 -0
- package/dist/esm/generate.js +28 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +20 -0
- package/dist/esm/init/initCommand.d.ts +6 -0
- package/dist/esm/init/initCommand.js +34 -0
- package/dist/esm/init/scaffold.d.ts +13 -0
- package/dist/esm/init/scaffold.js +45 -0
- package/dist/esm/init/templates.d.ts +1 -0
- package/dist/esm/init/templates.js +23 -0
- package/dist/esm/run-audit.d.ts +34 -0
- package/dist/esm/run-audit.js +166 -0
- package/dist/esm/sync.d.ts +20 -0
- package/dist/esm/sync.js +95 -0
- package/dist/esm/tmp.d.ts +1 -0
- package/dist/esm/tmp.js +14 -0
- package/dist/esm/types.d.ts +98 -0
- package/dist/esm/types.js +40 -0
- package/package.json +52 -2
- package/templates/audit.yaml.template +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Thorsen
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
The software is provided "as is" and the author disclaims all warranties with regard to this software including all implied warranties of merchantability and fitness. In no event shall the author be liable for any special, direct, indirect, or consequential damages or any damages whatsoever resulting from loss of use, data or profits, whether in an action of contract, negligence or other tortious action, arising out of or in connection with the use or performance of this software.
|
package/README.md
CHANGED
|
@@ -1 +1,148 @@
|
|
|
1
|
-
|
|
1
|
+
# v11y-check
|
|
2
|
+
|
|
3
|
+
Wraps [audit-ci](https://github.com/IBM/audit-ci) with a richer config model, typed JSON source of truth, and a sync workflow that automates allowlist management.
|
|
4
|
+
|
|
5
|
+
<!-- section:release-notes -->
|
|
6
|
+
## Release notes — v0.7.0 (2026-05-10)
|
|
7
|
+
|
|
8
|
+
### 🎉 Features
|
|
9
|
+
|
|
10
|
+
- Rename audit-deps to v11y-check (#383)
|
|
11
|
+
|
|
12
|
+
The package previously published as `@williamthorsen/audit-deps` has been renamed to `v11y-check`. The CLI command is now `v11y-check`, and the default config file is `.config/v11y-check.config.json`. Existing users should install `v11y-check` in place of `@williamthorsen/audit-deps`, rename their config file, and update any scripts that invoke `audit-deps`. Behavior is unchanged.
|
|
13
|
+
<!-- /section:release-notes -->
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
pnpm add -D v11y-check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The package is `v11y-check`; the bin is `v11y`. The package also ships `v11y-check` as a bin alias, so `pnpm exec v11y-check` and `npx v11y-check` continue to work.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
# Run a vulnerability check with sensible defaults (no config needed)
|
|
27
|
+
npx v11y-check check
|
|
28
|
+
|
|
29
|
+
# Scaffold a starter config and GitHub Actions workflow
|
|
30
|
+
npx v11y-check init
|
|
31
|
+
|
|
32
|
+
# Sync allowlists with current findings
|
|
33
|
+
npx v11y-check sync
|
|
34
|
+
|
|
35
|
+
# Raw audit-ci passthrough
|
|
36
|
+
npx v11y-check --raw
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No config file is required. When `.config/v11y-check.config.json` is absent, the tool uses built-in defaults: `severityThreshold: 'moderate'` for dev, `severityThreshold: 'low'` for prod, and empty allowlists.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
The source-of-truth config lives at `.config/v11y-check.config.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/v11y-check-v<version>/packages/v11y-check/schemas/config.json",
|
|
48
|
+
"dev": {
|
|
49
|
+
"severityThreshold": "moderate",
|
|
50
|
+
"allowlist": [
|
|
51
|
+
{
|
|
52
|
+
"addedAt": "2026-04-15T09:30:00.000Z",
|
|
53
|
+
"id": "GHSA-1234-5678-abcd",
|
|
54
|
+
"path": "lodash",
|
|
55
|
+
"reason": "Accepted risk: no user input reaches this path",
|
|
56
|
+
"url": "https://github.com/advisories/GHSA-1234-5678-abcd"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"prod": {
|
|
61
|
+
"severityThreshold": "low",
|
|
62
|
+
"allowlist": []
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Fields
|
|
68
|
+
|
|
69
|
+
- **`$schema`** (optional) — JSON Schema URL for editor autocomplete and validation. Automatically included by `init` and `sync`.
|
|
70
|
+
- **`dev`** / **`prod`** — Scope-specific settings:
|
|
71
|
+
- **`severityThreshold`** (optional) — Fail on advisories at or above this severity. Valid values: `'low'`, `'moderate'`, `'high'`, `'critical'`. When omitted, audit-ci uses its own defaults.
|
|
72
|
+
- **`allowlist`** — Typed advisory entries with `id`, `path`, `url`, and optional `reason` and `addedAt`. `addedAt` is an ISO 8601 UTC datetime (e.g., `2026-04-15T09:30:00.000Z`) populated automatically by `v11y sync` on new entries; existing entries retain whatever value they had. Older `YYYY-MM-DD` values are still accepted.
|
|
73
|
+
|
|
74
|
+
## CLI reference
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Usage: v11y [options]
|
|
78
|
+
v11y <command> [options]
|
|
79
|
+
|
|
80
|
+
Commands:
|
|
81
|
+
check (default) Grouped vulnerability check with severity indicators
|
|
82
|
+
sync Synchronize allowlists with current audit findings
|
|
83
|
+
init Scaffold a starter config file and GitHub Actions workflow
|
|
84
|
+
|
|
85
|
+
Scope options:
|
|
86
|
+
--dev Target dev dependencies only
|
|
87
|
+
--prod Target production dependencies only
|
|
88
|
+
|
|
89
|
+
Other options:
|
|
90
|
+
--config <path> Path to config file (default: .config/v11y-check.config.json)
|
|
91
|
+
--json Output results as JSON
|
|
92
|
+
--raw Run raw audit-ci passthrough
|
|
93
|
+
--verbose, -v Show detailed per-vulnerability output
|
|
94
|
+
--help, -h Show this help message
|
|
95
|
+
--version, -V Show version number
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Init options
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
--dry-run, -n Preview changes without writing files
|
|
102
|
+
--force, -f Overwrite existing files
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### JSON output shape
|
|
106
|
+
|
|
107
|
+
`--json` emits one object per requested scope plus a top-level `summary` block:
|
|
108
|
+
|
|
109
|
+
```jsonc
|
|
110
|
+
{
|
|
111
|
+
"prod": { "allowed": [...], "belowThreshold": [...], "stale": [...], "unallowed": [...] },
|
|
112
|
+
"dev": { "allowed": [...], "belowThreshold": [...], "stale": [...], "unallowed": [...] },
|
|
113
|
+
"summary": { "status": "vulnerabilities-found", "count": 3 }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`summary.status` is the highest-severity finding category present across the requested scopes. Below-threshold findings never affect it.
|
|
118
|
+
|
|
119
|
+
| `status` | When |
|
|
120
|
+
| ---------------------------- | ------------------------------------------------- |
|
|
121
|
+
| `vulnerabilities-found` | At least one unallowed advisory |
|
|
122
|
+
| `suppressed-vulnerabilities` | No unallowed; at least one allowlisted advisory |
|
|
123
|
+
| `stale-overrides` | No advisories; at least one stale allowlist entry |
|
|
124
|
+
| `none` | No findings |
|
|
125
|
+
|
|
126
|
+
`summary.count` is the total across the requested scopes for the active category. It is `0` when `status` is `none`.
|
|
127
|
+
|
|
128
|
+
## Scaffolded GitHub Actions workflow
|
|
129
|
+
|
|
130
|
+
`v11y init` scaffolds `.github/workflows/audit.yaml` alongside the config file. The workflow is a thin caller that delegates to the versioned reusable workflow published at `williamthorsen/node-monorepo-tools/.github/workflows/audit.reusable.yaml@workflow/audit-v1`, so your repository tracks the reusable workflow at a stable version tag.
|
|
131
|
+
|
|
132
|
+
The scaffolded workflow triggers on pull requests to `main`/`next`, on a daily schedule, and on manual `workflow_dispatch`. Commit the file into your repository so the caller runs in CI. If the reusable workflow's caller-side requirements change (for example, the tag bumps), re-run `v11y init --force` to refresh the file.
|
|
133
|
+
|
|
134
|
+
## Migration from v0.3
|
|
135
|
+
|
|
136
|
+
v0.4 introduces breaking changes to the config schema:
|
|
137
|
+
|
|
138
|
+
- **`outDir` removed.** Intermediate audit-ci files are now written to a temp directory and cleaned up automatically. Remove `outDir` from your config.
|
|
139
|
+
- **Severity booleans replaced by `severityThreshold`.** Replace `"moderate": true` with `"severityThreshold": "moderate"`, `"high": true` with `"severityThreshold": "high"`, etc. Only one threshold per scope is supported.
|
|
140
|
+
- **`generate` subcommand removed.** The `v11y generate` command no longer exists. Flat audit-ci configs are now managed internally.
|
|
141
|
+
- **Config is optional.** All commands now work without a config file, using built-in defaults.
|
|
142
|
+
|
|
143
|
+
To migrate an existing config:
|
|
144
|
+
|
|
145
|
+
1. Remove the `outDir` field.
|
|
146
|
+
2. Replace severity booleans with `severityThreshold` in each scope.
|
|
147
|
+
3. Optionally add a `$schema` field (run `v11y init --force` to regenerate, or add it manually).
|
|
148
|
+
4. Delete any generated `audit-ci.*.json` files that were in your config directory.
|
package/bin/v11y.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
try {
|
|
3
|
+
await import('../dist/esm/bin/v11y.js');
|
|
4
|
+
} catch (error) {
|
|
5
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
6
|
+
process.stderr.write('v11y: build output not found — run `pnpm run build` first\n');
|
|
7
|
+
} else {
|
|
8
|
+
process.stderr.write(`v11y: failed to load: ${error.message}\n`);
|
|
9
|
+
}
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
package/dist/esm/.cache
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cc7eb157b4617c1c38c8507dc4279631b07d91e6b647199879b5724bcaa312d8
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function routeCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { parseArgs, readPackageVersion, translateParseError } from "@williamthorsen/nmr-core";
|
|
3
|
+
import { auditCommand, checkCommand, extractMessage, syncCommand } from "../cli.js";
|
|
4
|
+
import { initCommand } from "../init/initCommand.js";
|
|
5
|
+
const VERSION = readPackageVersion(import.meta.url);
|
|
6
|
+
const SUBCOMMANDS = ["check", "init", "sync"];
|
|
7
|
+
const MIN_PREFIX_LENGTH = 3;
|
|
8
|
+
function showHelp() {
|
|
9
|
+
console.info(`
|
|
10
|
+
Usage: v11y [options]
|
|
11
|
+
v11y <command> [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
check (default) Grouped vulnerability check with severity indicators
|
|
15
|
+
sync Synchronize allowlists with current audit findings
|
|
16
|
+
init Scaffold a starter config file and GitHub Actions workflow
|
|
17
|
+
|
|
18
|
+
Scope options:
|
|
19
|
+
--dev Target dev dependencies only
|
|
20
|
+
--prod Target production dependencies only
|
|
21
|
+
|
|
22
|
+
Other options:
|
|
23
|
+
--config <path> Path to config file (default: .config/v11y-check.config.json)
|
|
24
|
+
--json Output results as JSON
|
|
25
|
+
--raw Run raw audit-ci passthrough
|
|
26
|
+
--verbose, -v Show detailed per-vulnerability output
|
|
27
|
+
--help, -h Show this help message
|
|
28
|
+
--version, -V Show version number
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
function showInitHelp() {
|
|
32
|
+
console.info(`
|
|
33
|
+
Usage: v11y init [options]
|
|
34
|
+
|
|
35
|
+
Scaffold a starter config file and GitHub Actions workflow.
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--dry-run, -n Preview changes without writing files
|
|
39
|
+
--force, -f Overwrite existing files
|
|
40
|
+
--help, -h Show this help message
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
function showSyncHelp() {
|
|
44
|
+
console.info(`
|
|
45
|
+
Usage: v11y sync [options]
|
|
46
|
+
|
|
47
|
+
Synchronize allowlists with current audit findings.
|
|
48
|
+
|
|
49
|
+
Scope options:
|
|
50
|
+
--dev Target dev dependencies only
|
|
51
|
+
--prod Target production dependencies only
|
|
52
|
+
|
|
53
|
+
Other options:
|
|
54
|
+
--config <path> Path to config file (default: .config/v11y-check.config.json)
|
|
55
|
+
--json Output results as JSON
|
|
56
|
+
--help, -h Show this help message
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
function parseSharedFlags(flags) {
|
|
60
|
+
const flagSchema = {
|
|
61
|
+
config: { long: "--config", type: "string" },
|
|
62
|
+
dev: { long: "--dev", type: "boolean" },
|
|
63
|
+
json: { long: "--json", type: "boolean", short: "-j" },
|
|
64
|
+
prod: { long: "--prod", type: "boolean" },
|
|
65
|
+
verbose: { long: "--verbose", type: "boolean", short: "-v" }
|
|
66
|
+
};
|
|
67
|
+
const { flags: parsed } = parseArgs(flags, flagSchema);
|
|
68
|
+
if (parsed.dev && parsed.prod) {
|
|
69
|
+
throw new Error("Cannot specify both --dev and --prod");
|
|
70
|
+
}
|
|
71
|
+
const scopes = [];
|
|
72
|
+
if (parsed.dev) scopes.push("dev");
|
|
73
|
+
if (parsed.prod) scopes.push("prod");
|
|
74
|
+
return {
|
|
75
|
+
configPath: parsed.config,
|
|
76
|
+
json: parsed.json,
|
|
77
|
+
scopes,
|
|
78
|
+
verbose: parsed.verbose
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function findTypoMatch(input) {
|
|
82
|
+
if (input.length < MIN_PREFIX_LENGTH || input.startsWith("-")) {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
for (const cmd of SUBCOMMANDS) {
|
|
86
|
+
if (cmd !== input && cmd.startsWith(input)) {
|
|
87
|
+
return cmd;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
async function routeCommand(args) {
|
|
93
|
+
const command = args[0];
|
|
94
|
+
if (command === "--help" || command === "-h") {
|
|
95
|
+
showHelp();
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
if (command === "--version" || command === "-V") {
|
|
99
|
+
console.info(VERSION);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
if (command === "check") {
|
|
103
|
+
return handleSubcommand(args.slice(1), checkCommand);
|
|
104
|
+
}
|
|
105
|
+
if (command === "init") {
|
|
106
|
+
return handleInit(args.slice(1));
|
|
107
|
+
}
|
|
108
|
+
if (command === "sync") {
|
|
109
|
+
return handleSubcommand(args.slice(1), syncCommand, showSyncHelp);
|
|
110
|
+
}
|
|
111
|
+
if (command !== void 0 && !command.startsWith("-")) {
|
|
112
|
+
const typoMatch = findTypoMatch(command);
|
|
113
|
+
if (typoMatch !== void 0) {
|
|
114
|
+
process.stderr.write(`Error: Unknown command '${command}'. Did you mean 'v11y ${typoMatch}'?
|
|
115
|
+
`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
process.stderr.write(`Error: Unknown command '${command}'.
|
|
119
|
+
`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
if (args.includes("--raw")) {
|
|
123
|
+
const filteredArgs = args.filter((a) => a !== "--raw");
|
|
124
|
+
return handleSubcommand(filteredArgs, auditCommand);
|
|
125
|
+
}
|
|
126
|
+
return handleSubcommand(args, checkCommand);
|
|
127
|
+
}
|
|
128
|
+
async function handleSubcommand(flags, handler, helpFn = showHelp) {
|
|
129
|
+
if (flags.some((f) => f === "--help" || f === "-h")) {
|
|
130
|
+
helpFn();
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
let options;
|
|
134
|
+
try {
|
|
135
|
+
options = parseSharedFlags(flags);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
process.stderr.write(`Error: ${translateParseError(error)}
|
|
138
|
+
`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return await handler(options);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
145
|
+
`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function handleInit(flags) {
|
|
150
|
+
if (flags.some((f) => f === "--help" || f === "-h")) {
|
|
151
|
+
showInitHelp();
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
const initFlagSchema = {
|
|
155
|
+
dryRun: { long: "--dry-run", type: "boolean", short: "-n" },
|
|
156
|
+
force: { long: "--force", type: "boolean", short: "-f" }
|
|
157
|
+
};
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = parseArgs(flags, initFlagSchema);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
process.stderr.write(`Error: ${translateParseError(error)}
|
|
163
|
+
`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
return initCommand({ dryRun: parsed.flags.dryRun, force: parsed.flags.force });
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
routeCommand
|
|
170
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { routeCommand } from "./route.js";
|
|
3
|
+
let exitCode;
|
|
4
|
+
try {
|
|
5
|
+
exitCode = await routeCommand(process.argv.slice(2));
|
|
6
|
+
} catch (error) {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
process.stderr.write(`v11y: unexpected error: ${message}
|
|
9
|
+
`);
|
|
10
|
+
exitCode = 1;
|
|
11
|
+
}
|
|
12
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CommandOptions } from './types.ts';
|
|
2
|
+
export declare function extractMessage(error: unknown): string;
|
|
3
|
+
export declare function auditCommand(options: CommandOptions): Promise<number>;
|
|
4
|
+
export declare function checkCommand(options: CommandOptions): Promise<number>;
|
|
5
|
+
export declare function syncCommand(options: CommandOptions): Promise<number>;
|
package/dist/esm/cli.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { formatCheckJson, formatCheckText } from "./format-check.js";
|
|
4
|
+
import { formatCheckVerboseText } from "./format-verbose.js";
|
|
5
|
+
import { generateAuditCiConfig } from "./generate.js";
|
|
6
|
+
import { scaffoldConfig } from "./init/scaffold.js";
|
|
7
|
+
import { parseAuditCiOutput, runAudit, runReport } from "./run-audit.js";
|
|
8
|
+
import { syncAllowlist } from "./sync.js";
|
|
9
|
+
import { withTempDir } from "./tmp.js";
|
|
10
|
+
import { AUDIT_SCOPES, isSeverityAtOrAbove } from "./types.js";
|
|
11
|
+
function extractMessage(error) {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
async function auditCommand(options) {
|
|
15
|
+
let loaded;
|
|
16
|
+
try {
|
|
17
|
+
loaded = await loadConfig(options.configPath);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
20
|
+
`);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const { config } = loaded;
|
|
24
|
+
const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
|
|
25
|
+
try {
|
|
26
|
+
return await withTempDir(async (tempDir) => {
|
|
27
|
+
let exitCode = 0;
|
|
28
|
+
const allResults = [];
|
|
29
|
+
for (const scope of scopes) {
|
|
30
|
+
const configPath = await generateAuditCiConfig(config[scope], scope, tempDir);
|
|
31
|
+
const result = runAudit({ configPath, json: options.json });
|
|
32
|
+
for (const warning of result.warnings) {
|
|
33
|
+
process.stderr.write(`warning: ${warning}
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
if (result.staleEntries.length > 0) {
|
|
37
|
+
process.stderr.write(`warning: stale allowlist entries in ${scope}: ${result.staleEntries.join(", ")}
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
if (options.json) {
|
|
41
|
+
const parsed = parseAuditCiOutput(result.stdout);
|
|
42
|
+
for (const warning of parsed.warnings) {
|
|
43
|
+
process.stderr.write(`warning: ${warning}
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
for (const r of parsed.results) {
|
|
47
|
+
if (!allResults.some((existing) => existing.id === r.id)) {
|
|
48
|
+
allResults.push(r);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if (result.stdout.length > 0) {
|
|
53
|
+
process.stdout.write(result.stdout);
|
|
54
|
+
}
|
|
55
|
+
if (result.stderr.length > 0) {
|
|
56
|
+
process.stderr.write(result.stderr);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (result.exitCode !== 0) {
|
|
60
|
+
exitCode = result.exitCode;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (options.json) {
|
|
64
|
+
process.stdout.write(JSON.stringify(allResults, null, 2) + "\n");
|
|
65
|
+
}
|
|
66
|
+
return exitCode;
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
70
|
+
`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const CHECK_SCOPE_ORDER = ["prod", "dev"];
|
|
75
|
+
async function checkCommand(options) {
|
|
76
|
+
let loaded;
|
|
77
|
+
try {
|
|
78
|
+
loaded = await loadConfig(options.configPath);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
81
|
+
`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
const { config } = loaded;
|
|
85
|
+
const requestedScopes = options.scopes.length > 0 ? options.scopes : [...CHECK_SCOPE_ORDER];
|
|
86
|
+
const scopes = CHECK_SCOPE_ORDER.filter((s) => requestedScopes.includes(s));
|
|
87
|
+
try {
|
|
88
|
+
return await withTempDir(async (tempDir) => {
|
|
89
|
+
const checkResult = {
|
|
90
|
+
dev: { allowed: [], belowThreshold: [], stale: [], unallowed: [] },
|
|
91
|
+
prod: { allowed: [], belowThreshold: [], stale: [], unallowed: [] }
|
|
92
|
+
};
|
|
93
|
+
const thresholds = {};
|
|
94
|
+
for (const scope of scopes) {
|
|
95
|
+
const scopeConfig = config[scope];
|
|
96
|
+
const effectiveThreshold = scopeConfig.severityThreshold ?? "low";
|
|
97
|
+
thresholds[scope] = effectiveThreshold;
|
|
98
|
+
const strippedScope = { ...scopeConfig, allowlist: [], severityThreshold: "low" };
|
|
99
|
+
const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
|
|
100
|
+
const reportOptions = { configPath: strippedConfigPath };
|
|
101
|
+
if (options.verbose) {
|
|
102
|
+
reportOptions.reportType = "full";
|
|
103
|
+
}
|
|
104
|
+
const report = runReport(reportOptions);
|
|
105
|
+
for (const warning of report.warnings) {
|
|
106
|
+
process.stderr.write(`warning: ${warning}
|
|
107
|
+
`);
|
|
108
|
+
}
|
|
109
|
+
if (report.stderr.length > 0) {
|
|
110
|
+
process.stderr.write(report.stderr);
|
|
111
|
+
}
|
|
112
|
+
const allowedIds = new Set(scopeConfig.allowlist.map((entry) => entry.id));
|
|
113
|
+
const foundIds = new Set(report.results.map((r) => r.id));
|
|
114
|
+
const allowlistById = new Map(scopeConfig.allowlist.map((entry) => [entry.id, entry]));
|
|
115
|
+
const scopeResult = { allowed: [], belowThreshold: [], stale: [], unallowed: [] };
|
|
116
|
+
for (const result of report.results) {
|
|
117
|
+
if (!isSeverityAtOrAbove(result.severity, effectiveThreshold)) {
|
|
118
|
+
scopeResult.belowThreshold.push(result);
|
|
119
|
+
} else if (allowedIds.has(result.id)) {
|
|
120
|
+
const entry = allowlistById.get(result.id);
|
|
121
|
+
scopeResult.allowed.push(buildAllowedVuln(result, entry));
|
|
122
|
+
} else {
|
|
123
|
+
scopeResult.unallowed.push(result);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const entry of scopeConfig.allowlist) {
|
|
127
|
+
if (!foundIds.has(entry.id)) {
|
|
128
|
+
scopeResult.stale.push({ id: entry.id });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
checkResult[scope] = scopeResult;
|
|
132
|
+
}
|
|
133
|
+
if (options.verbose && !options.json) {
|
|
134
|
+
process.stdout.write(formatCheckVerboseText(checkResult, scopes, void 0, thresholds));
|
|
135
|
+
} else if (options.json) {
|
|
136
|
+
process.stdout.write(formatCheckJson(checkResult, scopes));
|
|
137
|
+
} else {
|
|
138
|
+
process.stdout.write(formatCheckText(checkResult, scopes, void 0, thresholds));
|
|
139
|
+
}
|
|
140
|
+
const hasUnallowed = scopes.some((s) => checkResult[s].unallowed.length > 0);
|
|
141
|
+
return hasUnallowed ? 1 : 0;
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
145
|
+
`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function syncCommand(options) {
|
|
150
|
+
let loaded;
|
|
151
|
+
try {
|
|
152
|
+
loaded = await loadConfig(options.configPath);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
155
|
+
`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
const { configSource } = loaded;
|
|
159
|
+
if (configSource === "defaults") {
|
|
160
|
+
const { configResult } = scaffoldConfig({ dryRun: false, force: false });
|
|
161
|
+
if (configResult.outcome === "failed") {
|
|
162
|
+
process.stderr.write(`Error: Failed to create config file
|
|
163
|
+
`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
loaded = await loadConfig(options.configPath);
|
|
167
|
+
}
|
|
168
|
+
let { config } = loaded;
|
|
169
|
+
const { configFilePath } = loaded;
|
|
170
|
+
const scopes = options.scopes.length > 0 ? options.scopes : [...AUDIT_SCOPES];
|
|
171
|
+
try {
|
|
172
|
+
return await withTempDir(async (tempDir) => {
|
|
173
|
+
for (const scope of scopes) {
|
|
174
|
+
const strippedScope = { ...config[scope], allowlist: [] };
|
|
175
|
+
const strippedConfigPath = await generateAuditCiConfig(strippedScope, scope, tempDir);
|
|
176
|
+
const report = runReport({ configPath: strippedConfigPath });
|
|
177
|
+
for (const warning of report.warnings) {
|
|
178
|
+
process.stderr.write(`warning: ${warning}
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
const { syncResult, updatedConfig } = await syncAllowlist(config, scope, report.results, configFilePath);
|
|
182
|
+
config = updatedConfig;
|
|
183
|
+
if (options.json) {
|
|
184
|
+
process.stdout.write(JSON.stringify(syncResult, null, 2) + "\n");
|
|
185
|
+
} else {
|
|
186
|
+
process.stdout.write(`
|
|
187
|
+
--- ${scope} ---
|
|
188
|
+
`);
|
|
189
|
+
process.stdout.write(` added: ${syncResult.added.length}
|
|
190
|
+
`);
|
|
191
|
+
process.stdout.write(` kept: ${syncResult.kept.length}
|
|
192
|
+
`);
|
|
193
|
+
process.stdout.write(` removed: ${syncResult.removed.length}
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (configSource === "defaults") {
|
|
198
|
+
const totalEntries = config.dev.allowlist.length + config.prod.allowlist.length;
|
|
199
|
+
process.stdout.write(`Created config at ${configFilePath} with ${totalEntries} allowed entries.
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
return 0;
|
|
203
|
+
});
|
|
204
|
+
} catch (error) {
|
|
205
|
+
process.stderr.write(`Error: ${extractMessage(error)}
|
|
206
|
+
`);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function buildAllowedVuln(result, entry) {
|
|
211
|
+
const allowed = {
|
|
212
|
+
id: result.id,
|
|
213
|
+
path: result.path,
|
|
214
|
+
paths: result.paths,
|
|
215
|
+
url: result.url
|
|
216
|
+
};
|
|
217
|
+
if (result.ghsaId !== void 0) allowed.ghsaId = result.ghsaId;
|
|
218
|
+
if (result.severity !== void 0) allowed.severity = result.severity;
|
|
219
|
+
if (result.title !== void 0) allowed.title = result.title;
|
|
220
|
+
if (result.description !== void 0) allowed.description = result.description;
|
|
221
|
+
if (result.cvss !== void 0) allowed.cvss = result.cvss;
|
|
222
|
+
if (entry?.reason !== void 0) allowed.reason = entry.reason;
|
|
223
|
+
if (entry?.addedAt !== void 0) allowed.addedAt = entry.addedAt;
|
|
224
|
+
return allowed;
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
auditCommand,
|
|
228
|
+
checkCommand,
|
|
229
|
+
extractMessage,
|
|
230
|
+
syncCommand
|
|
231
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { V11yCheckConfig } from './types.ts';
|
|
2
|
+
export declare const DEFAULT_CONFIG_PATH = ".config/v11y-check.config.json";
|
|
3
|
+
export type ConfigSource = 'defaults' | 'file';
|
|
4
|
+
export interface LoadConfigResult {
|
|
5
|
+
config: V11yCheckConfig;
|
|
6
|
+
configDir: string;
|
|
7
|
+
configFilePath: string;
|
|
8
|
+
configSource: ConfigSource;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadConfig(configPath?: string, cwd?: string): Promise<LoadConfigResult>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_CONFIG, v11yCheckConfigSchema } from "./types.js";
|
|
5
|
+
const DEFAULT_CONFIG_PATH = ".config/v11y-check.config.json";
|
|
6
|
+
async function loadConfig(configPath, cwd) {
|
|
7
|
+
const resolvedCwd = cwd ?? process.cwd();
|
|
8
|
+
const filePath = path.resolve(resolvedCwd, configPath ?? DEFAULT_CONFIG_PATH);
|
|
9
|
+
const configDir = path.dirname(filePath);
|
|
10
|
+
if (configPath === void 0 && !existsSync(filePath)) {
|
|
11
|
+
return {
|
|
12
|
+
config: DEFAULT_CONFIG,
|
|
13
|
+
configDir,
|
|
14
|
+
configFilePath: filePath,
|
|
15
|
+
configSource: "defaults"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = await readFile(filePath, "utf8");
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(`Config file is not valid JSON: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
const result = v11yCheckConfigSchema.safeParse(parsed);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
33
|
+
throw new Error(`Invalid config in ${filePath}:
|
|
34
|
+
${issues}`);
|
|
35
|
+
}
|
|
36
|
+
return { config: result.data, configDir, configFilePath: filePath, configSource: "file" };
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
DEFAULT_CONFIG_PATH,
|
|
40
|
+
loadConfig
|
|
41
|
+
};
|