@securitychecks/cli 0.1.1-rc.1

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/dist/lib.js ADDED
@@ -0,0 +1,2187 @@
1
+ import { resolveTargetPath, collect, ARTIFACT_SCHEMA_VERSION } from '@securitychecks/collector';
2
+ import { readFile, mkdir, writeFile } from 'fs/promises';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join, dirname } from 'path';
6
+ import { gzipSync } from 'zlib';
7
+ import { randomUUID, createHash } from 'crypto';
8
+
9
+ // src/audit.ts
10
+ var CONFIG_DIR = join(homedir(), ".securitychecks");
11
+ join(CONFIG_DIR, "config.json");
12
+ function normalizeApiBaseUrl(input) {
13
+ const value = input.trim();
14
+ if (!value) {
15
+ throw new Error("API URL is empty");
16
+ }
17
+ let url;
18
+ try {
19
+ url = new URL(value);
20
+ } catch {
21
+ throw new Error(`Invalid API URL: ${input}`);
22
+ }
23
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
24
+ throw new Error("API URL must start with http:// or https://");
25
+ }
26
+ if (url.username || url.password) {
27
+ throw new Error("API URL must not include credentials");
28
+ }
29
+ url.hash = "";
30
+ url.search = "";
31
+ const pathname = url.pathname.replace(/\/+$/, "");
32
+ const stripped = pathname.replace(/\/api\/v1$/i, "").replace(/\/v1$/i, "");
33
+ url.pathname = stripped.length === 0 ? "/" : `${stripped}/`;
34
+ return url.toString().replace(/\/$/, "");
35
+ }
36
+
37
+ // src/lib/license.ts
38
+ var CLOUD_API_KEY_ENV_VARS = [
39
+ "SECURITYCHECKS_API_KEY",
40
+ "SECURITYCHECKS_LICENSE_KEY"
41
+ ];
42
+ var DEFAULT_CLOUD_BASE_URL = "https://api.securitychecks.ai";
43
+ function getCloudApiKey() {
44
+ for (const envVar of CLOUD_API_KEY_ENV_VARS) {
45
+ const value = process.env[envVar];
46
+ if (value) {
47
+ return value;
48
+ }
49
+ }
50
+ return void 0;
51
+ }
52
+ function getCloudApiBaseUrl() {
53
+ const raw = process.env["SECURITYCHECKS_API_URL"] ?? DEFAULT_CLOUD_BASE_URL;
54
+ return normalizeApiBaseUrl(raw);
55
+ }
56
+
57
+ // src/lib/errors.ts
58
+ var ErrorCodes = {
59
+ // CONFIG errors (001-099)
60
+ CONFIG_NOT_FOUND: "SC_CONFIG_001",
61
+ CONFIG_INVALID: "SC_CONFIG_002",
62
+ CONFIG_SCHEMA_ERROR: "SC_CONFIG_003",
63
+ // PARSE errors (100-199)
64
+ PARSE_TYPESCRIPT_ERROR: "SC_PARSE_101",
65
+ PARSE_FILE_NOT_FOUND: "SC_PARSE_102",
66
+ PARSE_UNSUPPORTED_SYNTAX: "SC_PARSE_103",
67
+ // CHECK errors (200-299)
68
+ CHECK_EXECUTION_ERROR: "SC_CHECK_201",
69
+ CHECK_TIMEOUT: "SC_CHECK_202",
70
+ CHECK_INVARIANT_NOT_FOUND: "SC_CHECK_203",
71
+ // IO errors (300-399)
72
+ IO_READ_ERROR: "SC_IO_301",
73
+ IO_WRITE_ERROR: "SC_IO_302",
74
+ IO_PERMISSION_DENIED: "SC_IO_303",
75
+ IO_PATH_NOT_FOUND: "SC_IO_304",
76
+ // CLI errors (400-499)
77
+ CLI_INVALID_ARGUMENT: "SC_CLI_401",
78
+ CLI_MISSING_ARGUMENT: "SC_CLI_402",
79
+ CLI_UNKNOWN_COMMAND: "SC_CLI_403",
80
+ // ARTIFACT errors (500-599)
81
+ ARTIFACT_NOT_FOUND: "SC_ARTIFACT_501",
82
+ ARTIFACT_INVALID: "SC_ARTIFACT_502",
83
+ ARTIFACT_VERSION_MISMATCH: "SC_ARTIFACT_503",
84
+ // CLOUD errors (600-699)
85
+ CLOUD_AUTH_FAILED: "SC_CLOUD_601",
86
+ CLOUD_PERMISSION_DENIED: "SC_CLOUD_602",
87
+ CLOUD_NOT_FOUND: "SC_CLOUD_603",
88
+ CLOUD_RATE_LIMITED: "SC_CLOUD_604",
89
+ CLOUD_API_ERROR: "SC_CLOUD_605",
90
+ CLOUD_NETWORK_ERROR: "SC_CLOUD_606",
91
+ CLOUD_INVALID_API_KEY: "SC_CLOUD_607",
92
+ AUTH_REQUIRED: "SC_CLOUD_608",
93
+ OFFLINE_NOT_SUPPORTED: "SC_CLOUD_609"
94
+ };
95
+ var ErrorMessages = {
96
+ [ErrorCodes.CONFIG_NOT_FOUND]: "Configuration file not found",
97
+ [ErrorCodes.CONFIG_INVALID]: "Configuration file is invalid",
98
+ [ErrorCodes.CONFIG_SCHEMA_ERROR]: "Configuration does not match expected schema",
99
+ [ErrorCodes.PARSE_TYPESCRIPT_ERROR]: "Failed to parse TypeScript file",
100
+ [ErrorCodes.PARSE_FILE_NOT_FOUND]: "Source file not found",
101
+ [ErrorCodes.PARSE_UNSUPPORTED_SYNTAX]: "Unsupported syntax encountered",
102
+ [ErrorCodes.CHECK_EXECUTION_ERROR]: "Error executing invariant check",
103
+ [ErrorCodes.CHECK_TIMEOUT]: "Invariant check timed out",
104
+ [ErrorCodes.CHECK_INVARIANT_NOT_FOUND]: "Invariant not found",
105
+ [ErrorCodes.IO_READ_ERROR]: "Failed to read file",
106
+ [ErrorCodes.IO_WRITE_ERROR]: "Failed to write file",
107
+ [ErrorCodes.IO_PERMISSION_DENIED]: "Permission denied",
108
+ [ErrorCodes.IO_PATH_NOT_FOUND]: "Path not found",
109
+ [ErrorCodes.CLI_INVALID_ARGUMENT]: "Invalid argument provided",
110
+ [ErrorCodes.CLI_MISSING_ARGUMENT]: "Required argument missing",
111
+ [ErrorCodes.CLI_UNKNOWN_COMMAND]: "Unknown command",
112
+ [ErrorCodes.ARTIFACT_NOT_FOUND]: "Artifact file not found",
113
+ [ErrorCodes.ARTIFACT_INVALID]: "Invalid artifact format",
114
+ [ErrorCodes.ARTIFACT_VERSION_MISMATCH]: "Artifact version not supported",
115
+ [ErrorCodes.CLOUD_AUTH_FAILED]: "Authentication failed",
116
+ [ErrorCodes.CLOUD_PERMISSION_DENIED]: "Permission denied",
117
+ [ErrorCodes.CLOUD_NOT_FOUND]: "Resource not found",
118
+ [ErrorCodes.CLOUD_RATE_LIMITED]: "Rate limit exceeded",
119
+ [ErrorCodes.CLOUD_API_ERROR]: "Cloud API error",
120
+ [ErrorCodes.CLOUD_NETWORK_ERROR]: "Network error",
121
+ [ErrorCodes.CLOUD_INVALID_API_KEY]: "Invalid API key format",
122
+ [ErrorCodes.AUTH_REQUIRED]: "API key required for evaluation",
123
+ [ErrorCodes.OFFLINE_NOT_SUPPORTED]: "Offline mode is not supported"
124
+ };
125
+ var ErrorRemediation = {
126
+ // CONFIG errors
127
+ [ErrorCodes.CONFIG_NOT_FOUND]: `
128
+ Create a configuration file in your project root:
129
+
130
+ scheck init
131
+
132
+ Or create securitychecks.config.ts manually:
133
+
134
+ export default {
135
+ include: ['src/**/*.ts'],
136
+ exclude: ['node_modules/**'],
137
+ };
138
+ `.trim(),
139
+ [ErrorCodes.CONFIG_INVALID]: `
140
+ Check your securitychecks.config.ts for syntax errors.
141
+
142
+ Common issues:
143
+ - Missing export default
144
+ - Invalid JSON in securitychecks.json
145
+ - Typo in configuration keys
146
+
147
+ Run with --verbose for more details.
148
+ `.trim(),
149
+ [ErrorCodes.CONFIG_SCHEMA_ERROR]: `
150
+ Your configuration has invalid options. Check these common issues:
151
+
152
+ - 'include' and 'exclude' must be arrays of glob patterns
153
+ - 'testPatterns' must be an array of test file patterns
154
+ - 'servicePatterns' must be an array of service file patterns
155
+
156
+ See: https://securitychecks.ai/docs/configuration
157
+ `.trim(),
158
+ // PARSE errors
159
+ [ErrorCodes.PARSE_TYPESCRIPT_ERROR]: `
160
+ A TypeScript file failed to parse. This usually means:
161
+
162
+ 1. The file has syntax errors - run tsc to check
163
+ 2. The file uses unsupported TypeScript features
164
+ 3. There are missing dependencies
165
+
166
+ Try:
167
+ npx tsc --noEmit
168
+
169
+ If the error persists, exclude the problematic file:
170
+ exclude: ['path/to/problematic-file.ts']
171
+ `.trim(),
172
+ [ErrorCodes.PARSE_FILE_NOT_FOUND]: `
173
+ The specified source file doesn't exist. Check:
174
+
175
+ 1. The file path is correct
176
+ 2. The file hasn't been moved or deleted
177
+ 3. Your include/exclude patterns are correct
178
+
179
+ Run: ls <path> to verify the file exists.
180
+ `.trim(),
181
+ [ErrorCodes.PARSE_UNSUPPORTED_SYNTAX]: `
182
+ The file contains syntax that can't be parsed. This may happen with:
183
+
184
+ - Very new TypeScript/JavaScript features
185
+ - Non-standard syntax extensions
186
+ - Malformed source code
187
+
188
+ Try excluding the file or updating the parser.
189
+ `.trim(),
190
+ // CHECK errors
191
+ [ErrorCodes.CHECK_EXECUTION_ERROR]: `
192
+ An invariant check failed to run. This is usually a bug in scheck.
193
+
194
+ Please report this issue with:
195
+ 1. The full error message (--verbose)
196
+ 2. A minimal reproduction
197
+ 3. Your Node.js and @securitychecks/cli versions
198
+
199
+ Report at: https://github.com/securitychecks/securitychecks.ai/issues
200
+ `.trim(),
201
+ [ErrorCodes.CHECK_TIMEOUT]: `
202
+ An invariant check took too long. This can happen with:
203
+
204
+ 1. Very large codebases
205
+ 2. Complex file structures
206
+ 3. Slow file system access
207
+
208
+ Try:
209
+ - Narrowing include patterns to scan fewer files
210
+ - Excluding large generated files
211
+ - Running with --only to check specific invariants
212
+ `.trim(),
213
+ [ErrorCodes.CHECK_INVARIANT_NOT_FOUND]: `
214
+ The specified invariant ID doesn't exist.
215
+
216
+ List available invariants:
217
+ scheck explain --list
218
+
219
+ Common invariant IDs:
220
+ - AUTHZ.SERVICE_LAYER.ENFORCED
221
+ - WEBHOOK.IDEMPOTENT
222
+ - TRANSACTION.POST_COMMIT.SIDE_EFFECTS
223
+ `.trim(),
224
+ // IO errors
225
+ [ErrorCodes.IO_READ_ERROR]: `
226
+ Failed to read a file. Check:
227
+
228
+ 1. The file exists and is readable
229
+ 2. You have permission to read the file
230
+ 3. The file is not locked by another process
231
+
232
+ Try: cat <file> to verify readability.
233
+ `.trim(),
234
+ [ErrorCodes.IO_WRITE_ERROR]: `
235
+ Failed to write a file. Check:
236
+
237
+ 1. The directory exists
238
+ 2. You have write permission
239
+ 3. There's enough disk space
240
+ 4. The file is not locked
241
+
242
+ Try: touch <file> to verify writability.
243
+ `.trim(),
244
+ [ErrorCodes.IO_PERMISSION_DENIED]: `
245
+ Permission denied accessing a file or directory.
246
+
247
+ On Unix/Mac:
248
+ chmod +r <file> # Make readable
249
+ chmod +w <file> # Make writable
250
+
251
+ On Windows: Check file properties > Security tab.
252
+ `.trim(),
253
+ [ErrorCodes.IO_PATH_NOT_FOUND]: `
254
+ The specified path doesn't exist.
255
+
256
+ Check:
257
+ 1. You're in the correct directory
258
+ 2. The path is spelled correctly
259
+ 3. The directory structure is correct
260
+
261
+ Run: pwd && ls to verify your location.
262
+ `.trim(),
263
+ // CLI errors
264
+ [ErrorCodes.CLI_INVALID_ARGUMENT]: `
265
+ Invalid command-line argument.
266
+
267
+ Run: scheck --help
268
+
269
+ Common commands:
270
+ scheck run # Run all checks
271
+ scheck run --ci # CI mode (fails on P0/P1)
272
+ scheck explain <id> # Explain an invariant
273
+ scheck init # Initialize configuration
274
+ `.trim(),
275
+ [ErrorCodes.CLI_MISSING_ARGUMENT]: `
276
+ A required argument is missing.
277
+
278
+ Check the command syntax:
279
+ scheck --help
280
+ scheck <command> --help
281
+ `.trim(),
282
+ [ErrorCodes.CLI_UNKNOWN_COMMAND]: `
283
+ Unknown command. Available commands:
284
+
285
+ run Run invariant checks
286
+ init Initialize configuration
287
+ explain Explain an invariant
288
+ baseline Manage baseline
289
+ waive Waive a finding
290
+
291
+ Run: scheck --help
292
+ `.trim(),
293
+ // ARTIFACT errors
294
+ [ErrorCodes.ARTIFACT_NOT_FOUND]: `
295
+ Artifact file not found.
296
+
297
+ The artifact file stores collected code facts. Options:
298
+
299
+ 1. Let scheck collect automatically (default):
300
+ scheck run
301
+
302
+ 2. Collect manually first:
303
+ npx scc collect -o .securitychecks/artifacts.json
304
+ scheck run --artifact .securitychecks/artifacts.json
305
+
306
+ 3. Check if file was deleted:
307
+ ls .securitychecks/
308
+ `.trim(),
309
+ [ErrorCodes.ARTIFACT_INVALID]: `
310
+ The artifact file is malformed or corrupt.
311
+
312
+ Common issues:
313
+ - Incomplete JSON (process was killed during write)
314
+ - Modified manually with syntax errors
315
+ - Wrong file format
316
+
317
+ Fix:
318
+ 1. Delete the corrupt artifact:
319
+ rm .securitychecks/artifacts.json
320
+
321
+ 2. Re-collect:
322
+ scheck run
323
+ (or: npx scc collect -o .securitychecks/artifacts.json)
324
+ `.trim(),
325
+ [ErrorCodes.ARTIFACT_VERSION_MISMATCH]: `
326
+ The artifact was created by an incompatible version.
327
+
328
+ This happens when:
329
+ - Artifact was created by an older/newer scheck version
330
+ - Artifact schema has changed
331
+
332
+ Fix:
333
+ 1. Delete the old artifact:
334
+ rm .securitychecks/artifacts.json
335
+
336
+ 2. Re-collect with current version:
337
+ scheck run
338
+
339
+ Your current version: scheck --version
340
+ `.trim(),
341
+ // CLOUD errors
342
+ [ErrorCodes.CLOUD_AUTH_FAILED]: `
343
+ Authentication failed. Your API key may be invalid or expired.
344
+
345
+ Fix:
346
+ 1. Generate a new API key at https://securitychecks.ai/dashboard/settings/api-keys
347
+ 2. Log in again:
348
+ scheck login
349
+
350
+ Environment variable:
351
+ export SECURITYCHECKS_API_KEY=sc_live_...
352
+ `.trim(),
353
+ [ErrorCodes.CLOUD_PERMISSION_DENIED]: `
354
+ You don't have permission for this action.
355
+
356
+ Check:
357
+ 1. You have access to the project/organization
358
+ 2. Your API key has the required scopes
359
+ 3. Your subscription is active
360
+
361
+ Manage at: https://securitychecks.ai/dashboard
362
+ `.trim(),
363
+ [ErrorCodes.CLOUD_NOT_FOUND]: `
364
+ The requested resource was not found.
365
+
366
+ Check:
367
+ 1. The project slug is correct
368
+ 2. The project exists and you have access
369
+ 3. The resource ID is valid
370
+
371
+ List your projects:
372
+ scheck config --show
373
+ `.trim(),
374
+ [ErrorCodes.CLOUD_RATE_LIMITED]: `
375
+ You've hit the rate limit. Please try again later.
376
+
377
+ Options:
378
+ 1. Wait a few minutes and retry
379
+ 2. Upgrade your plan for higher limits
380
+
381
+ Plan limits: https://securitychecks.ai/pricing
382
+ `.trim(),
383
+ [ErrorCodes.CLOUD_API_ERROR]: `
384
+ The SecurityChecks API returned an error.
385
+
386
+ This could be:
387
+ 1. A temporary service issue - try again shortly
388
+ 2. An invalid request - check your parameters
389
+
390
+ Status: https://status.securitychecks.ai
391
+ Help: https://securitychecks.ai/docs/troubleshooting
392
+ `.trim(),
393
+ [ErrorCodes.CLOUD_NETWORK_ERROR]: `
394
+ Could not connect to SecurityChecks API.
395
+
396
+ Check:
397
+ 1. Your internet connection
398
+ 2. Firewall/proxy settings
399
+ 3. API endpoint accessibility
400
+
401
+ Default API: https://api.securitychecks.ai
402
+ `.trim(),
403
+ [ErrorCodes.CLOUD_INVALID_API_KEY]: `
404
+ The API key format is invalid.
405
+
406
+ API keys should start with:
407
+ - sc_live_ for production
408
+ - sc_test_ for testing
409
+
410
+ Get a key at: https://securitychecks.ai/dashboard/settings/api-keys
411
+ `.trim(),
412
+ [ErrorCodes.AUTH_REQUIRED]: `
413
+ An API key is required to run security checks.
414
+
415
+ SecurityChecks uses cloud evaluation to protect proprietary patterns.
416
+ Your source code never leaves your machine - only structural facts are sent.
417
+
418
+ Setup:
419
+ 1. Get your API key at https://securitychecks.ai/dashboard/settings/api-keys
420
+ 2. Set environment variable:
421
+ export SECURITYCHECKS_API_KEY=sc_live_...
422
+
423
+ Or add to securitychecks.config.yaml:
424
+ calibration:
425
+ apiKey: sc_live_...
426
+ `.trim(),
427
+ [ErrorCodes.OFFLINE_NOT_SUPPORTED]: `
428
+ Offline mode is not supported.
429
+
430
+ SecurityChecks requires cloud evaluation to protect proprietary patterns.
431
+ Your source code never leaves your machine - only structural facts are sent.
432
+
433
+ Options:
434
+ 1. Remove --offline flag and ensure network connectivity
435
+ 2. For air-gapped environments, contact sales for an enterprise on-premise license:
436
+ https://securitychecks.ai/enterprise
437
+ `.trim()
438
+ };
439
+ var CLIError = class _CLIError extends Error {
440
+ code;
441
+ details;
442
+ cause;
443
+ constructor(code, message, options) {
444
+ const baseMessage = message ?? ErrorMessages[code];
445
+ super(baseMessage, { cause: options?.cause });
446
+ this.name = "CLIError";
447
+ this.code = code;
448
+ this.details = options?.details;
449
+ this.cause = options?.cause;
450
+ Error.captureStackTrace?.(this, _CLIError);
451
+ }
452
+ /**
453
+ * Get remediation guidance for this error
454
+ */
455
+ getRemediation() {
456
+ return ErrorRemediation[this.code];
457
+ }
458
+ /**
459
+ * Format error for user display
460
+ */
461
+ toUserString(verbose = false) {
462
+ const parts = [`[${this.code}] ${this.message}`];
463
+ if (verbose && this.details) {
464
+ parts.push(`
465
+ Details: ${JSON.stringify(this.details, null, 2)}`);
466
+ }
467
+ if (verbose && this.cause) {
468
+ parts.push(`
469
+ Caused by: ${this.cause.message}`);
470
+ if (this.cause.stack) {
471
+ parts.push(`
472
+ ${this.cause.stack}`);
473
+ }
474
+ }
475
+ return parts.join("");
476
+ }
477
+ /**
478
+ * Format error with remediation for user display
479
+ */
480
+ toUserStringWithRemediation() {
481
+ const parts = [this.toUserString()];
482
+ const remediation = this.getRemediation();
483
+ if (remediation) {
484
+ parts.push("\n\nHow to fix:\n");
485
+ const indented = remediation.split("\n").map((line) => ` ${line}`).join("\n");
486
+ parts.push(indented);
487
+ }
488
+ return parts.join("");
489
+ }
490
+ /**
491
+ * Format error for JSON output
492
+ */
493
+ toJSON() {
494
+ return {
495
+ code: this.code,
496
+ message: this.message,
497
+ remediation: this.getRemediation(),
498
+ details: this.details,
499
+ cause: this.cause ? {
500
+ message: this.cause.message,
501
+ stack: this.cause.stack
502
+ } : void 0
503
+ };
504
+ }
505
+ };
506
+ function isCLIError(error) {
507
+ return error instanceof CLIError;
508
+ }
509
+ function wrapError(error, code, message) {
510
+ if (error instanceof CLIError) {
511
+ return error;
512
+ }
513
+ const cause = error instanceof Error ? error : new Error(String(error));
514
+ return new CLIError(code, message, { cause });
515
+ }
516
+ function detectCIContext() {
517
+ if (process.env["GITHUB_ACTIONS"] === "true") {
518
+ return detectGitHubActions();
519
+ }
520
+ if (process.env["GITLAB_CI"] === "true") {
521
+ return detectGitLabCI();
522
+ }
523
+ if (process.env["CIRCLECI"] === "true") {
524
+ return detectCircleCI();
525
+ }
526
+ if (process.env["JENKINS_URL"]) {
527
+ return detectJenkins();
528
+ }
529
+ return null;
530
+ }
531
+ function detectGitHubActions() {
532
+ const eventName = process.env["GITHUB_EVENT_NAME"];
533
+ const isPullRequest = eventName === "pull_request" || eventName === "pull_request_target";
534
+ const eventPayload = readGitHubEventPayload();
535
+ let branch;
536
+ if (isPullRequest) {
537
+ branch = eventPayload?.pull_request?.head?.ref || process.env["GITHUB_HEAD_REF"];
538
+ } else {
539
+ const ref = process.env["GITHUB_REF"] || "";
540
+ branch = ref.replace(/^refs\/heads\//, "");
541
+ }
542
+ const commitSha = isPullRequest ? eventPayload?.pull_request?.head?.sha || process.env["GITHUB_SHA"] : process.env["GITHUB_SHA"];
543
+ let prNumber;
544
+ if (isPullRequest) {
545
+ prNumber = eventPayload?.number;
546
+ if (!prNumber) {
547
+ const prRef = process.env["GITHUB_REF"] || "";
548
+ const match = prRef.match(/refs\/pull\/(\d+)/);
549
+ if (match && match[1]) {
550
+ prNumber = parseInt(match[1], 10);
551
+ }
552
+ }
553
+ }
554
+ return {
555
+ provider: "github-actions",
556
+ branch,
557
+ commitSha,
558
+ prNumber,
559
+ repository: process.env["GITHUB_REPOSITORY"],
560
+ isPullRequest
561
+ };
562
+ }
563
+ function readGitHubEventPayload() {
564
+ const eventPath = process.env["GITHUB_EVENT_PATH"];
565
+ if (!eventPath) {
566
+ return null;
567
+ }
568
+ try {
569
+ const raw = readFileSync(eventPath, "utf8");
570
+ return JSON.parse(raw);
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ function detectGitLabCI() {
576
+ const mrIid = process.env["CI_MERGE_REQUEST_IID"];
577
+ const isPullRequest = !!mrIid;
578
+ let prNumber;
579
+ if (mrIid) {
580
+ prNumber = parseInt(mrIid, 10);
581
+ }
582
+ return {
583
+ provider: "gitlab-ci",
584
+ branch: process.env["CI_COMMIT_REF_NAME"],
585
+ commitSha: process.env["CI_COMMIT_SHA"],
586
+ prNumber,
587
+ repository: process.env["CI_PROJECT_PATH"],
588
+ isPullRequest
589
+ };
590
+ }
591
+ function detectCircleCI() {
592
+ let prNumber;
593
+ const prUrl = process.env["CIRCLE_PULL_REQUEST"];
594
+ if (prUrl) {
595
+ const match = prUrl.match(/\/pull\/(\d+)/);
596
+ if (match && match[1]) {
597
+ prNumber = parseInt(match[1], 10);
598
+ }
599
+ }
600
+ return {
601
+ provider: "circleci",
602
+ branch: process.env["CIRCLE_BRANCH"],
603
+ commitSha: process.env["CIRCLE_SHA1"],
604
+ prNumber,
605
+ repository: `${process.env["CIRCLE_PROJECT_USERNAME"]}/${process.env["CIRCLE_PROJECT_REPONAME"]}`,
606
+ isPullRequest: !!prUrl
607
+ };
608
+ }
609
+ function detectJenkins() {
610
+ const changeId = process.env["CHANGE_ID"];
611
+ const isPullRequest = !!changeId;
612
+ let prNumber;
613
+ if (changeId) {
614
+ prNumber = parseInt(changeId, 10);
615
+ }
616
+ return {
617
+ provider: "jenkins",
618
+ branch: process.env["BRANCH_NAME"] || process.env["GIT_BRANCH"],
619
+ commitSha: process.env["GIT_COMMIT"],
620
+ prNumber,
621
+ isPullRequest
622
+ };
623
+ }
624
+ function buildHeaders(extra = {}) {
625
+ const headers = { ...extra };
626
+ const bypassSecret = process.env["VERCEL_AUTOMATION_BYPASS_SECRET"];
627
+ if (bypassSecret) {
628
+ headers["x-vercel-protection-bypass"] = bypassSecret;
629
+ }
630
+ return headers;
631
+ }
632
+ function isTruthy(value) {
633
+ if (!value) return false;
634
+ return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
635
+ }
636
+ function isCloudEvalAvailable(apiKey) {
637
+ return !!apiKey;
638
+ }
639
+ function buildEvaluatePayload(artifact, options) {
640
+ const ciContext = options.ciContext !== void 0 ? options.ciContext : detectCIContext();
641
+ return {
642
+ artifact: {
643
+ version: artifact.version,
644
+ schemaVersion: artifact.schemaVersion,
645
+ profile: artifact.profile,
646
+ extractedAt: artifact.extractedAt,
647
+ targetPath: artifact.codebase?.root,
648
+ codebase: {
649
+ file_count: artifact.codebase?.filesScanned ?? 0,
650
+ languages: artifact.codebase?.languages ?? []
651
+ },
652
+ services: artifact.services,
653
+ authzCalls: artifact.authzCalls ?? [],
654
+ cacheOperations: artifact.cacheOperations ?? [],
655
+ transactionScopes: artifact.transactionScopes ?? [],
656
+ webhookHandlers: artifact.webhookHandlers ?? [],
657
+ jobHandlers: artifact.jobHandlers ?? [],
658
+ membershipMutations: artifact.membershipMutations ?? [],
659
+ tests: artifact.tests ?? [],
660
+ routes: artifact.routes ?? [],
661
+ callGraph: artifact.callGraph,
662
+ dataFlow: artifact.dataFlows?.flows ?? [],
663
+ rlsPolicies: artifact.rlsArtifact?.rlsPolicies ?? []
664
+ },
665
+ options: {
666
+ invariants: options.invariants,
667
+ skip: options.skip,
668
+ severity: options.severity,
669
+ projectSlug: options.projectSlug,
670
+ // CI context for scan association (enables PR comments)
671
+ branch: ciContext?.branch,
672
+ commitSha: ciContext?.commitSha,
673
+ prNumber: ciContext?.prNumber,
674
+ // Opt-in guard for GitHub PR CI mode to enforce webhook + evaluate env alignment.
675
+ // This is intentionally gated to avoid breaking standalone CLI workflows that do not use GitHub App webhooks.
676
+ requireExistingCiScan: ciContext?.provider === "github-actions" && ciContext?.isPullRequest && isTruthy(process.env["SECURITYCHECKS_REQUIRE_EXISTING_CI_SCAN"]) ? true : void 0
677
+ }
678
+ };
679
+ }
680
+ async function submitForEvaluation(artifact, options) {
681
+ const endpoint = `${options.baseUrl}/api/v1/evaluate`;
682
+ const payload = buildEvaluatePayload(artifact, options);
683
+ const json = JSON.stringify(payload);
684
+ const gz = gzipSync(Buffer.from(json, "utf8"));
685
+ const response = await fetch(endpoint, {
686
+ method: "POST",
687
+ headers: buildHeaders({
688
+ "Content-Type": "application/json",
689
+ "Content-Encoding": "gzip",
690
+ Authorization: `Bearer ${options.apiKey}`
691
+ }),
692
+ body: gz
693
+ });
694
+ if (!response.ok) {
695
+ const errorBody = await response.json().catch(() => ({}));
696
+ const serverMessage = errorBody?.message;
697
+ const errorText = serverMessage ? `${errorBody.error || "Cloud API error"}: ${serverMessage}` : errorBody.error;
698
+ if (response.status === 401) {
699
+ throw new CLIError(ErrorCodes.CLOUD_AUTH_FAILED, "Invalid API key. Check your SECURITYCHECKS_API_KEY.");
700
+ }
701
+ if (response.status === 413) {
702
+ throw new CLIError(
703
+ ErrorCodes.CLOUD_API_ERROR,
704
+ `Artifact too large. ${errorBody.details || "Contact support if this persists."}`,
705
+ { details: errorBody.details }
706
+ );
707
+ }
708
+ if (response.status === 429) {
709
+ const remaining = errorBody.usage?.scansRemaining ?? 0;
710
+ throw new CLIError(
711
+ ErrorCodes.CLOUD_RATE_LIMITED,
712
+ `Monthly scan limit reached (${remaining} remaining). Upgrade at https://securitychecks.ai/pricing`,
713
+ { details: { scansRemaining: remaining } }
714
+ );
715
+ }
716
+ if (response.status === 503) {
717
+ throw new CLIError(ErrorCodes.CLOUD_API_ERROR, "Cloud evaluation temporarily unavailable. Try again later.");
718
+ }
719
+ throw new CLIError(
720
+ ErrorCodes.CLOUD_API_ERROR,
721
+ errorText || `Cloud API error: ${response.status}`,
722
+ { details: errorBody }
723
+ );
724
+ }
725
+ return await response.json();
726
+ }
727
+ async function pollForResults(scanId, options) {
728
+ const timeout = options.timeout ?? 3e5;
729
+ const pollInterval = options.pollInterval ?? 2e3;
730
+ const startTime = Date.now();
731
+ while (Date.now() - startTime < timeout) {
732
+ const response = await fetch(`${options.baseUrl}/api/v1/scans/${scanId}`, {
733
+ method: "GET",
734
+ headers: buildHeaders({
735
+ Authorization: `Bearer ${options.apiKey}`
736
+ })
737
+ });
738
+ if (!response.ok) {
739
+ if (response.status === 404) {
740
+ throw new CLIError(ErrorCodes.CLOUD_NOT_FOUND, `Scan ${scanId} not found`);
741
+ }
742
+ throw new CLIError(ErrorCodes.CLOUD_API_ERROR, `Failed to get scan status: ${response.status}`);
743
+ }
744
+ const scan = await response.json();
745
+ options.onProgress?.({
746
+ status: scan.status,
747
+ message: scan.status === "RUNNING" ? "Evaluating..." : void 0
748
+ });
749
+ if (scan.status === "COMPLETED") {
750
+ return {
751
+ findings: scan.findings ?? [],
752
+ stats: scan.stats ?? {
753
+ invariantsRun: 0,
754
+ patternsRun: 0,
755
+ findingsCount: scan.findings?.length ?? 0,
756
+ executionMs: Date.now() - startTime
757
+ },
758
+ usage: scan.usage ?? { scansUsed: 0, scansRemaining: 0 }
759
+ };
760
+ }
761
+ if (scan.status === "FAILED") {
762
+ throw new CLIError(
763
+ ErrorCodes.CLOUD_API_ERROR,
764
+ `Scan failed: ${scan.errorMessage ?? "Unknown error"}`,
765
+ { details: { scanId, errorMessage: scan.errorMessage } }
766
+ );
767
+ }
768
+ if (scan.status === "CANCELLED") {
769
+ throw new CLIError(ErrorCodes.CLOUD_API_ERROR, "Scan was cancelled", { details: { scanId } });
770
+ }
771
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
772
+ }
773
+ throw new CLIError(
774
+ ErrorCodes.CHECK_TIMEOUT,
775
+ `Scan timed out after ${timeout / 1e3}s. Check dashboard for results.`,
776
+ { details: { timeoutMs: timeout, scanId } }
777
+ );
778
+ }
779
+ async function evaluateCloud(artifact, options) {
780
+ const { scanId } = await submitForEvaluation(artifact, options);
781
+ options.onProgress?.({
782
+ status: "PENDING",
783
+ message: "Submitted for evaluation..."
784
+ });
785
+ return pollForResults(scanId, options);
786
+ }
787
+ async function checkCloudHealth(baseUrl) {
788
+ try {
789
+ const response = await fetch(`${baseUrl}/api/health`, {
790
+ method: "GET",
791
+ headers: buildHeaders(),
792
+ signal: AbortSignal.timeout(5e3)
793
+ });
794
+ return response.ok;
795
+ } catch {
796
+ return false;
797
+ }
798
+ }
799
+ async function getCloudInvariants(baseUrl, apiKey) {
800
+ const response = await fetch(`${baseUrl}/api/v1/evaluate`, {
801
+ method: "GET",
802
+ headers: buildHeaders({
803
+ Authorization: `Bearer ${apiKey}`
804
+ })
805
+ });
806
+ if (!response.ok) {
807
+ throw new CLIError(ErrorCodes.CLOUD_API_ERROR, `Failed to fetch invariants: ${response.status}`);
808
+ }
809
+ const data = await response.json();
810
+ return data.invariants;
811
+ }
812
+
813
+ // src/lib/project-slug.ts
814
+ function getProjectSlug(env = process.env) {
815
+ const raw = env["SECURITYCHECKS_PROJECT"];
816
+ const trimmed = raw?.trim();
817
+ return trimmed ? trimmed : void 0;
818
+ }
819
+
820
+ // src/audit.ts
821
+ function toArtifact(collectorArtifact) {
822
+ return {
823
+ version: "1.0",
824
+ extractedAt: collectorArtifact.extractedAt,
825
+ targetPath: collectorArtifact.codebase.root,
826
+ services: collectorArtifact.services,
827
+ authzCalls: collectorArtifact.authzCalls ?? [],
828
+ cacheOperations: collectorArtifact.cacheOperations ?? [],
829
+ transactionScopes: collectorArtifact.transactionScopes ?? [],
830
+ webhookHandlers: collectorArtifact.webhookHandlers ?? [],
831
+ jobHandlers: collectorArtifact.jobHandlers ?? [],
832
+ membershipMutations: collectorArtifact.membershipMutations ?? [],
833
+ tests: collectorArtifact.tests ?? [],
834
+ routes: collectorArtifact.routes ?? [],
835
+ dataFlows: collectorArtifact.dataFlows,
836
+ callGraph: collectorArtifact.callGraph
837
+ };
838
+ }
839
+ async function audit(options = {}) {
840
+ const startTime = Date.now();
841
+ const apiKey = getCloudApiKey();
842
+ if (!apiKey) {
843
+ throw new CLIError(
844
+ ErrorCodes.AUTH_REQUIRED,
845
+ "API key required for scanning",
846
+ {
847
+ details: {
848
+ remediation: `Set SECURITYCHECKS_API_KEY environment variable.
849
+ Get your API key at https://securitychecks.ai/dashboard/settings/api-keys
850
+
851
+ For air-gapped environments, contact sales@securitychecks.ai for enterprise options.`
852
+ }
853
+ }
854
+ );
855
+ }
856
+ const targetPath = resolveTargetPath(options.targetPath);
857
+ const collectorArtifact = await collect({
858
+ targetPath,
859
+ profile: "securitychecks"
860
+ });
861
+ const cloudResult = await evaluateCloud(collectorArtifact, {
862
+ apiKey,
863
+ baseUrl: getCloudApiBaseUrl(),
864
+ invariants: options.only,
865
+ skip: options.skip,
866
+ projectSlug: getProjectSlug()
867
+ });
868
+ const artifact = toArtifact(collectorArtifact);
869
+ const findings = cloudResult.findings;
870
+ const byPriority = {
871
+ P0: findings.filter((f) => f.severity === "P0").length,
872
+ P1: findings.filter((f) => f.severity === "P1").length,
873
+ P2: findings.filter((f) => f.severity === "P2").length
874
+ };
875
+ const invariantIds = [...new Set(findings.map((f) => f.invariantId))];
876
+ const results = invariantIds.map((id) => ({
877
+ invariantId: id,
878
+ passed: !findings.some((f) => f.invariantId === id),
879
+ findings: findings.filter((f) => f.invariantId === id),
880
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
881
+ duration: 0
882
+ }));
883
+ const passed = results.filter((r) => r.passed).length;
884
+ const failed = results.filter((r) => !r.passed).length;
885
+ const waived = findings.filter((f) => f.waived).length;
886
+ return {
887
+ version: "1.0",
888
+ targetPath,
889
+ runAt: (/* @__PURE__ */ new Date()).toISOString(),
890
+ duration: Date.now() - startTime,
891
+ summary: {
892
+ total: cloudResult.stats.invariantsRun,
893
+ passed,
894
+ failed,
895
+ waived,
896
+ byPriority
897
+ },
898
+ results,
899
+ artifact
900
+ };
901
+ }
902
+ var SUPPORTED_SCHEMA_RANGE = {
903
+ // Minimum version we can consume
904
+ minMajor: 1,
905
+ minMinor: 0,
906
+ // Maximum major version we understand (breaking changes)
907
+ maxMajor: 1
908
+ };
909
+ function parseSemver(version) {
910
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
911
+ if (!match || !match[1] || !match[2] || !match[3]) return null;
912
+ return {
913
+ major: parseInt(match[1], 10),
914
+ minor: parseInt(match[2], 10),
915
+ patch: parseInt(match[3], 10)
916
+ };
917
+ }
918
+ function validateSchemaVersion(artifactSchemaVersion) {
919
+ const currentVersion = ARTIFACT_SCHEMA_VERSION;
920
+ const effectiveVersion = artifactSchemaVersion ?? "1.0.0";
921
+ const parsed = parseSemver(effectiveVersion);
922
+ if (!parsed) {
923
+ return {
924
+ valid: false,
925
+ artifactVersion: effectiveVersion,
926
+ currentVersion,
927
+ error: `Invalid schema version format: "${effectiveVersion}" (expected semver like "1.0.0")`,
928
+ remediation: "Re-collect artifacts with: npx scc collect -o .securitychecks/artifacts.json"
929
+ };
930
+ }
931
+ const { major, minor } = parsed;
932
+ if (major > SUPPORTED_SCHEMA_RANGE.maxMajor) {
933
+ return {
934
+ valid: false,
935
+ artifactVersion: effectiveVersion,
936
+ currentVersion,
937
+ error: `Artifact schema version ${effectiveVersion} is too new (CLI supports up to ${SUPPORTED_SCHEMA_RANGE.maxMajor}.x.x)`,
938
+ remediation: `Upgrade scheck: npm install -g @securitychecks/cli@latest`
939
+ };
940
+ }
941
+ if (major < SUPPORTED_SCHEMA_RANGE.minMajor) {
942
+ return {
943
+ valid: false,
944
+ artifactVersion: effectiveVersion,
945
+ currentVersion,
946
+ error: `Artifact schema version ${effectiveVersion} is too old (CLI requires ${SUPPORTED_SCHEMA_RANGE.minMajor}.x.x+)`,
947
+ remediation: "Re-collect artifacts: npx scc collect -o .securitychecks/artifacts.json"
948
+ };
949
+ }
950
+ if (minor < SUPPORTED_SCHEMA_RANGE.minMinor) {
951
+ return {
952
+ valid: false,
953
+ artifactVersion: effectiveVersion,
954
+ currentVersion,
955
+ error: `Artifact schema version ${effectiveVersion} is missing required fields (CLI requires ${SUPPORTED_SCHEMA_RANGE.minMajor}.${SUPPORTED_SCHEMA_RANGE.minMinor}.x+)`,
956
+ remediation: "Re-collect artifacts: npx scc collect -o .securitychecks/artifacts.json"
957
+ };
958
+ }
959
+ return {
960
+ valid: true,
961
+ artifactVersion: effectiveVersion,
962
+ currentVersion
963
+ };
964
+ }
965
+ function getCurrentSchemaVersion() {
966
+ return ARTIFACT_SCHEMA_VERSION;
967
+ }
968
+ var ANCHOR_EXTRACTORS = {
969
+ // Webhook: include provider from context if available
970
+ "WEBHOOK.IDEMPOTENT": (finding) => {
971
+ const context = finding.evidence[0]?.context ?? "";
972
+ const providerMatch = context.match(/^(stripe|github|slack|svix|generic):/i);
973
+ return {
974
+ provider: providerMatch?.[1]?.toLowerCase() ?? ""
975
+ };
976
+ },
977
+ // Transaction: include side effect type from message
978
+ "TRANSACTION.POST_COMMIT.SIDE_EFFECTS": (finding) => {
979
+ const typeMatch = finding.message.match(/contains (\w+) side effect/i);
980
+ return {
981
+ sideEffectType: typeMatch?.[1]?.toLowerCase() ?? ""
982
+ };
983
+ },
984
+ // Membership revocation: include mutation type
985
+ "AUTHZ.MEMBERSHIP.REVOCATION.IMMEDIATE": (finding) => {
986
+ const context = finding.evidence[0]?.context ?? "";
987
+ const mutationMatch = context.match(/mutationType[:\s]+(\w+)/i);
988
+ return {
989
+ mutationType: mutationMatch?.[1]?.toLowerCase() ?? ""
990
+ };
991
+ },
992
+ // Keys revocation: include entity type
993
+ "AUTHZ.KEYS.REVOCATION.IMMEDIATE": (finding) => {
994
+ const context = finding.evidence[0]?.context ?? "";
995
+ const entityMatch = context.match(/entity[:\s]+(\w+)/i);
996
+ return {
997
+ entity: entityMatch?.[1]?.toLowerCase() ?? ""
998
+ };
999
+ }
1000
+ };
1001
+ function extractIdentityPayload(finding) {
1002
+ const primary = finding.evidence[0];
1003
+ const base = {
1004
+ invariantId: finding.invariantId.toLowerCase(),
1005
+ file: normalizePath(primary?.file ?? ""),
1006
+ symbol: (primary?.symbol ?? "").toLowerCase()
1007
+ };
1008
+ const extractor = ANCHOR_EXTRACTORS[finding.invariantId];
1009
+ if (extractor) {
1010
+ const anchors = extractor(finding);
1011
+ Object.assign(base, anchors);
1012
+ }
1013
+ return base;
1014
+ }
1015
+ function normalizePath(path) {
1016
+ return path.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\//, "").toLowerCase();
1017
+ }
1018
+ function hashPayload(payload) {
1019
+ const keys = Object.keys(payload).sort();
1020
+ const canonical = keys.map((k) => `${k}:${payload[k]}`).join("|");
1021
+ const hash = createHash("sha256").update(canonical).digest("hex");
1022
+ return hash.slice(0, 12);
1023
+ }
1024
+ function generateFindingId(finding) {
1025
+ const payload = extractIdentityPayload(finding);
1026
+ const hash = hashPayload(payload);
1027
+ return `${finding.invariantId}:${hash}`;
1028
+ }
1029
+ function attachFindingId(finding) {
1030
+ const findingId2 = generateFindingId(finding);
1031
+ return Object.assign(finding, { findingId: findingId2 });
1032
+ }
1033
+ function attachFindingIds(findings) {
1034
+ return findings.map(attachFindingId);
1035
+ }
1036
+
1037
+ // src/baseline/schema.ts
1038
+ var BASELINE_SCHEMA_VERSION = "1.0.0";
1039
+ var WAIVER_SCHEMA_VERSION = "1.1.0";
1040
+ var WAIVER_REASON_KEYS = [
1041
+ "false_positive",
1042
+ "acceptable_risk",
1043
+ "will_fix_later",
1044
+ "not_applicable",
1045
+ "other"
1046
+ ];
1047
+ function isValidWaiverReasonKey(value) {
1048
+ return WAIVER_REASON_KEYS.includes(value);
1049
+ }
1050
+ var CLI_PACKAGE_NAME = "@securitychecks/cli";
1051
+ function getGeneratedBy(version) {
1052
+ return `${CLI_PACKAGE_NAME}@${version}`;
1053
+ }
1054
+ function createEmptyBaseline(version = "0.0.0") {
1055
+ return {
1056
+ schemaVersion: BASELINE_SCHEMA_VERSION,
1057
+ toolVersion: version,
1058
+ generatedBy: getGeneratedBy(version),
1059
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1060
+ entries: {}
1061
+ };
1062
+ }
1063
+ function createEmptyWaiverFile(version = "0.0.0") {
1064
+ return {
1065
+ schemaVersion: WAIVER_SCHEMA_VERSION,
1066
+ toolVersion: version,
1067
+ generatedBy: getGeneratedBy(version),
1068
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1069
+ entries: {}
1070
+ };
1071
+ }
1072
+ var CLI_VERSION = "0.1.1-rc.1";
1073
+ var SCHECK_DIR = ".scheck";
1074
+ var BASELINE_FILE = "baseline.json";
1075
+ var WAIVER_FILE = "waivers.json";
1076
+ function getBaselinePath(rootPath) {
1077
+ return join(rootPath, SCHECK_DIR, BASELINE_FILE);
1078
+ }
1079
+ function getWaiverPath(rootPath) {
1080
+ return join(rootPath, SCHECK_DIR, WAIVER_FILE);
1081
+ }
1082
+ async function loadBaseline(rootPath) {
1083
+ const path = getBaselinePath(rootPath);
1084
+ if (!existsSync(path)) {
1085
+ return createEmptyBaseline(CLI_VERSION);
1086
+ }
1087
+ try {
1088
+ const content = await readFile(path, "utf-8");
1089
+ const data = JSON.parse(content);
1090
+ if (!data.schemaVersion) {
1091
+ data.schemaVersion = BASELINE_SCHEMA_VERSION;
1092
+ }
1093
+ if (!data.toolVersion) {
1094
+ data.toolVersion = CLI_VERSION;
1095
+ }
1096
+ if (!data.generatedBy) {
1097
+ data.generatedBy = getGeneratedBy(CLI_VERSION);
1098
+ }
1099
+ return data;
1100
+ } catch {
1101
+ console.warn(`Warning: Could not parse baseline file at ${path}, using empty baseline`);
1102
+ return createEmptyBaseline(CLI_VERSION);
1103
+ }
1104
+ }
1105
+ async function saveBaseline(rootPath, baseline, collectorSchemaVersion) {
1106
+ const path = getBaselinePath(rootPath);
1107
+ await mkdir(dirname(path), { recursive: true });
1108
+ baseline.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1109
+ baseline.toolVersion = CLI_VERSION;
1110
+ baseline.generatedBy = getGeneratedBy(CLI_VERSION);
1111
+ if (collectorSchemaVersion) {
1112
+ baseline.collectorSchemaVersion = collectorSchemaVersion;
1113
+ }
1114
+ const orderedBaseline = {
1115
+ schemaVersion: baseline.schemaVersion,
1116
+ toolVersion: baseline.toolVersion,
1117
+ ...baseline.collectorSchemaVersion ? { collectorSchemaVersion: baseline.collectorSchemaVersion } : {},
1118
+ generatedBy: baseline.generatedBy,
1119
+ updatedAt: baseline.updatedAt,
1120
+ entries: sortEntriesByFindingId(baseline.entries)
1121
+ };
1122
+ await writeFile(path, JSON.stringify(orderedBaseline, null, 2) + "\n", "utf-8");
1123
+ }
1124
+ function sortEntriesByFindingId(entries) {
1125
+ const sorted = {};
1126
+ const keys = Object.keys(entries).sort();
1127
+ for (const key of keys) {
1128
+ sorted[key] = entries[key];
1129
+ }
1130
+ return sorted;
1131
+ }
1132
+ function addToBaseline(baseline, findings, notes) {
1133
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1134
+ let added = 0;
1135
+ for (const finding of findings) {
1136
+ const findingId2 = generateFindingId(finding);
1137
+ if (!baseline.entries[findingId2]) {
1138
+ baseline.entries[findingId2] = {
1139
+ findingId: findingId2,
1140
+ invariantId: finding.invariantId,
1141
+ file: finding.evidence[0]?.file ?? "",
1142
+ symbol: finding.evidence[0]?.symbol,
1143
+ createdAt: now,
1144
+ lastSeenAt: now,
1145
+ notes
1146
+ };
1147
+ added++;
1148
+ } else {
1149
+ baseline.entries[findingId2].lastSeenAt = now;
1150
+ }
1151
+ }
1152
+ return added;
1153
+ }
1154
+ function isInBaseline(baseline, finding) {
1155
+ const findingId2 = generateFindingId(finding);
1156
+ return findingId2 in baseline.entries;
1157
+ }
1158
+ function pruneBaseline(baseline, staleDays = 90) {
1159
+ const cutoff = /* @__PURE__ */ new Date();
1160
+ cutoff.setDate(cutoff.getDate() - staleDays);
1161
+ const cutoffIso = cutoff.toISOString();
1162
+ let removed = 0;
1163
+ for (const [id, entry] of Object.entries(baseline.entries)) {
1164
+ if (entry.lastSeenAt < cutoffIso) {
1165
+ delete baseline.entries[id];
1166
+ removed++;
1167
+ }
1168
+ }
1169
+ return removed;
1170
+ }
1171
+ async function loadWaivers(rootPath) {
1172
+ const path = getWaiverPath(rootPath);
1173
+ if (!existsSync(path)) {
1174
+ return createEmptyWaiverFile(CLI_VERSION);
1175
+ }
1176
+ try {
1177
+ const content = await readFile(path, "utf-8");
1178
+ const data = JSON.parse(content);
1179
+ if (!data.schemaVersion) {
1180
+ data.schemaVersion = WAIVER_SCHEMA_VERSION;
1181
+ }
1182
+ if (!data.toolVersion) {
1183
+ data.toolVersion = CLI_VERSION;
1184
+ }
1185
+ if (!data.generatedBy) {
1186
+ data.generatedBy = getGeneratedBy(CLI_VERSION);
1187
+ }
1188
+ return data;
1189
+ } catch {
1190
+ console.warn(`Warning: Could not parse waiver file at ${path}, using empty waivers`);
1191
+ return createEmptyWaiverFile(CLI_VERSION);
1192
+ }
1193
+ }
1194
+ async function saveWaivers(rootPath, waivers) {
1195
+ const path = getWaiverPath(rootPath);
1196
+ await mkdir(dirname(path), { recursive: true });
1197
+ waivers.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1198
+ waivers.toolVersion = CLI_VERSION;
1199
+ waivers.generatedBy = getGeneratedBy(CLI_VERSION);
1200
+ const orderedWaivers = {
1201
+ schemaVersion: waivers.schemaVersion,
1202
+ toolVersion: waivers.toolVersion,
1203
+ generatedBy: waivers.generatedBy,
1204
+ updatedAt: waivers.updatedAt,
1205
+ entries: sortEntriesByFindingId(waivers.entries)
1206
+ };
1207
+ await writeFile(path, JSON.stringify(orderedWaivers, null, 2) + "\n", "utf-8");
1208
+ }
1209
+ function addWaiver(waivers, finding, options) {
1210
+ const now = /* @__PURE__ */ new Date();
1211
+ const expiresAt = new Date(now);
1212
+ expiresAt.setDate(expiresAt.getDate() + options.expiresInDays);
1213
+ const findingId2 = generateFindingId(finding);
1214
+ const entry = {
1215
+ findingId: findingId2,
1216
+ invariantId: finding.invariantId,
1217
+ file: finding.evidence[0]?.file ?? "",
1218
+ symbol: finding.evidence[0]?.symbol,
1219
+ reasonKey: options.reasonKey,
1220
+ reason: options.reason,
1221
+ owner: options.owner,
1222
+ expiresAt: expiresAt.toISOString(),
1223
+ createdAt: now.toISOString()
1224
+ };
1225
+ waivers.entries[findingId2] = entry;
1226
+ return entry;
1227
+ }
1228
+ function getValidWaiver(waivers, finding) {
1229
+ const findingId2 = generateFindingId(finding);
1230
+ const waiver = waivers.entries[findingId2];
1231
+ if (!waiver) {
1232
+ return void 0;
1233
+ }
1234
+ const now = /* @__PURE__ */ new Date();
1235
+ const expiresAt = new Date(waiver.expiresAt);
1236
+ if (expiresAt < now) {
1237
+ return void 0;
1238
+ }
1239
+ return waiver;
1240
+ }
1241
+ function pruneExpiredWaivers(waivers) {
1242
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1243
+ let removed = 0;
1244
+ for (const [id, entry] of Object.entries(waivers.entries)) {
1245
+ if (entry.expiresAt < now) {
1246
+ delete waivers.entries[id];
1247
+ removed++;
1248
+ }
1249
+ }
1250
+ return removed;
1251
+ }
1252
+ function getExpiringWaivers(waivers, withinDays = 7) {
1253
+ const now = /* @__PURE__ */ new Date();
1254
+ const threshold = new Date(now);
1255
+ threshold.setDate(threshold.getDate() + withinDays);
1256
+ const thresholdIso = threshold.toISOString();
1257
+ return Object.values(waivers.entries).filter(
1258
+ (entry) => entry.expiresAt > now.toISOString() && entry.expiresAt <= thresholdIso
1259
+ );
1260
+ }
1261
+
1262
+ // src/baseline/matcher.ts
1263
+ function categorizeFindings(findings, baseline, waivers, failSeverities = ["P0", "P1"]) {
1264
+ const categorized = [];
1265
+ const newFindings = [];
1266
+ const baselinedFindings = [];
1267
+ const waivedFindings = [];
1268
+ for (const finding of findings) {
1269
+ const findingId2 = generateFindingId(finding);
1270
+ const isBaselined = isInBaseline(baseline, finding);
1271
+ const waiver = getValidWaiver(waivers, finding);
1272
+ const isFailSeverity = failSeverities.includes(finding.severity);
1273
+ const shouldFail = isFailSeverity && !isBaselined && !waiver;
1274
+ const categorizedFinding = {
1275
+ ...finding,
1276
+ findingId: findingId2,
1277
+ isBaselined,
1278
+ waiver,
1279
+ shouldFail
1280
+ };
1281
+ categorized.push(categorizedFinding);
1282
+ if (waiver) {
1283
+ waivedFindings.push(categorizedFinding);
1284
+ } else if (isBaselined) {
1285
+ baselinedFindings.push(categorizedFinding);
1286
+ } else {
1287
+ newFindings.push(categorizedFinding);
1288
+ }
1289
+ }
1290
+ return {
1291
+ all: categorized,
1292
+ new: newFindings,
1293
+ baselined: baselinedFindings,
1294
+ waived: waivedFindings,
1295
+ counts: {
1296
+ total: categorized.length,
1297
+ new: newFindings.length,
1298
+ baselined: baselinedFindings.length,
1299
+ waived: waivedFindings.length,
1300
+ willFail: categorized.filter((f) => f.shouldFail).length
1301
+ }
1302
+ };
1303
+ }
1304
+ function getCIExitCode(result) {
1305
+ return result.counts.willFail > 0 ? 1 : 0;
1306
+ }
1307
+ function getCISummary(result) {
1308
+ const { counts } = result;
1309
+ if (counts.total === 0) {
1310
+ return "No findings detected.";
1311
+ }
1312
+ const parts = [];
1313
+ if (counts.willFail > 0) {
1314
+ parts.push(`${counts.willFail} new finding(s) require attention`);
1315
+ }
1316
+ if (counts.baselined > 0) {
1317
+ parts.push(`${counts.baselined} baselined`);
1318
+ }
1319
+ if (counts.waived > 0) {
1320
+ parts.push(`${counts.waived} waived`);
1321
+ }
1322
+ if (counts.willFail === 0) {
1323
+ parts.unshift("All findings are baselined or waived");
1324
+ }
1325
+ return parts.join(", ") + ".";
1326
+ }
1327
+ function resolveCollisions(findings) {
1328
+ const seen = /* @__PURE__ */ new Map();
1329
+ const result = [];
1330
+ for (const finding of findings) {
1331
+ let findingId2 = generateFindingId(finding);
1332
+ const count = seen.get(findingId2) ?? 0;
1333
+ if (count > 0) {
1334
+ const suffix = String.fromCharCode(96 + count);
1335
+ findingId2 = `${findingId2}:${suffix}`;
1336
+ }
1337
+ seen.set(generateFindingId(finding), count + 1);
1338
+ result.push({
1339
+ ...finding,
1340
+ findingId: findingId2
1341
+ });
1342
+ }
1343
+ return result;
1344
+ }
1345
+ function hasCollisions(findings) {
1346
+ const ids = /* @__PURE__ */ new Set();
1347
+ for (const finding of findings) {
1348
+ const id = generateFindingId(finding);
1349
+ if (ids.has(id)) {
1350
+ return true;
1351
+ }
1352
+ ids.add(id);
1353
+ }
1354
+ return false;
1355
+ }
1356
+
1357
+ // src/lib/correlation.ts
1358
+ var COMPOUNDING_RULES = [
1359
+ // Webhook + Transaction = Replay causes inconsistent state
1360
+ {
1361
+ invariants: ["WEBHOOK.IDEMPOTENT", "TRANSACTION.POST_COMMIT.SIDE_EFFECTS"],
1362
+ effect: {
1363
+ description: "Webhook replay can cause duplicate side effects AND inconsistent database state",
1364
+ riskMultiplier: 2,
1365
+ signals: ["webhook_replay", "transaction_side_effect", "data_inconsistency"]
1366
+ },
1367
+ attackPathTemplate: {
1368
+ title: "Webhook Replay Attack with Data Inconsistency",
1369
+ exploitability: "easy",
1370
+ impact: "high",
1371
+ timeWindow: "Immediate - no time limit on replay"
1372
+ }
1373
+ },
1374
+ // No auth + No service auth = Complete bypass
1375
+ {
1376
+ invariants: ["AUTHZ.SERVICE_LAYER.ENFORCED", "AUTHZ.MEMBERSHIP.REVOCATION.IMMEDIATE"],
1377
+ effect: {
1378
+ description: "Missing service-layer auth combined with delayed revocation allows extended unauthorized access",
1379
+ riskMultiplier: 2.5,
1380
+ signals: ["auth_bypass", "delayed_revocation", "privilege_persistence"]
1381
+ },
1382
+ attackPathTemplate: {
1383
+ title: "Extended Privilege Persistence",
1384
+ exploitability: "medium",
1385
+ impact: "critical",
1386
+ timeWindow: "Until cache expires or session timeout"
1387
+ }
1388
+ },
1389
+ // Cache + Membership revocation = Stale permissions
1390
+ {
1391
+ invariants: ["CACHE.INVALIDATION.ON_AUTH_CHANGE", "AUTHZ.MEMBERSHIP.REVOCATION.IMMEDIATE"],
1392
+ effect: {
1393
+ description: "Membership change without cache invalidation allows continued access via stale cache",
1394
+ riskMultiplier: 2,
1395
+ signals: ["stale_cache", "permission_leak", "revocation_bypass"]
1396
+ },
1397
+ attackPathTemplate: {
1398
+ title: "Stale Permission Cache Exploit",
1399
+ exploitability: "medium",
1400
+ impact: "high",
1401
+ timeWindow: "Cache TTL (often 5-15 minutes)"
1402
+ }
1403
+ },
1404
+ // Transaction + Cache = Inconsistent read after rollback
1405
+ {
1406
+ invariants: ["TRANSACTION.POST_COMMIT.SIDE_EFFECTS", "CACHE.INVALIDATION.ON_AUTH_CHANGE"],
1407
+ effect: {
1408
+ description: "Side effect in transaction + missing cache invalidation can leave cache inconsistent after rollback",
1409
+ riskMultiplier: 1.5,
1410
+ signals: ["rollback_inconsistency", "cache_stale", "side_effect_mismatch"]
1411
+ }
1412
+ },
1413
+ // Billing + Auth = Free tier bypass
1414
+ {
1415
+ invariants: ["BILLING.SERVER_ENFORCED", "AUTHZ.SERVICE_LAYER.ENFORCED"],
1416
+ effect: {
1417
+ description: "Missing billing enforcement + auth gap allows access to paid features without payment",
1418
+ riskMultiplier: 2,
1419
+ signals: ["billing_bypass", "feature_theft", "revenue_loss"]
1420
+ },
1421
+ attackPathTemplate: {
1422
+ title: "Billing Bypass via Auth Gap",
1423
+ exploitability: "medium",
1424
+ impact: "high"
1425
+ }
1426
+ },
1427
+ // Jobs + Transaction = Retry causes duplicate side effects
1428
+ {
1429
+ invariants: ["JOBS.RETRY_SAFE", "TRANSACTION.POST_COMMIT.SIDE_EFFECTS"],
1430
+ effect: {
1431
+ description: "Non-idempotent job with side effects in transaction can cause duplicates on retry",
1432
+ riskMultiplier: 1.8,
1433
+ signals: ["job_retry", "duplicate_side_effect", "data_duplication"]
1434
+ }
1435
+ },
1436
+ // API Key + Cache = Revoked key still works
1437
+ {
1438
+ invariants: ["AUTHZ.KEYS.REVOCATION.IMMEDIATE", "CACHE.INVALIDATION.ON_AUTH_CHANGE"],
1439
+ effect: {
1440
+ description: "API key revocation without cache invalidation allows continued API access",
1441
+ riskMultiplier: 2,
1442
+ signals: ["key_still_valid", "cache_bypass", "api_access_leak"]
1443
+ },
1444
+ attackPathTemplate: {
1445
+ title: "API Key Revocation Bypass",
1446
+ exploitability: "easy",
1447
+ impact: "high",
1448
+ timeWindow: "Until cache expires"
1449
+ }
1450
+ }
1451
+ ];
1452
+ function correlateFindings(results, artifact) {
1453
+ const allFindings = results.flatMap((r) => r.findings);
1454
+ if (allFindings.length === 0) {
1455
+ return {
1456
+ correlations: [],
1457
+ stats: {
1458
+ totalFindings: 0,
1459
+ correlatedFindings: 0,
1460
+ correlationGroups: 0,
1461
+ severityEscalations: 0
1462
+ }
1463
+ };
1464
+ }
1465
+ const groups = groupFindingsByLocation(allFindings);
1466
+ const correlations = [];
1467
+ let severityEscalations = 0;
1468
+ for (const group of groups.values()) {
1469
+ if (group.length < 2) continue;
1470
+ const correlation = findCorrelation(group);
1471
+ if (correlation) {
1472
+ correlations.push(correlation);
1473
+ if (severityToNumber(correlation.adjustedSeverity) > severityToNumber(correlation.primary.severity)) {
1474
+ severityEscalations++;
1475
+ }
1476
+ }
1477
+ }
1478
+ const correlatedFindingIds = /* @__PURE__ */ new Set();
1479
+ for (const c of correlations) {
1480
+ correlatedFindingIds.add(findingId(c.primary));
1481
+ for (const r of c.related) {
1482
+ correlatedFindingIds.add(findingId(r));
1483
+ }
1484
+ }
1485
+ return {
1486
+ correlations,
1487
+ stats: {
1488
+ totalFindings: allFindings.length,
1489
+ correlatedFindings: correlatedFindingIds.size,
1490
+ correlationGroups: correlations.length,
1491
+ severityEscalations
1492
+ }
1493
+ };
1494
+ }
1495
+ function groupFindingsByLocation(findings) {
1496
+ const groups = /* @__PURE__ */ new Map();
1497
+ for (const finding of findings) {
1498
+ const evidence = finding.evidence[0];
1499
+ if (!evidence) continue;
1500
+ const key = `${evidence.file}:${evidence.symbol ?? "unknown"}`;
1501
+ if (!groups.has(key)) {
1502
+ groups.set(key, []);
1503
+ }
1504
+ groups.get(key).push(finding);
1505
+ }
1506
+ return groups;
1507
+ }
1508
+ function findCorrelation(findings, _artifact) {
1509
+ const invariantIds = new Set(findings.map((f) => f.invariantId));
1510
+ let bestMatch = null;
1511
+ let matchCount = 0;
1512
+ for (const rule of COMPOUNDING_RULES) {
1513
+ const matches = rule.invariants.filter((inv) => invariantIds.has(inv));
1514
+ if (matches.length >= 2 && matches.length > matchCount) {
1515
+ bestMatch = rule;
1516
+ matchCount = matches.length;
1517
+ }
1518
+ }
1519
+ if (!bestMatch) {
1520
+ if (findings.length >= 2) {
1521
+ return createGenericCorrelation(findings);
1522
+ }
1523
+ return null;
1524
+ }
1525
+ const matchingFindings = findings.filter(
1526
+ (f) => bestMatch.invariants.includes(f.invariantId)
1527
+ );
1528
+ matchingFindings.sort(
1529
+ (a, b) => severityToNumber(b.severity) - severityToNumber(a.severity)
1530
+ );
1531
+ const primary = matchingFindings[0];
1532
+ const related = matchingFindings.slice(1);
1533
+ const evidence = primary.evidence[0];
1534
+ const sharedContext = {
1535
+ file: evidence?.file,
1536
+ functionName: evidence?.symbol,
1537
+ findingCount: matchingFindings.length
1538
+ };
1539
+ const adjustedSeverity = calculateAdjustedSeverity(
1540
+ primary.severity,
1541
+ bestMatch.effect.riskMultiplier
1542
+ );
1543
+ let attackPath;
1544
+ if (bestMatch.attackPathTemplate) {
1545
+ attackPath = buildAttackPath(
1546
+ bestMatch.attackPathTemplate,
1547
+ matchingFindings
1548
+ );
1549
+ }
1550
+ return {
1551
+ primary,
1552
+ related,
1553
+ sharedContext,
1554
+ compoundingEffect: bestMatch.effect,
1555
+ adjustedSeverity,
1556
+ attackPath
1557
+ };
1558
+ }
1559
+ function createGenericCorrelation(findings) {
1560
+ findings.sort(
1561
+ (a, b) => severityToNumber(b.severity) - severityToNumber(a.severity)
1562
+ );
1563
+ const primary = findings[0];
1564
+ const related = findings.slice(1);
1565
+ const evidence = primary.evidence[0];
1566
+ return {
1567
+ primary,
1568
+ related,
1569
+ sharedContext: {
1570
+ file: evidence?.file,
1571
+ functionName: evidence?.symbol,
1572
+ findingCount: findings.length
1573
+ },
1574
+ compoundingEffect: {
1575
+ description: `Multiple security issues in the same location (${findings.length} findings)`,
1576
+ riskMultiplier: 1 + (findings.length - 1) * 0.2,
1577
+ signals: findings.map((f) => f.invariantId)
1578
+ },
1579
+ adjustedSeverity: primary.severity
1580
+ };
1581
+ }
1582
+ function buildAttackPath(template, findings) {
1583
+ const steps = [];
1584
+ for (let i = 0; i < findings.length; i++) {
1585
+ const finding = findings[i];
1586
+ const evidence = finding.evidence[0];
1587
+ steps.push({
1588
+ step: i + 1,
1589
+ description: getAttackStepDescription(finding),
1590
+ invariantId: finding.invariantId,
1591
+ location: evidence ? { file: evidence.file, line: evidence.line } : void 0
1592
+ });
1593
+ }
1594
+ return {
1595
+ ...template,
1596
+ steps
1597
+ };
1598
+ }
1599
+ function getAttackStepDescription(finding) {
1600
+ const invariant = finding.invariantId;
1601
+ switch (invariant) {
1602
+ case "WEBHOOK.IDEMPOTENT":
1603
+ return "Attacker replays webhook request (no idempotency protection)";
1604
+ case "TRANSACTION.POST_COMMIT.SIDE_EFFECTS":
1605
+ return "Side effect fires inside transaction (may be duplicated or inconsistent)";
1606
+ case "AUTHZ.SERVICE_LAYER.ENFORCED":
1607
+ return "Service function called without authorization check";
1608
+ case "AUTHZ.MEMBERSHIP.REVOCATION.IMMEDIATE":
1609
+ return "Membership/role change does not immediately revoke access";
1610
+ case "AUTHZ.KEYS.REVOCATION.IMMEDIATE":
1611
+ return "API key revocation does not immediately invalidate the key";
1612
+ case "CACHE.INVALIDATION.ON_AUTH_CHANGE":
1613
+ return "Auth change does not invalidate cached permissions";
1614
+ case "BILLING.SERVER_ENFORCED":
1615
+ return "Billing/entitlement check bypassed or missing";
1616
+ case "JOBS.RETRY_SAFE":
1617
+ return "Background job is not idempotent (retry causes duplicates)";
1618
+ default:
1619
+ return finding.message;
1620
+ }
1621
+ }
1622
+ function severityToNumber(severity) {
1623
+ switch (severity) {
1624
+ case "P0":
1625
+ return 3;
1626
+ case "P1":
1627
+ return 2;
1628
+ case "P2":
1629
+ return 1;
1630
+ default:
1631
+ return 0;
1632
+ }
1633
+ }
1634
+ function numberToSeverity(num) {
1635
+ if (num >= 3) return "P0";
1636
+ if (num >= 2) return "P1";
1637
+ return "P2";
1638
+ }
1639
+ function calculateAdjustedSeverity(baseSeverity, multiplier) {
1640
+ const base = severityToNumber(baseSeverity);
1641
+ const adjusted = Math.min(3, Math.ceil(base * multiplier));
1642
+ return numberToSeverity(adjusted);
1643
+ }
1644
+ function findingId(finding) {
1645
+ const evidence = finding.evidence[0];
1646
+ return `${finding.invariantId}:${evidence?.file ?? "unknown"}:${evidence?.line ?? 0}`;
1647
+ }
1648
+ function formatCorrelatedFinding(correlation) {
1649
+ const lines = [];
1650
+ const location = correlation.sharedContext.file ? `${correlation.sharedContext.file}:${correlation.sharedContext.functionName ?? "unknown"}` : "Unknown location";
1651
+ lines.push(`
1652
+ \u250C\u2500 CORRELATED FINDINGS \u2500 ${location}`);
1653
+ lines.push(`\u2502`);
1654
+ lines.push(`\u2502 [${correlation.adjustedSeverity}] ${correlation.primary.message}`);
1655
+ lines.push(`\u2502 \u2514\u2500 ${correlation.primary.invariantId}`);
1656
+ for (const related of correlation.related) {
1657
+ lines.push(`\u2502 [${related.severity}] ${related.message}`);
1658
+ lines.push(`\u2502 \u2514\u2500 ${related.invariantId}`);
1659
+ }
1660
+ lines.push(`\u2502`);
1661
+ lines.push(`\u2502 \u26A0 Compounding Effect:`);
1662
+ lines.push(`\u2502 ${correlation.compoundingEffect.description}`);
1663
+ lines.push(`\u2502 Risk multiplier: ${correlation.compoundingEffect.riskMultiplier}x`);
1664
+ if (correlation.attackPath) {
1665
+ lines.push(`\u2502`);
1666
+ lines.push(`\u2502 \u{1F3AF} Attack Path: ${correlation.attackPath.title}`);
1667
+ lines.push(`\u2502 Exploitability: ${correlation.attackPath.exploitability}`);
1668
+ lines.push(`\u2502 Impact: ${correlation.attackPath.impact}`);
1669
+ if (correlation.attackPath.timeWindow) {
1670
+ lines.push(`\u2502 Time window: ${correlation.attackPath.timeWindow}`);
1671
+ }
1672
+ lines.push(`\u2502`);
1673
+ for (const step of correlation.attackPath.steps) {
1674
+ lines.push(`\u2502 ${step.step}. ${step.description}`);
1675
+ }
1676
+ }
1677
+ lines.push(`\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1678
+ return lines.join("\n");
1679
+ }
1680
+ function formatCorrelationStats(result) {
1681
+ const { stats } = result;
1682
+ return `
1683
+ Correlation Analysis:
1684
+ Total findings: ${stats.totalFindings}
1685
+ Correlated: ${stats.correlatedFindings} (${Math.round(stats.correlatedFindings / stats.totalFindings * 100)}%)
1686
+ Correlation groups: ${stats.correlationGroups}
1687
+ Severity escalations: ${stats.severityEscalations}
1688
+ `.trim();
1689
+ }
1690
+ var DEFAULT_ENDPOINT = "https://api.securitychecks.ai/v1/correlations";
1691
+ function toObservation(correlation, framework) {
1692
+ const allFindings = [correlation.primary, ...correlation.related];
1693
+ const invariants = [...new Set(allFindings.map((f) => f.invariantId))];
1694
+ return {
1695
+ invariants,
1696
+ context: {
1697
+ framework,
1698
+ file: correlation.sharedContext.file,
1699
+ functionName: correlation.sharedContext.functionName,
1700
+ route: correlation.sharedContext.route
1701
+ },
1702
+ stats: {
1703
+ findingCount: correlation.sharedContext.findingCount,
1704
+ severityBefore: correlation.primary.severity,
1705
+ severityAfter: correlation.adjustedSeverity,
1706
+ wasEscalated: correlation.adjustedSeverity !== correlation.primary.severity,
1707
+ riskMultiplier: correlation.compoundingEffect.riskMultiplier
1708
+ },
1709
+ attackPath: correlation.attackPath ? {
1710
+ title: correlation.attackPath.title,
1711
+ exploitability: correlation.attackPath.exploitability,
1712
+ impact: correlation.attackPath.impact,
1713
+ timeWindow: correlation.attackPath.timeWindow,
1714
+ steps: correlation.attackPath.steps
1715
+ } : void 0,
1716
+ compoundingEffect: {
1717
+ description: correlation.compoundingEffect.description,
1718
+ signals: correlation.compoundingEffect.signals
1719
+ },
1720
+ meta: {
1721
+ clientVersion: "0.1.1-rc.1",
1722
+ requestId: randomUUID(),
1723
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1724
+ }
1725
+ };
1726
+ }
1727
+ async function reportCorrelations(result, config, framework) {
1728
+ if (!config.enabled || result.correlations.length === 0) {
1729
+ return { success: true, stored: 0 };
1730
+ }
1731
+ const endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
1732
+ const timeout = config.timeout ?? 5e3;
1733
+ try {
1734
+ const observations = result.correlations.map((c) => toObservation(c, framework));
1735
+ const payload = {
1736
+ correlations: observations,
1737
+ summary: result.stats,
1738
+ meta: {
1739
+ clientVersion: "0.1.1-rc.1",
1740
+ framework
1741
+ }
1742
+ };
1743
+ const controller = new AbortController();
1744
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1745
+ try {
1746
+ const response = await fetch(endpoint, {
1747
+ method: "POST",
1748
+ headers: {
1749
+ "Content-Type": "application/json",
1750
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` },
1751
+ "X-Client-Version": "0.1.1-rc.1"
1752
+ },
1753
+ body: JSON.stringify(payload),
1754
+ signal: controller.signal
1755
+ });
1756
+ clearTimeout(timeoutId);
1757
+ if (!response.ok) {
1758
+ return { success: false };
1759
+ }
1760
+ const data = await response.json();
1761
+ return {
1762
+ success: true,
1763
+ stored: data.stored ?? observations.length,
1764
+ errors: data.errors ?? 0
1765
+ };
1766
+ } finally {
1767
+ clearTimeout(timeoutId);
1768
+ }
1769
+ } catch (_error) {
1770
+ return { success: false };
1771
+ }
1772
+ }
1773
+ async function reportCorrelationFeedback(requestId, wasAccurate, reason, config) {
1774
+ const endpoint = config?.endpoint ?? DEFAULT_ENDPOINT;
1775
+ try {
1776
+ const response = await fetch(endpoint, {
1777
+ method: "PATCH",
1778
+ headers: {
1779
+ "Content-Type": "application/json",
1780
+ ...config?.apiKey && { Authorization: `Bearer ${config.apiKey}` }
1781
+ },
1782
+ body: JSON.stringify({
1783
+ requestId,
1784
+ wasAccurate,
1785
+ feedbackReason: reason
1786
+ })
1787
+ });
1788
+ return response.ok;
1789
+ } catch {
1790
+ return false;
1791
+ }
1792
+ }
1793
+ var DEFAULT_ENDPOINT2 = "https://api.securitychecks.ai/v1/telemetry";
1794
+ function buildTelemetry(result, options) {
1795
+ const byInvariant = {};
1796
+ for (const checkResult of result.results) {
1797
+ byInvariant[checkResult.invariantId] = checkResult.findings.length;
1798
+ }
1799
+ const ciProvider = detectCIProvider();
1800
+ return {
1801
+ scanId: randomUUID(),
1802
+ codebase: {
1803
+ filesScanned: options.filesScanned,
1804
+ servicesCount: result.artifact.services.length
1805
+ },
1806
+ frameworks: options.frameworks,
1807
+ findings: {
1808
+ byInvariant,
1809
+ byPriority: result.summary.byPriority,
1810
+ total: result.summary.byPriority.P0 + result.summary.byPriority.P1 + result.summary.byPriority.P2
1811
+ },
1812
+ correlation: options.correlation ? {
1813
+ groups: options.correlation.stats.correlationGroups,
1814
+ escalations: options.correlation.stats.severityEscalations,
1815
+ correlatedFindings: options.correlation.stats.correlatedFindings
1816
+ } : void 0,
1817
+ calibration: options.calibratedCount !== void 0 ? {
1818
+ calibrated: options.calibratedCount,
1819
+ suppressed: options.suppressedCount ?? 0
1820
+ } : void 0,
1821
+ patterns: options.patternsApplied !== void 0 ? {
1822
+ applied: options.patternsApplied,
1823
+ findings: options.patternFindings ?? 0
1824
+ } : void 0,
1825
+ meta: {
1826
+ duration: result.duration,
1827
+ clientVersion: "0.1.1-rc.1",
1828
+ mode: options.mode ?? (ciProvider ? "ci" : "manual"),
1829
+ ciProvider
1830
+ },
1831
+ baseline: options.categorization ? {
1832
+ size: options.baselineSize ?? 0,
1833
+ waivers: options.waiversCount ?? 0,
1834
+ newFindings: options.categorization.counts.new
1835
+ } : void 0,
1836
+ feedback: options.categorization ? {
1837
+ waivedCount: options.categorization.counts.waived,
1838
+ waiverReasons: buildWaiverReasonCounts(options.categorization),
1839
+ baselinedCount: options.categorization.counts.baselined
1840
+ } : void 0
1841
+ };
1842
+ }
1843
+ async function reportTelemetry(telemetry, config) {
1844
+ if (!config.enabled) {
1845
+ return true;
1846
+ }
1847
+ const endpoint = config.endpoint ?? DEFAULT_ENDPOINT2;
1848
+ const timeout = config.timeout ?? 5e3;
1849
+ try {
1850
+ const controller = new AbortController();
1851
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1852
+ try {
1853
+ const response = await fetch(endpoint, {
1854
+ method: "POST",
1855
+ headers: {
1856
+ "Content-Type": "application/json",
1857
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` },
1858
+ "X-Client-Version": telemetry.meta.clientVersion
1859
+ },
1860
+ body: JSON.stringify(telemetry),
1861
+ signal: controller.signal
1862
+ });
1863
+ clearTimeout(timeoutId);
1864
+ return response.ok;
1865
+ } finally {
1866
+ clearTimeout(timeoutId);
1867
+ }
1868
+ } catch {
1869
+ return false;
1870
+ }
1871
+ }
1872
+ function buildWaiverReasonCounts(categorization) {
1873
+ const counts = {};
1874
+ for (const finding of categorization.waived) {
1875
+ const waiver = finding.waiver;
1876
+ if (!waiver) continue;
1877
+ const candidate = waiver.reasonKey ?? waiver.reason;
1878
+ const key = candidate && isValidWaiverReasonKey(candidate) ? candidate : "other";
1879
+ counts[key] = (counts[key] ?? 0) + 1;
1880
+ }
1881
+ return counts;
1882
+ }
1883
+ function detectCIProvider() {
1884
+ if (process.env["GITHUB_ACTIONS"]) return "github";
1885
+ if (process.env["GITLAB_CI"]) return "gitlab";
1886
+ if (process.env["JENKINS_URL"]) return "jenkins";
1887
+ if (process.env["CIRCLECI"]) return "circleci";
1888
+ if (process.env["TRAVIS"]) return "travis";
1889
+ if (process.env["BITBUCKET_BUILD_NUMBER"]) return "bitbucket";
1890
+ if (process.env["AZURE_PIPELINES"]) return "azure";
1891
+ if (process.env["CI"]) return "unknown";
1892
+ return void 0;
1893
+ }
1894
+ function isTelemetryDisabled() {
1895
+ return process.env["SECURITYCHECKS_TELEMETRY"] === "false" || process.env["DO_NOT_TRACK"] === "1";
1896
+ }
1897
+
1898
+ // src/lib/calibration.ts
1899
+ var AGGREGATE_CALIBRATION_ENDPOINT = "https://api.securitychecks.ai/v1/calibration";
1900
+ var AGGREGATE_CACHE_TTL_MS = 60 * 60 * 1e3;
1901
+ var aggregateCache = null;
1902
+ var aggregateCacheTimestamp = 0;
1903
+ async function fetchAggregateCalibration(frameworks, config) {
1904
+ if (!config.enabled) {
1905
+ return { data: null, fromCache: false, error: "Aggregate calibration disabled" };
1906
+ }
1907
+ if (config.cacheEnabled !== false && aggregateCache && Date.now() - aggregateCacheTimestamp < AGGREGATE_CACHE_TTL_MS) {
1908
+ return { data: aggregateCache, fromCache: true };
1909
+ }
1910
+ const endpoint = config.endpoint ?? AGGREGATE_CALIBRATION_ENDPOINT;
1911
+ const timeout = config.timeout ?? 5e3;
1912
+ try {
1913
+ const controller = new AbortController();
1914
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1915
+ try {
1916
+ const url = new URL(endpoint);
1917
+ url.searchParams.set("frameworks", frameworks.join(","));
1918
+ const response = await fetch(url.toString(), {
1919
+ method: "GET",
1920
+ headers: {
1921
+ "Accept": "application/json",
1922
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
1923
+ },
1924
+ signal: controller.signal
1925
+ });
1926
+ clearTimeout(timeoutId);
1927
+ if (!response.ok) {
1928
+ return {
1929
+ data: null,
1930
+ fromCache: false,
1931
+ error: `HTTP ${response.status}: ${response.statusText}`
1932
+ };
1933
+ }
1934
+ const data = await response.json();
1935
+ if (config.cacheEnabled !== false) {
1936
+ aggregateCache = data;
1937
+ aggregateCacheTimestamp = Date.now();
1938
+ }
1939
+ return { data, fromCache: false };
1940
+ } finally {
1941
+ clearTimeout(timeoutId);
1942
+ }
1943
+ } catch (err) {
1944
+ return {
1945
+ data: null,
1946
+ fromCache: false,
1947
+ error: err instanceof Error ? err.message : "Unknown error"
1948
+ };
1949
+ }
1950
+ }
1951
+ function clearAggregateCache() {
1952
+ aggregateCache = null;
1953
+ aggregateCacheTimestamp = 0;
1954
+ }
1955
+ function getFrameworkBaseline(calibration, framework) {
1956
+ return calibration.frameworks.find((f) => f.framework === framework);
1957
+ }
1958
+ function shouldSkipPattern(calibration, patternId, framework, accuracyThreshold = 0.3) {
1959
+ const pattern = calibration.patterns.find(
1960
+ (p) => p.patternId === patternId && (p.framework === framework || p.framework === null)
1961
+ );
1962
+ if (!pattern || pattern.accuracy === null) {
1963
+ return false;
1964
+ }
1965
+ if (pattern.confidence !== "high") {
1966
+ return false;
1967
+ }
1968
+ return pattern.accuracy < accuracyThreshold;
1969
+ }
1970
+ function getSkippedPatterns(calibration, framework, accuracyThreshold = 0.3) {
1971
+ return calibration.patterns.filter((p) => {
1972
+ if (p.accuracy === null || p.confidence !== "high") {
1973
+ return false;
1974
+ }
1975
+ if (framework && p.framework && p.framework !== framework) {
1976
+ return false;
1977
+ }
1978
+ return p.accuracy < accuracyThreshold;
1979
+ }).map((p) => p.patternId);
1980
+ }
1981
+ function getVerifiedCorrelations(calibration) {
1982
+ return calibration.correlations.filter((c) => c.isVerified);
1983
+ }
1984
+ function calculateRelativeSeverity(findingCount, baseline, type = "total") {
1985
+ let avg;
1986
+ switch (type) {
1987
+ case "P0":
1988
+ avg = baseline.avgP0;
1989
+ break;
1990
+ case "P1":
1991
+ avg = baseline.avgP1;
1992
+ break;
1993
+ case "P2":
1994
+ avg = baseline.avgP2;
1995
+ break;
1996
+ default:
1997
+ avg = baseline.avgFindings;
1998
+ }
1999
+ if (avg === 0) {
2000
+ return findingCount > 0 ? "above_average" : "average";
2001
+ }
2002
+ const ratio = findingCount / avg;
2003
+ if (ratio < 0.5) return "below_average";
2004
+ if (ratio < 1.5) return "average";
2005
+ if (ratio < 3) return "above_average";
2006
+ return "critical";
2007
+ }
2008
+ function formatAggregateCalibrationSummary(calibration, frameworks, findings) {
2009
+ const lines = [];
2010
+ for (const fw of frameworks) {
2011
+ const baseline = getFrameworkBaseline(calibration, fw);
2012
+ if (baseline && baseline.confidence !== "low") {
2013
+ const severity = calculateRelativeSeverity(findings.total, baseline);
2014
+ const avgStr = baseline.avgFindings.toFixed(1);
2015
+ if (severity === "below_average") {
2016
+ lines.push(`${fw}: ${findings.total} findings (${avgStr} avg) - Below average`);
2017
+ } else if (severity === "above_average") {
2018
+ lines.push(`${fw}: ${findings.total} findings (${avgStr} avg) - Above average`);
2019
+ } else if (severity === "critical") {
2020
+ lines.push(`${fw}: ${findings.total} findings (${avgStr} avg) - Significantly above average`);
2021
+ } else {
2022
+ lines.push(`${fw}: ${findings.total} findings (${avgStr} avg) - Typical`);
2023
+ }
2024
+ }
2025
+ }
2026
+ const skipped = getSkippedPatterns(calibration, frameworks[0]);
2027
+ if (skipped.length > 0) {
2028
+ lines.push(`Skipped ${skipped.length} low-accuracy patterns`);
2029
+ }
2030
+ if (calibration.meta.totalScansAnalyzed < 100) {
2031
+ lines.push(`Calibration based on ${calibration.meta.totalScansAnalyzed} scans (limited data)`);
2032
+ }
2033
+ return lines.join("\n");
2034
+ }
2035
+ function isAggregateCalibrationDisabled() {
2036
+ return process.env["SECURITYCHECKS_CALIBRATION"] === "false";
2037
+ }
2038
+
2039
+ // src/lib/staff.ts
2040
+ function getStaffQuestion(invariantId) {
2041
+ const questions = {
2042
+ "AUTHZ.SERVICE_LAYER.ENFORCED": "What happens when a background job calls this function directly, bypassing the route?",
2043
+ "AUTHZ.MEMBERSHIP.REVOCATION.IMMEDIATE": "If I remove someone from a team right now, can they still access team resources?",
2044
+ "AUTHZ.KEYS.REVOCATION.IMMEDIATE": "If I revoke this API key, does it stop working immediately or is it cached?",
2045
+ "WEBHOOK.IDEMPOTENT": "What happens when Stripe retries this webhook? Will we double-charge the customer?",
2046
+ "WEBHOOK.SIGNATURE.VERIFIED": "Are we verifying webhook signatures before processing any side effects?",
2047
+ "TRANSACTION.POST_COMMIT.SIDE_EFFECTS": "If this transaction rolls back, did we already send an email the user will never receive?",
2048
+ "TESTS.NO_FALSE_CONFIDENCE": "Is this test actually verifying behavior, or just making CI green?",
2049
+ "CACHE.INVALIDATION.ON_AUTH_CHANGE": "When someone loses access, how long until the cache catches up?",
2050
+ "JOBS.RETRY_SAFE": "If this job runs twice, will we have duplicate data or double-bill someone?",
2051
+ "BILLING.SERVER_ENFORCED": "Can someone bypass the paywall by calling the API directly?",
2052
+ "ANALYTICS.SCHEMA.STABLE": "If someone adds a field here, will it break our dashboards?",
2053
+ "DATAFLOW.UNTRUSTED.SQL_QUERY": "Can untrusted input reach a raw SQL/NoSQL query without strict validation?",
2054
+ "DATAFLOW.UNTRUSTED.COMMAND_EXEC": "Can user input make it into exec/spawn/eval and change what runs?",
2055
+ "DATAFLOW.UNTRUSTED.FILE_ACCESS": "Can user input control file paths or write locations here?",
2056
+ "DATAFLOW.UNTRUSTED.RESPONSE": "Can user input drive redirects or HTML output without sanitization?",
2057
+ "SECRETS.HARDCODED": "If this repo were accidentally made public, what credentials would be exposed?",
2058
+ "CRYPTO.ALGORITHM.STRONG": "Is this encryption strong enough for the data it protects?"
2059
+ };
2060
+ return questions[invariantId] ?? null;
2061
+ }
2062
+ function generateTestSkeleton(invariant, framework, context) {
2063
+ if (!invariant) return "Unknown invariant";
2064
+ const testFn = framework === "jest" ? "test" : framework === "playwright" ? "test" : "it";
2065
+ const describe = framework === "playwright" ? "" : "describe";
2066
+ const wrap = (body) => {
2067
+ if (framework === "playwright") {
2068
+ return body.trim();
2069
+ }
2070
+ return `
2071
+ ${describe}('${escapeString(invariant.name)}', () => {
2072
+ ${indent(body.trim(), 2)}
2073
+ });
2074
+ `.trim();
2075
+ };
2076
+ switch (invariant.id) {
2077
+ case "AUTHZ.SERVICE_LAYER.ENFORCED":
2078
+ return wrap(`
2079
+ ${testFn}('denies access without valid authorization', async () => {
2080
+ // Arrange: create context without auth
2081
+ const unauthorizedContext = { userId: null, tenantId: null };
2082
+
2083
+ // Act + Assert: service call should throw (or return a forbidden result)
2084
+ await expect(
2085
+ yourService.sensitiveOperation({ context: unauthorizedContext })
2086
+ ).rejects.toThrow(/unauthorized|forbidden/i);
2087
+ });
2088
+
2089
+ ${testFn}('denies access to wrong-tenant resources', async () => {
2090
+ // Arrange: user from tenant-1 trying to access tenant-2 resource
2091
+ const context = { userId: 'user-1', tenantId: 'tenant-1' };
2092
+ const resourceFromOtherTenant = { id: 'resource-1', tenantId: 'tenant-2' };
2093
+
2094
+ // Act + Assert
2095
+ await expect(
2096
+ yourService.getResource({ context, resourceId: resourceFromOtherTenant.id })
2097
+ ).rejects.toThrow(/forbidden|access denied/i);
2098
+ });
2099
+ `);
2100
+ case "AUTHZ.MEMBERSHIP.REVOCATION.IMMEDIATE":
2101
+ return wrap(`
2102
+ ${testFn}('denies access immediately after membership removal', async () => {
2103
+ // Arrange: user with team membership
2104
+ const userId = 'user-1';
2105
+ const teamId = 'team-1';
2106
+ await addMemberToTeam(userId, teamId);
2107
+
2108
+ // Act: remove membership
2109
+ await removeMemberFromTeam(userId, teamId);
2110
+
2111
+ // Assert: immediate access denial (no TTL grace period)
2112
+ await expect(accessTeamResource({ userId, teamId })).rejects.toThrow(/forbidden|not a member/i);
2113
+ });
2114
+ `);
2115
+ case "WEBHOOK.IDEMPOTENT":
2116
+ return wrap(`
2117
+ ${testFn}('handles duplicate webhook events idempotently', async () => {
2118
+ // Arrange: create a webhook event
2119
+ const event = { id: 'evt_test_123', type: 'payment.succeeded', data: { amount: 1000 } };
2120
+
2121
+ // Act: process the same event twice
2122
+ await processWebhook(event);
2123
+ await processWebhook(event); // duplicate
2124
+
2125
+ // Assert: side effect only happened once
2126
+ const payments = await getPaymentRecords();
2127
+ expect(payments.filter((p: any) => p.eventId === event.id)).toHaveLength(1);
2128
+ });
2129
+
2130
+ ${testFn}('stores event IDs to prevent duplicates', async () => {
2131
+ const event = { id: 'evt_test_456', type: 'payment.succeeded' };
2132
+
2133
+ await processWebhook(event);
2134
+
2135
+ // Verify idempotency key was stored
2136
+ const stored = await getProcessedEventIds();
2137
+ expect(stored).toContain(event.id);
2138
+ });
2139
+ `);
2140
+ case "TRANSACTION.POST_COMMIT.SIDE_EFFECTS":
2141
+ return wrap(`
2142
+ ${testFn}('does not send side effects if transaction rolls back', async () => {
2143
+ const emailSpy = vi.spyOn(emailService, 'send');
2144
+
2145
+ // Act: trigger action that should fail and rollback
2146
+ await expect(createOrderWithInvalidData({ /* invalid data causing rollback */ })).rejects.toThrow();
2147
+
2148
+ // Assert: no email was sent
2149
+ expect(emailSpy).not.toHaveBeenCalled();
2150
+ });
2151
+
2152
+ ${testFn}('sends side effects only after successful commit', async () => {
2153
+ const emailSpy = vi.spyOn(emailService, 'send');
2154
+
2155
+ // Act: successful order creation
2156
+ await createOrder({ productId: 'prod-1', quantity: 1 });
2157
+
2158
+ // Assert: email was sent
2159
+ expect(emailSpy).toHaveBeenCalledOnce();
2160
+ });
2161
+ `);
2162
+ default: {
2163
+ const proof = invariant.requiredProof ? invariant.requiredProof : "(see invariant docs)";
2164
+ const contextLine = context ? `// Context: ${context}` : "";
2165
+ return wrap(`
2166
+ ${testFn}('enforces ${invariant.id}', async () => {
2167
+ // TODO: implement test for ${invariant.id}
2168
+ // Required proof: ${proof}
2169
+ ${contextLine}
2170
+
2171
+ throw new Error('Test not implemented');
2172
+ });
2173
+ `);
2174
+ }
2175
+ }
2176
+ }
2177
+ function indent(text, spaces) {
2178
+ const prefix = " ".repeat(spaces);
2179
+ return text.split("\n").map((line) => line ? `${prefix}${line}` : line).join("\n");
2180
+ }
2181
+ function escapeString(text) {
2182
+ return text.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2183
+ }
2184
+
2185
+ export { BASELINE_SCHEMA_VERSION, CLIError, ErrorCodes, ErrorMessages, ErrorRemediation, SUPPORTED_SCHEMA_RANGE, WAIVER_SCHEMA_VERSION, addToBaseline, addWaiver, attachFindingId, attachFindingIds, audit, buildTelemetry, calculateRelativeSeverity, categorizeFindings, checkCloudHealth, clearAggregateCache, correlateFindings, evaluateCloud, extractIdentityPayload, fetchAggregateCalibration, formatAggregateCalibrationSummary, formatCorrelatedFinding, formatCorrelationStats, generateFindingId, generateTestSkeleton, getCIExitCode, getCISummary, getCloudInvariants, getCurrentSchemaVersion, getExpiringWaivers, getFrameworkBaseline, getSkippedPatterns, getStaffQuestion, getValidWaiver, getVerifiedCorrelations, hasCollisions, isAggregateCalibrationDisabled, isCLIError, isCloudEvalAvailable, isInBaseline, isTelemetryDisabled, loadBaseline, loadWaivers, pruneBaseline, pruneExpiredWaivers, reportCorrelationFeedback, reportCorrelations, reportTelemetry, resolveCollisions, saveBaseline, saveWaivers, shouldSkipPattern, validateSchemaVersion, wrapError };
2186
+ //# sourceMappingURL=lib.js.map
2187
+ //# sourceMappingURL=lib.js.map