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.
Files changed (42) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -1
  3. package/bin/v11y.js +11 -0
  4. package/dist/esm/.cache +1 -0
  5. package/dist/esm/bin/route.d.ts +1 -0
  6. package/dist/esm/bin/route.js +170 -0
  7. package/dist/esm/bin/v11y.d.ts +1 -0
  8. package/dist/esm/bin/v11y.js +12 -0
  9. package/dist/esm/cli.d.ts +5 -0
  10. package/dist/esm/cli.js +231 -0
  11. package/dist/esm/config.d.ts +10 -0
  12. package/dist/esm/config.js +41 -0
  13. package/dist/esm/format-actions.d.ts +3 -0
  14. package/dist/esm/format-actions.js +34 -0
  15. package/dist/esm/format-check.d.ts +38 -0
  16. package/dist/esm/format-check.js +134 -0
  17. package/dist/esm/format-summary.d.ts +8 -0
  18. package/dist/esm/format-summary.js +12 -0
  19. package/dist/esm/format-time.d.ts +1 -0
  20. package/dist/esm/format-time.js +36 -0
  21. package/dist/esm/format-verbose.d.ts +3 -0
  22. package/dist/esm/format-verbose.js +144 -0
  23. package/dist/esm/generate.d.ts +9 -0
  24. package/dist/esm/generate.js +28 -0
  25. package/dist/esm/index.d.ts +5 -0
  26. package/dist/esm/index.js +20 -0
  27. package/dist/esm/init/initCommand.d.ts +6 -0
  28. package/dist/esm/init/initCommand.js +34 -0
  29. package/dist/esm/init/scaffold.d.ts +13 -0
  30. package/dist/esm/init/scaffold.js +45 -0
  31. package/dist/esm/init/templates.d.ts +1 -0
  32. package/dist/esm/init/templates.js +23 -0
  33. package/dist/esm/run-audit.d.ts +34 -0
  34. package/dist/esm/run-audit.js +166 -0
  35. package/dist/esm/sync.d.ts +20 -0
  36. package/dist/esm/sync.js +95 -0
  37. package/dist/esm/tmp.d.ts +1 -0
  38. package/dist/esm/tmp.js +14 -0
  39. package/dist/esm/types.d.ts +98 -0
  40. package/dist/esm/types.js +40 -0
  41. package/package.json +52 -2
  42. package/templates/audit.yaml.template +27 -0
@@ -0,0 +1,166 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import process from "node:process";
3
+ function resolveAuditCiBin() {
4
+ try {
5
+ const resolved = import.meta.resolve("audit-ci");
6
+ const modulePath = new URL(resolved).pathname;
7
+ const pkgDir = modulePath.replace(/\/dist\/.*$/, "");
8
+ return `${pkgDir}/dist/bin.js`;
9
+ } catch {
10
+ return "audit-ci";
11
+ }
12
+ }
13
+ function parseAuditCiOutput(jsonString) {
14
+ const warnings = [];
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(jsonString);
18
+ } catch {
19
+ if (jsonString.trim().length > 0) {
20
+ warnings.push(`Failed to parse audit-ci output as JSON (${jsonString.length} bytes)`);
21
+ }
22
+ return { results: [], warnings };
23
+ }
24
+ if (typeof parsed !== "object" || parsed === null) {
25
+ return { results: [], warnings };
26
+ }
27
+ const advisories = extractAdvisories(parsed);
28
+ const results = [];
29
+ for (const advisory of advisories) {
30
+ const paths = extractPaths(advisory);
31
+ const firstPath = paths[0] ?? advisory.module_name;
32
+ const result = {
33
+ id: String(advisory.id),
34
+ path: firstPath,
35
+ paths,
36
+ url: advisory.url
37
+ };
38
+ if (typeof advisory.github_advisory_id === "string") {
39
+ result.ghsaId = advisory.github_advisory_id;
40
+ }
41
+ if (typeof advisory.severity === "string") {
42
+ result.severity = advisory.severity;
43
+ }
44
+ if (typeof advisory.title === "string") {
45
+ result.title = advisory.title;
46
+ }
47
+ if (typeof advisory.overview === "string") {
48
+ result.description = advisory.overview;
49
+ }
50
+ if (advisory.cvss !== void 0) {
51
+ result.cvss = advisory.cvss;
52
+ }
53
+ results.push(result);
54
+ }
55
+ return { results, warnings };
56
+ }
57
+ function isObject(value) {
58
+ return typeof value === "object" && value !== null;
59
+ }
60
+ function isAuditCiAdvisory(value) {
61
+ if (!isObject(value)) return false;
62
+ return "id" in value && "url" in value && "findings" in value;
63
+ }
64
+ function collectAdvisories(obj) {
65
+ if (!("advisories" in obj) || typeof obj.advisories !== "object" || obj.advisories === null) {
66
+ return [];
67
+ }
68
+ return Object.values(obj.advisories).filter(isAuditCiAdvisory);
69
+ }
70
+ function extractAdvisories(parsed) {
71
+ if (typeof parsed !== "object" || parsed === null) return [];
72
+ if ("advisories" in parsed) {
73
+ return collectAdvisories(parsed);
74
+ }
75
+ if (Array.isArray(parsed)) {
76
+ const advisories = [];
77
+ for (const item of parsed) {
78
+ if (isObject(item)) {
79
+ advisories.push(...collectAdvisories(item));
80
+ }
81
+ }
82
+ return advisories;
83
+ }
84
+ return [];
85
+ }
86
+ function extractPaths(advisory) {
87
+ const seen = /* @__PURE__ */ new Set();
88
+ const ordered = [];
89
+ for (const finding of advisory.findings) {
90
+ for (const pathValue of finding.paths) {
91
+ if (!seen.has(pathValue)) {
92
+ seen.add(pathValue);
93
+ ordered.push(pathValue);
94
+ }
95
+ }
96
+ }
97
+ return ordered;
98
+ }
99
+ function extractStaleEntries(jsonString) {
100
+ const warnings = [];
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(jsonString);
104
+ } catch {
105
+ if (jsonString.trim().length > 0) {
106
+ warnings.push(`Failed to parse audit-ci output as JSON for stale entry detection (${jsonString.length} bytes)`);
107
+ }
108
+ return { entries: [], warnings };
109
+ }
110
+ if (typeof parsed !== "object" || parsed === null) return { entries: [], warnings };
111
+ if (!("allowlistedAdvisoriesNotFound" in parsed)) return { entries: [], warnings };
112
+ const notFound = parsed.allowlistedAdvisoriesNotFound;
113
+ if (Array.isArray(notFound)) {
114
+ return { entries: notFound.filter((item) => typeof item === "string"), warnings };
115
+ }
116
+ return { entries: [], warnings };
117
+ }
118
+ function runAudit({ configPath, cwd, json }) {
119
+ const bin = resolveAuditCiBin();
120
+ const outputFormat = json ? "json" : "text";
121
+ const args = ["--config", configPath, "--output-format", outputFormat];
122
+ const result = spawnSync(process.execPath, [bin, ...args], {
123
+ cwd: cwd ?? process.cwd(),
124
+ encoding: "utf8",
125
+ stdio: ["ignore", "pipe", "pipe"]
126
+ });
127
+ if (result.error) {
128
+ throw new Error(`Failed to launch audit-ci: ${result.error.message}`);
129
+ }
130
+ const stdout = result.stdout;
131
+ const stderr = result.stderr;
132
+ const staleResult = json ? extractStaleEntries(stdout) : { entries: [], warnings: [] };
133
+ return {
134
+ exitCode: result.status ?? 1,
135
+ staleEntries: staleResult.entries,
136
+ stdout,
137
+ stderr,
138
+ warnings: staleResult.warnings
139
+ };
140
+ }
141
+ function runReport({ configPath, cwd, reportType }) {
142
+ const bin = resolveAuditCiBin();
143
+ const args = ["--config", configPath, "--output-format", "json"];
144
+ if (reportType === "full") {
145
+ args.push("--report-type", "full");
146
+ }
147
+ const result = spawnSync(process.execPath, [bin, ...args], {
148
+ cwd: cwd ?? process.cwd(),
149
+ encoding: "utf8",
150
+ stdio: ["ignore", "pipe", "pipe"]
151
+ });
152
+ if (result.error) {
153
+ throw new Error(`Failed to launch audit-ci: ${result.error.message}`);
154
+ }
155
+ const stdout = result.stdout;
156
+ const stderr = result.stderr;
157
+ const parseResult = parseAuditCiOutput(stdout);
158
+ return { results: parseResult.results, stdout, stderr, warnings: parseResult.warnings };
159
+ }
160
+ export {
161
+ extractStaleEntries,
162
+ parseAuditCiOutput,
163
+ resolveAuditCiBin,
164
+ runAudit,
165
+ runReport
166
+ };
@@ -0,0 +1,20 @@
1
+ import type { AllowlistEntry, AuditResult, AuditScope, V11yCheckConfig } from './types.ts';
2
+ export declare function formatUtcDatetime(date: Date): string;
3
+ export declare function formatFriendlyUtc(date: Date): string;
4
+ export interface SyncResult {
5
+ added: AllowlistEntry[];
6
+ kept: AllowlistEntry[];
7
+ removed: AllowlistEntry[];
8
+ scope: AuditScope;
9
+ }
10
+ export declare function computeSyncDiff(currentAllowlist: AllowlistEntry[], auditResults: AuditResult[], date?: Date): {
11
+ added: AllowlistEntry[];
12
+ kept: AllowlistEntry[];
13
+ removed: AllowlistEntry[];
14
+ };
15
+ export declare function buildUpdatedConfig(config: V11yCheckConfig, scope: AuditScope, newAllowlist: AllowlistEntry[]): V11yCheckConfig;
16
+ export declare function serializeConfig(config: V11yCheckConfig): string;
17
+ export declare function syncAllowlist(config: V11yCheckConfig, scope: AuditScope, auditResults: AuditResult[], configFilePath: string, date?: Date): Promise<{
18
+ syncResult: SyncResult;
19
+ updatedConfig: V11yCheckConfig;
20
+ }>;
@@ -0,0 +1,95 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ function formatUtcDatetime(date) {
3
+ return date.toISOString();
4
+ }
5
+ function formatFriendlyUtc(date) {
6
+ const iso = date.toISOString();
7
+ return iso.slice(0, 10) + " " + iso.slice(11, 19) + " UTC";
8
+ }
9
+ function serializeEntry(entry) {
10
+ return {
11
+ ...entry.addedAt !== void 0 && { addedAt: entry.addedAt },
12
+ id: entry.id,
13
+ path: entry.path,
14
+ ...entry.reason !== void 0 && { reason: entry.reason },
15
+ url: entry.url
16
+ };
17
+ }
18
+ function serializeScopeConfig(scopeConfig) {
19
+ const result = {};
20
+ if (scopeConfig.severityThreshold !== void 0) {
21
+ result.severityThreshold = scopeConfig.severityThreshold;
22
+ }
23
+ result.allowlist = scopeConfig.allowlist.map(serializeEntry);
24
+ return result;
25
+ }
26
+ function computeSyncDiff(currentAllowlist, auditResults, date) {
27
+ const now = date ?? /* @__PURE__ */ new Date();
28
+ const currentById = new Map(currentAllowlist.map((entry) => [entry.id, entry]));
29
+ const auditById = new Map(auditResults.map((result) => [result.id, result]));
30
+ const added = [];
31
+ const kept = [];
32
+ const removed = [];
33
+ const nowIso = formatUtcDatetime(now);
34
+ for (const [id, result] of auditById) {
35
+ const existing = currentById.get(id);
36
+ if (existing !== void 0) {
37
+ kept.push(existing);
38
+ } else {
39
+ added.push({
40
+ addedAt: nowIso,
41
+ id: result.id,
42
+ path: result.path,
43
+ reason: `Added by v11y sync at ${formatFriendlyUtc(now)}`,
44
+ url: result.url
45
+ });
46
+ }
47
+ }
48
+ for (const [id, entry] of currentById) {
49
+ if (!auditById.has(id)) {
50
+ removed.push(entry);
51
+ }
52
+ }
53
+ return { added, kept, removed };
54
+ }
55
+ function buildUpdatedConfig(config, scope, newAllowlist) {
56
+ const sorted = [...newAllowlist].sort((a, b) => a.id.localeCompare(b.id));
57
+ return {
58
+ ...config,
59
+ [scope]: {
60
+ ...config[scope],
61
+ allowlist: sorted
62
+ }
63
+ };
64
+ }
65
+ function serializeConfig(config) {
66
+ const serializable = {};
67
+ if (config.$schema !== void 0) {
68
+ serializable.$schema = config.$schema;
69
+ }
70
+ serializable.dev = serializeScopeConfig(config.dev);
71
+ serializable.prod = serializeScopeConfig(config.prod);
72
+ return JSON.stringify(serializable, null, 2) + "\n";
73
+ }
74
+ async function syncAllowlist(config, scope, auditResults, configFilePath, date) {
75
+ const { added, kept, removed } = computeSyncDiff(config[scope].allowlist, auditResults, date);
76
+ const updatedConfig = buildUpdatedConfig(config, scope, [...kept, ...added]);
77
+ try {
78
+ await writeFile(configFilePath, serializeConfig(updatedConfig), "utf8");
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ throw new Error(`Failed to write config file '${configFilePath}': ${message}`);
82
+ }
83
+ return {
84
+ syncResult: { added, kept, removed, scope },
85
+ updatedConfig
86
+ };
87
+ }
88
+ export {
89
+ buildUpdatedConfig,
90
+ computeSyncDiff,
91
+ formatFriendlyUtc,
92
+ formatUtcDatetime,
93
+ serializeConfig,
94
+ syncAllowlist
95
+ };
@@ -0,0 +1 @@
1
+ export declare function withTempDir<T>(fn: (tempDir: string) => Promise<T>): Promise<T>;
@@ -0,0 +1,14 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ async function withTempDir(fn) {
5
+ const dir = await mkdtemp(path.join(tmpdir(), "v11y-check-"));
6
+ try {
7
+ return await fn(dir);
8
+ } finally {
9
+ await rm(dir, { recursive: true, force: true });
10
+ }
11
+ }
12
+ export {
13
+ withTempDir
14
+ };
@@ -0,0 +1,98 @@
1
+ import { z } from 'zod';
2
+ export type AuditScope = 'dev' | 'prod';
3
+ export declare const AUDIT_SCOPES: readonly AuditScope[];
4
+ export declare const SEVERITY_THRESHOLDS: readonly ["low", "moderate", "high", "critical"];
5
+ export type SeverityThreshold = (typeof SEVERITY_THRESHOLDS)[number];
6
+ export interface AllowlistEntry {
7
+ addedAt?: string | undefined;
8
+ id: string;
9
+ path: string;
10
+ reason?: string | undefined;
11
+ url: string;
12
+ }
13
+ export declare const allowlistEntrySchema: z.ZodObject<{
14
+ addedAt: z.ZodOptional<z.ZodString>;
15
+ id: z.ZodString;
16
+ path: z.ZodString;
17
+ reason: z.ZodOptional<z.ZodString>;
18
+ url: z.ZodString;
19
+ }, z.core.$strip>;
20
+ export interface ScopeConfig {
21
+ allowlist: AllowlistEntry[];
22
+ severityThreshold?: SeverityThreshold | undefined;
23
+ }
24
+ export declare const scopeConfigSchema: z.ZodObject<{
25
+ allowlist: z.ZodArray<z.ZodObject<{
26
+ addedAt: z.ZodOptional<z.ZodString>;
27
+ id: z.ZodString;
28
+ path: z.ZodString;
29
+ reason: z.ZodOptional<z.ZodString>;
30
+ url: z.ZodString;
31
+ }, z.core.$strip>>;
32
+ severityThreshold: z.ZodOptional<z.ZodEnum<{
33
+ low: "low";
34
+ moderate: "moderate";
35
+ high: "high";
36
+ critical: "critical";
37
+ }>>;
38
+ }, z.core.$strict>;
39
+ export interface V11yCheckConfig {
40
+ $schema?: string | undefined;
41
+ dev: ScopeConfig;
42
+ prod: ScopeConfig;
43
+ }
44
+ export declare const v11yCheckConfigSchema: z.ZodObject<{
45
+ $schema: z.ZodOptional<z.ZodString>;
46
+ dev: z.ZodObject<{
47
+ allowlist: z.ZodArray<z.ZodObject<{
48
+ addedAt: z.ZodOptional<z.ZodString>;
49
+ id: z.ZodString;
50
+ path: z.ZodString;
51
+ reason: z.ZodOptional<z.ZodString>;
52
+ url: z.ZodString;
53
+ }, z.core.$strip>>;
54
+ severityThreshold: z.ZodOptional<z.ZodEnum<{
55
+ low: "low";
56
+ moderate: "moderate";
57
+ high: "high";
58
+ critical: "critical";
59
+ }>>;
60
+ }, z.core.$strict>;
61
+ prod: z.ZodObject<{
62
+ allowlist: z.ZodArray<z.ZodObject<{
63
+ addedAt: z.ZodOptional<z.ZodString>;
64
+ id: z.ZodString;
65
+ path: z.ZodString;
66
+ reason: z.ZodOptional<z.ZodString>;
67
+ url: z.ZodString;
68
+ }, z.core.$strip>>;
69
+ severityThreshold: z.ZodOptional<z.ZodEnum<{
70
+ low: "low";
71
+ moderate: "moderate";
72
+ high: "high";
73
+ critical: "critical";
74
+ }>>;
75
+ }, z.core.$strict>;
76
+ }, z.core.$strict>;
77
+ export declare const DEFAULT_CONFIG: V11yCheckConfig;
78
+ export interface AuditResult {
79
+ cvss?: {
80
+ score?: number;
81
+ vectorString?: string;
82
+ } | undefined;
83
+ description?: string | undefined;
84
+ ghsaId?: string | undefined;
85
+ id: string;
86
+ path: string;
87
+ paths: string[];
88
+ severity?: string | undefined;
89
+ title?: string | undefined;
90
+ url: string;
91
+ }
92
+ export interface CommandOptions {
93
+ configPath?: string | undefined;
94
+ json: boolean;
95
+ scopes: AuditScope[];
96
+ verbose: boolean;
97
+ }
98
+ export declare function isSeverityAtOrAbove(severity: string | undefined, threshold: SeverityThreshold): boolean;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ const AUDIT_SCOPES = ["dev", "prod"];
3
+ const SEVERITY_THRESHOLDS = ["low", "moderate", "high", "critical"];
4
+ const allowlistEntrySchema = z.object({
5
+ addedAt: z.string().optional(),
6
+ id: z.string(),
7
+ path: z.string(),
8
+ reason: z.string().optional(),
9
+ url: z.string()
10
+ });
11
+ const scopeConfigSchema = z.object({
12
+ allowlist: z.array(allowlistEntrySchema),
13
+ severityThreshold: z.enum(SEVERITY_THRESHOLDS).optional()
14
+ }).strict();
15
+ const v11yCheckConfigSchema = z.object({
16
+ $schema: z.string().optional(),
17
+ dev: scopeConfigSchema,
18
+ prod: scopeConfigSchema
19
+ }).strict();
20
+ const DEFAULT_CONFIG = {
21
+ dev: { allowlist: [], severityThreshold: "moderate" },
22
+ prod: { allowlist: [], severityThreshold: "low" }
23
+ };
24
+ function isSeverityAtOrAbove(severity, threshold) {
25
+ if (severity === void 0) return true;
26
+ const levels = SEVERITY_THRESHOLDS;
27
+ const severityIndex = levels.indexOf(severity);
28
+ const thresholdIndex = SEVERITY_THRESHOLDS.indexOf(threshold);
29
+ if (severityIndex === -1) return true;
30
+ return severityIndex >= thresholdIndex;
31
+ }
32
+ export {
33
+ AUDIT_SCOPES,
34
+ DEFAULT_CONFIG,
35
+ SEVERITY_THRESHOLDS,
36
+ allowlistEntrySchema,
37
+ isSeverityAtOrAbove,
38
+ scopeConfigSchema,
39
+ v11yCheckConfigSchema
40
+ };
package/package.json CHANGED
@@ -1,4 +1,54 @@
1
1
  {
2
2
  "name": "v11y-check",
3
- "version": "0.0.0"
4
- }
3
+ "version": "0.7.0",
4
+ "private": false,
5
+ "description": "Wrap audit-ci with a richer config model, typed JSON source of truth, and sync workflow",
6
+ "keywords": [
7
+ "audit",
8
+ "audit-ci",
9
+ "dependencies",
10
+ "security",
11
+ "vulnerabilities"
12
+ ],
13
+ "homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/v11y-check#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/williamthorsen/node-monorepo-tools/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/williamthorsen/node-monorepo-tools.git",
20
+ "directory": "packages/v11y-check"
21
+ },
22
+ "license": "ISC",
23
+ "author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "import": "./dist/esm/index.js",
28
+ "types": "./dist/esm/index.d.ts"
29
+ }
30
+ },
31
+ "bin": {
32
+ "v11y": "bin/v11y.js",
33
+ "v11y-check": "bin/v11y.js"
34
+ },
35
+ "files": [
36
+ "bin",
37
+ "dist",
38
+ "templates"
39
+ ],
40
+ "dependencies": {
41
+ "audit-ci": "7.1.0",
42
+ "zod": "4.4.3",
43
+ "@williamthorsen/nmr-core": "0.3.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.17.0"
47
+ },
48
+ "scripts": {
49
+ "test": "pnpm exec vitest --config=vitest.standalone.config.ts",
50
+ "test:coverage": "pnpm exec vitest --config=vitest.standalone.config.ts --coverage",
51
+ "test:integration": "pnpm exec vitest --config=vitest.integration.config.ts",
52
+ "test:watch": "pnpm exec vitest --config=vitest.standalone.config.ts --watch"
53
+ }
54
+ }
@@ -0,0 +1,27 @@
1
+ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2
+ name: Dependency audit
3
+
4
+ on:
5
+ pull_request:
6
+ branches:
7
+ - main
8
+ - next
9
+
10
+ # Daily schedule — adjust cron expression per repo if a different cadence is desired.
11
+ schedule:
12
+ - cron: '0 8 * * *'
13
+
14
+ workflow_dispatch: {}
15
+
16
+ # Permissions must be declared here. The GITHUB_TOKEN scope passed to the called
17
+ # reusable workflow is bounded by this caller's permissions block; when omitted,
18
+ # the repository's default workflow permissions apply (read-only here), which would
19
+ # silently deny the issues/pull-requests writes the reusable workflow needs.
20
+ permissions:
21
+ contents: read
22
+ issues: write
23
+ pull-requests: write
24
+
25
+ jobs:
26
+ audit:
27
+ uses: williamthorsen/node-monorepo-tools/.github/workflows/audit.reusable.yaml@workflow/audit-v1