@quantracode/vibecheck 0.3.3 → 0.4.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/README.md +253 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1246 -699
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
|
|
12
12
|
// src/commands/scan.ts
|
|
13
|
-
import
|
|
13
|
+
import path9 from "path";
|
|
14
14
|
|
|
15
15
|
// ../schema/dist/schemas/claim.js
|
|
16
16
|
import { z } from "zod";
|
|
@@ -296,189 +296,301 @@ var SupplyChainInfoSchema = z5.object({
|
|
|
296
296
|
})
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
-
// ../schema/dist/schemas/
|
|
299
|
+
// ../schema/dist/schemas/custom-rule.js
|
|
300
300
|
import { z as z6 } from "zod";
|
|
301
|
+
var FileTypeSchema = z6.enum([
|
|
302
|
+
"ts",
|
|
303
|
+
"tsx",
|
|
304
|
+
"js",
|
|
305
|
+
"jsx",
|
|
306
|
+
"json",
|
|
307
|
+
"env",
|
|
308
|
+
"yaml",
|
|
309
|
+
"yml",
|
|
310
|
+
"md",
|
|
311
|
+
"config",
|
|
312
|
+
"any"
|
|
313
|
+
]);
|
|
314
|
+
var MatchConditionSchema = z6.object({
|
|
315
|
+
/** String or regex pattern to search for */
|
|
316
|
+
contains: z6.string().optional(),
|
|
317
|
+
/** Pattern that should NOT be present (for checking missing security controls) */
|
|
318
|
+
not_contains: z6.string().optional(),
|
|
319
|
+
/** Regex pattern to match (advanced) */
|
|
320
|
+
regex: z6.string().optional(),
|
|
321
|
+
/** Require the match to be case-sensitive (default: false) */
|
|
322
|
+
case_sensitive: z6.boolean().optional().default(false),
|
|
323
|
+
/** Require ALL conditions to match (default: true for contains, false for not_contains) */
|
|
324
|
+
all_must_match: z6.boolean().optional()
|
|
325
|
+
});
|
|
326
|
+
var FileFilterSchema = z6.object({
|
|
327
|
+
/** File types to include (e.g., ["ts", "js"]) */
|
|
328
|
+
file_type: z6.array(FileTypeSchema).optional(),
|
|
329
|
+
/** Glob patterns to include */
|
|
330
|
+
include: z6.array(z6.string()).optional(),
|
|
331
|
+
/** Glob patterns to exclude */
|
|
332
|
+
exclude: z6.array(z6.string()).optional(),
|
|
333
|
+
/** Only match files in specific directories */
|
|
334
|
+
directories: z6.array(z6.string()).optional()
|
|
335
|
+
});
|
|
336
|
+
var ContextConditionSchema = z6.object({
|
|
337
|
+
/** Match only in specific function/handler types */
|
|
338
|
+
in_function: z6.array(z6.enum([
|
|
339
|
+
"GET",
|
|
340
|
+
"POST",
|
|
341
|
+
"PUT",
|
|
342
|
+
"PATCH",
|
|
343
|
+
"DELETE",
|
|
344
|
+
"route_handler",
|
|
345
|
+
"middleware",
|
|
346
|
+
"any"
|
|
347
|
+
])).optional(),
|
|
348
|
+
/** Require presence of imports */
|
|
349
|
+
requires_import: z6.array(z6.string()).optional(),
|
|
350
|
+
/** Exclude if imports are present */
|
|
351
|
+
excludes_import: z6.array(z6.string()).optional(),
|
|
352
|
+
/** Only match if file contains certain keywords */
|
|
353
|
+
file_contains: z6.array(z6.string()).optional(),
|
|
354
|
+
/** Only match if file does NOT contain certain keywords */
|
|
355
|
+
file_not_contains: z6.array(z6.string()).optional()
|
|
356
|
+
});
|
|
357
|
+
var CustomRuleSchema = z6.object({
|
|
358
|
+
/** Unique rule identifier (e.g., "VC-CUSTOM-001" or "MY-RULE-001") */
|
|
359
|
+
id: z6.string().regex(/^[A-Z]+-[A-Z]+-\d{3,}$/, {
|
|
360
|
+
message: "Rule ID must match pattern XXX-XXX-000 (e.g., VC-CUSTOM-001)"
|
|
361
|
+
}),
|
|
362
|
+
/** Rule version for tracking updates */
|
|
363
|
+
version: z6.string().optional().default("1.0.0"),
|
|
364
|
+
/** Severity level */
|
|
365
|
+
severity: SeveritySchema,
|
|
366
|
+
/** Confidence score (0.0 - 1.0) */
|
|
367
|
+
confidence: z6.number().min(0).max(1).default(0.8),
|
|
368
|
+
/** Category */
|
|
369
|
+
category: CategorySchema,
|
|
370
|
+
/** Human-readable title */
|
|
371
|
+
title: z6.string().min(1),
|
|
372
|
+
/** Detailed description of the security issue */
|
|
373
|
+
description: z6.string().min(1),
|
|
374
|
+
/** File filters to limit where the rule applies */
|
|
375
|
+
files: FileFilterSchema.optional(),
|
|
376
|
+
/** Match conditions - what to look for */
|
|
377
|
+
match: MatchConditionSchema,
|
|
378
|
+
/** Context conditions for more sophisticated matching */
|
|
379
|
+
context: ContextConditionSchema.optional(),
|
|
380
|
+
/** Recommended fix explanation */
|
|
381
|
+
recommended_fix: z6.string(),
|
|
382
|
+
/** Optional code patch in unified diff format */
|
|
383
|
+
patch: z6.string().optional(),
|
|
384
|
+
/** Reference links */
|
|
385
|
+
links: z6.object({
|
|
386
|
+
owasp: z6.string().url().optional(),
|
|
387
|
+
cwe: z6.string().url().optional(),
|
|
388
|
+
documentation: z6.string().url().optional()
|
|
389
|
+
}).optional(),
|
|
390
|
+
/** Optional metadata */
|
|
391
|
+
metadata: z6.object({
|
|
392
|
+
/** Author of the rule */
|
|
393
|
+
author: z6.string().optional(),
|
|
394
|
+
/** Tags for categorization */
|
|
395
|
+
tags: z6.array(z6.string()).optional(),
|
|
396
|
+
/** Creation date */
|
|
397
|
+
created: z6.string().optional(),
|
|
398
|
+
/** Last updated date */
|
|
399
|
+
updated: z6.string().optional()
|
|
400
|
+
}).optional(),
|
|
401
|
+
/** Whether the rule is enabled (default: true) */
|
|
402
|
+
enabled: z6.boolean().optional().default(true)
|
|
403
|
+
});
|
|
404
|
+
var CustomRuleCollectionSchema = z6.object({
|
|
405
|
+
/** Schema version */
|
|
406
|
+
schema_version: z6.string().optional().default("1.0"),
|
|
407
|
+
/** Array of rules */
|
|
408
|
+
rules: z6.array(CustomRuleSchema)
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ../schema/dist/schemas/artifact.js
|
|
412
|
+
import { z as z7 } from "zod";
|
|
301
413
|
var ARTIFACT_VERSION = "0.3";
|
|
302
414
|
var SUPPORTED_VERSIONS = ["0.1", "0.2", "0.3"];
|
|
303
|
-
var ArtifactVersionSchema =
|
|
304
|
-
var ToolInfoSchema =
|
|
305
|
-
name:
|
|
306
|
-
version:
|
|
415
|
+
var ArtifactVersionSchema = z7.enum(SUPPORTED_VERSIONS);
|
|
416
|
+
var ToolInfoSchema = z7.object({
|
|
417
|
+
name: z7.string(),
|
|
418
|
+
version: z7.string()
|
|
307
419
|
});
|
|
308
|
-
var GitInfoSchema =
|
|
309
|
-
branch:
|
|
310
|
-
commit:
|
|
311
|
-
remoteUrl:
|
|
312
|
-
isDirty:
|
|
420
|
+
var GitInfoSchema = z7.object({
|
|
421
|
+
branch: z7.string().optional(),
|
|
422
|
+
commit: z7.string().optional(),
|
|
423
|
+
remoteUrl: z7.string().optional(),
|
|
424
|
+
isDirty: z7.boolean().optional()
|
|
313
425
|
});
|
|
314
|
-
var RepoInfoSchema =
|
|
315
|
-
name:
|
|
316
|
-
rootPathHash:
|
|
426
|
+
var RepoInfoSchema = z7.object({
|
|
427
|
+
name: z7.string(),
|
|
428
|
+
rootPathHash: z7.string(),
|
|
317
429
|
git: GitInfoSchema.optional()
|
|
318
430
|
});
|
|
319
|
-
var SeverityCountsSchema =
|
|
320
|
-
critical:
|
|
321
|
-
high:
|
|
322
|
-
medium:
|
|
323
|
-
low:
|
|
324
|
-
info:
|
|
431
|
+
var SeverityCountsSchema = z7.object({
|
|
432
|
+
critical: z7.number().int().nonnegative(),
|
|
433
|
+
high: z7.number().int().nonnegative(),
|
|
434
|
+
medium: z7.number().int().nonnegative(),
|
|
435
|
+
low: z7.number().int().nonnegative(),
|
|
436
|
+
info: z7.number().int().nonnegative()
|
|
325
437
|
});
|
|
326
|
-
var CategoryCountsSchema =
|
|
327
|
-
auth:
|
|
328
|
-
validation:
|
|
329
|
-
middleware:
|
|
330
|
-
secrets:
|
|
331
|
-
injection:
|
|
332
|
-
privacy:
|
|
333
|
-
config:
|
|
334
|
-
network:
|
|
335
|
-
crypto:
|
|
336
|
-
uploads:
|
|
337
|
-
hallucinations:
|
|
338
|
-
abuse:
|
|
438
|
+
var CategoryCountsSchema = z7.object({
|
|
439
|
+
auth: z7.number().int().nonnegative(),
|
|
440
|
+
validation: z7.number().int().nonnegative(),
|
|
441
|
+
middleware: z7.number().int().nonnegative(),
|
|
442
|
+
secrets: z7.number().int().nonnegative(),
|
|
443
|
+
injection: z7.number().int().nonnegative(),
|
|
444
|
+
privacy: z7.number().int().nonnegative(),
|
|
445
|
+
config: z7.number().int().nonnegative(),
|
|
446
|
+
network: z7.number().int().nonnegative(),
|
|
447
|
+
crypto: z7.number().int().nonnegative(),
|
|
448
|
+
uploads: z7.number().int().nonnegative(),
|
|
449
|
+
hallucinations: z7.number().int().nonnegative(),
|
|
450
|
+
abuse: z7.number().int().nonnegative(),
|
|
339
451
|
// Phase 4 categories
|
|
340
|
-
correlation:
|
|
341
|
-
authorization:
|
|
342
|
-
lifecycle:
|
|
343
|
-
"supply-chain":
|
|
344
|
-
other:
|
|
452
|
+
correlation: z7.number().int().nonnegative(),
|
|
453
|
+
authorization: z7.number().int().nonnegative(),
|
|
454
|
+
lifecycle: z7.number().int().nonnegative(),
|
|
455
|
+
"supply-chain": z7.number().int().nonnegative(),
|
|
456
|
+
other: z7.number().int().nonnegative()
|
|
345
457
|
});
|
|
346
|
-
var SummarySchema =
|
|
347
|
-
totalFindings:
|
|
458
|
+
var SummarySchema = z7.object({
|
|
459
|
+
totalFindings: z7.number().int().nonnegative(),
|
|
348
460
|
bySeverity: SeverityCountsSchema,
|
|
349
461
|
byCategory: CategoryCountsSchema
|
|
350
462
|
});
|
|
351
|
-
var RouteEntrySchema =
|
|
352
|
-
routeId:
|
|
353
|
-
method:
|
|
354
|
-
path:
|
|
355
|
-
handler:
|
|
356
|
-
file:
|
|
357
|
-
startLine:
|
|
358
|
-
endLine:
|
|
359
|
-
handlerSymbol:
|
|
463
|
+
var RouteEntrySchema = z7.object({
|
|
464
|
+
routeId: z7.string(),
|
|
465
|
+
method: z7.string(),
|
|
466
|
+
path: z7.string(),
|
|
467
|
+
handler: z7.string().optional(),
|
|
468
|
+
file: z7.string(),
|
|
469
|
+
startLine: z7.number().int().positive().optional(),
|
|
470
|
+
endLine: z7.number().int().positive().optional(),
|
|
471
|
+
handlerSymbol: z7.string().optional(),
|
|
360
472
|
// Deprecated: for backward compat with 0.1
|
|
361
|
-
line:
|
|
362
|
-
middleware:
|
|
473
|
+
line: z7.number().int().positive().optional(),
|
|
474
|
+
middleware: z7.array(z7.string()).optional()
|
|
363
475
|
});
|
|
364
|
-
var MiddlewareCoverageEntrySchema =
|
|
365
|
-
routeId:
|
|
366
|
-
covered:
|
|
367
|
-
reason:
|
|
476
|
+
var MiddlewareCoverageEntrySchema = z7.object({
|
|
477
|
+
routeId: z7.string(),
|
|
478
|
+
covered: z7.boolean(),
|
|
479
|
+
reason: z7.string().optional()
|
|
368
480
|
});
|
|
369
|
-
var MiddlewareEntrySchema =
|
|
370
|
-
name:
|
|
371
|
-
file:
|
|
372
|
-
line:
|
|
373
|
-
matcher:
|
|
374
|
-
appliesTo:
|
|
481
|
+
var MiddlewareEntrySchema = z7.object({
|
|
482
|
+
name: z7.string().optional(),
|
|
483
|
+
file: z7.string(),
|
|
484
|
+
line: z7.number().int().positive().optional(),
|
|
485
|
+
matcher: z7.array(z7.string()).optional(),
|
|
486
|
+
appliesTo: z7.array(z7.string()).optional()
|
|
375
487
|
});
|
|
376
|
-
var MiddlewareMapSchema =
|
|
377
|
-
middlewareFile:
|
|
378
|
-
matcher:
|
|
379
|
-
coverage:
|
|
488
|
+
var MiddlewareMapSchema = z7.object({
|
|
489
|
+
middlewareFile: z7.string().optional(),
|
|
490
|
+
matcher: z7.array(z7.string()),
|
|
491
|
+
coverage: z7.array(MiddlewareCoverageEntrySchema)
|
|
380
492
|
});
|
|
381
|
-
var IntentEntrySchema =
|
|
382
|
-
intentId:
|
|
493
|
+
var IntentEntrySchema = z7.object({
|
|
494
|
+
intentId: z7.string(),
|
|
383
495
|
type: ClaimTypeSchema,
|
|
384
496
|
scope: ClaimScopeSchema,
|
|
385
|
-
targetRouteId:
|
|
497
|
+
targetRouteId: z7.string().optional(),
|
|
386
498
|
source: ClaimSourceSchema,
|
|
387
|
-
location:
|
|
388
|
-
file:
|
|
389
|
-
startLine:
|
|
390
|
-
endLine:
|
|
499
|
+
location: z7.object({
|
|
500
|
+
file: z7.string(),
|
|
501
|
+
startLine: z7.number().int().positive(),
|
|
502
|
+
endLine: z7.number().int().positive()
|
|
391
503
|
}),
|
|
392
504
|
strength: ClaimStrengthSchema,
|
|
393
|
-
textEvidence:
|
|
505
|
+
textEvidence: z7.string()
|
|
394
506
|
});
|
|
395
|
-
var IntentMapSchema =
|
|
396
|
-
intents:
|
|
507
|
+
var IntentMapSchema = z7.object({
|
|
508
|
+
intents: z7.array(IntentEntrySchema)
|
|
397
509
|
});
|
|
398
|
-
var RouteMapSchema =
|
|
399
|
-
routes:
|
|
510
|
+
var RouteMapSchema = z7.object({
|
|
511
|
+
routes: z7.array(RouteEntrySchema)
|
|
400
512
|
});
|
|
401
|
-
var CoverageMetricsSchema =
|
|
402
|
-
authCoverage:
|
|
403
|
-
totalStateChanging:
|
|
404
|
-
protectedCount:
|
|
405
|
-
unprotectedCount:
|
|
513
|
+
var CoverageMetricsSchema = z7.object({
|
|
514
|
+
authCoverage: z7.object({
|
|
515
|
+
totalStateChanging: z7.number().int().nonnegative(),
|
|
516
|
+
protectedCount: z7.number().int().nonnegative(),
|
|
517
|
+
unprotectedCount: z7.number().int().nonnegative()
|
|
406
518
|
}).optional(),
|
|
407
|
-
validationCoverage:
|
|
408
|
-
totalStateChanging:
|
|
409
|
-
validatedCount:
|
|
519
|
+
validationCoverage: z7.object({
|
|
520
|
+
totalStateChanging: z7.number().int().nonnegative(),
|
|
521
|
+
validatedCount: z7.number().int().nonnegative()
|
|
410
522
|
}).optional(),
|
|
411
|
-
middlewareCoverage:
|
|
412
|
-
totalApiRoutes:
|
|
413
|
-
coveredApiRoutes:
|
|
523
|
+
middlewareCoverage: z7.object({
|
|
524
|
+
totalApiRoutes: z7.number().int().nonnegative(),
|
|
525
|
+
coveredApiRoutes: z7.number().int().nonnegative()
|
|
414
526
|
}).optional()
|
|
415
527
|
});
|
|
416
|
-
var MetricsSchema =
|
|
417
|
-
filesScanned:
|
|
418
|
-
linesOfCode:
|
|
419
|
-
scanDurationMs:
|
|
420
|
-
rulesExecuted:
|
|
528
|
+
var MetricsSchema = z7.object({
|
|
529
|
+
filesScanned: z7.number().int().nonnegative(),
|
|
530
|
+
linesOfCode: z7.number().int().nonnegative(),
|
|
531
|
+
scanDurationMs: z7.number().nonnegative(),
|
|
532
|
+
rulesExecuted: z7.number().int().nonnegative()
|
|
421
533
|
}).merge(CoverageMetricsSchema);
|
|
422
|
-
var CIMetadataSchema =
|
|
534
|
+
var CIMetadataSchema = z7.object({
|
|
423
535
|
/** Security score (0-100) */
|
|
424
|
-
securityScore:
|
|
536
|
+
securityScore: z7.number().int().min(0).max(100),
|
|
425
537
|
/** Overall status for CI badge */
|
|
426
|
-
status:
|
|
538
|
+
status: z7.enum(["pass", "warn", "fail"]),
|
|
427
539
|
/** Badge generation timestamp */
|
|
428
|
-
badgeGeneratedAt:
|
|
540
|
+
badgeGeneratedAt: z7.string().datetime().optional(),
|
|
429
541
|
/** SHA-256 hash for determinism certification */
|
|
430
|
-
artifactHash:
|
|
542
|
+
artifactHash: z7.string().optional(),
|
|
431
543
|
/** Whether artifact passed determinism certification */
|
|
432
|
-
deterministicCertified:
|
|
544
|
+
deterministicCertified: z7.boolean().optional()
|
|
433
545
|
});
|
|
434
|
-
var CorrelationSummarySchema =
|
|
546
|
+
var CorrelationSummarySchema = z7.object({
|
|
435
547
|
/** Total number of correlation findings generated */
|
|
436
|
-
totalCorrelations:
|
|
548
|
+
totalCorrelations: z7.number().int().nonnegative(),
|
|
437
549
|
/** Count by correlation pattern */
|
|
438
|
-
byPattern:
|
|
550
|
+
byPattern: z7.record(z7.string(), z7.number().int().nonnegative()),
|
|
439
551
|
/** Correlation pass duration in ms */
|
|
440
|
-
correlationDurationMs:
|
|
552
|
+
correlationDurationMs: z7.number().nonnegative().optional()
|
|
441
553
|
});
|
|
442
|
-
var GraphNodeSchema =
|
|
443
|
-
id:
|
|
444
|
-
type:
|
|
445
|
-
label:
|
|
446
|
-
file:
|
|
447
|
-
line:
|
|
448
|
-
metadata:
|
|
554
|
+
var GraphNodeSchema = z7.object({
|
|
555
|
+
id: z7.string(),
|
|
556
|
+
type: z7.enum(["route", "middleware", "finding", "intent", "function"]),
|
|
557
|
+
label: z7.string(),
|
|
558
|
+
file: z7.string().optional(),
|
|
559
|
+
line: z7.number().int().positive().optional(),
|
|
560
|
+
metadata: z7.record(z7.string(), z7.unknown()).optional()
|
|
449
561
|
});
|
|
450
|
-
var GraphEdgeSchema =
|
|
451
|
-
source:
|
|
452
|
-
target:
|
|
453
|
-
type:
|
|
454
|
-
label:
|
|
562
|
+
var GraphEdgeSchema = z7.object({
|
|
563
|
+
source: z7.string(),
|
|
564
|
+
target: z7.string(),
|
|
565
|
+
type: z7.enum(["calls", "protects", "validates", "correlates", "references"]),
|
|
566
|
+
label: z7.string().optional()
|
|
455
567
|
});
|
|
456
|
-
var ProofTraceGraphSchema =
|
|
457
|
-
nodes:
|
|
458
|
-
edges:
|
|
568
|
+
var ProofTraceGraphSchema = z7.object({
|
|
569
|
+
nodes: z7.array(GraphNodeSchema),
|
|
570
|
+
edges: z7.array(GraphEdgeSchema)
|
|
459
571
|
});
|
|
460
|
-
var ScanArtifactSchema =
|
|
572
|
+
var ScanArtifactSchema = z7.object({
|
|
461
573
|
artifactVersion: ArtifactVersionSchema,
|
|
462
|
-
generatedAt:
|
|
574
|
+
generatedAt: z7.string().datetime(),
|
|
463
575
|
tool: ToolInfoSchema,
|
|
464
576
|
repo: RepoInfoSchema.optional(),
|
|
465
577
|
summary: SummarySchema,
|
|
466
|
-
findings:
|
|
578
|
+
findings: z7.array(FindingSchema),
|
|
467
579
|
// Phase 3: Enhanced maps
|
|
468
|
-
routeMap:
|
|
469
|
-
|
|
580
|
+
routeMap: z7.union([
|
|
581
|
+
z7.array(RouteEntrySchema),
|
|
470
582
|
// Legacy format (0.1)
|
|
471
583
|
RouteMapSchema
|
|
472
584
|
// New format (0.2)
|
|
473
585
|
]).optional(),
|
|
474
|
-
middlewareMap:
|
|
475
|
-
|
|
586
|
+
middlewareMap: z7.union([
|
|
587
|
+
z7.array(MiddlewareEntrySchema),
|
|
476
588
|
// Legacy format (0.1)
|
|
477
589
|
MiddlewareMapSchema
|
|
478
590
|
// New format (0.2)
|
|
479
591
|
]).optional(),
|
|
480
592
|
intentMap: IntentMapSchema.optional(),
|
|
481
|
-
proofTraces:
|
|
593
|
+
proofTraces: z7.record(z7.string(), ProofTraceSchema).optional(),
|
|
482
594
|
metrics: MetricsSchema.optional(),
|
|
483
595
|
// Phase 4: Supply chain analysis
|
|
484
596
|
supplyChainInfo: SupplyChainInfoSchema.optional(),
|
|
@@ -547,7 +659,7 @@ function validateArtifact(json) {
|
|
|
547
659
|
}
|
|
548
660
|
|
|
549
661
|
// src/constants.ts
|
|
550
|
-
var CLI_VERSION = "0.
|
|
662
|
+
var CLI_VERSION = "0.4.1";
|
|
551
663
|
|
|
552
664
|
// src/utils/file-utils.ts
|
|
553
665
|
import fs from "fs";
|
|
@@ -656,6 +768,601 @@ function getRepoName(cwd) {
|
|
|
656
768
|
return path2.basename(cwd);
|
|
657
769
|
}
|
|
658
770
|
|
|
771
|
+
// src/utils/apply-patches.ts
|
|
772
|
+
import { readFile, writeFile } from "fs/promises";
|
|
773
|
+
import { existsSync } from "fs";
|
|
774
|
+
import path3 from "path";
|
|
775
|
+
import readline from "readline";
|
|
776
|
+
function isUnifiedDiff(patch) {
|
|
777
|
+
const lines = patch.trim().split("\n");
|
|
778
|
+
return lines.some(
|
|
779
|
+
(line) => line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("@@ ")
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
async function applyUnifiedDiff(filePath, patch, baseDir) {
|
|
783
|
+
try {
|
|
784
|
+
const absolutePath = path3.isAbsolute(filePath) ? filePath : path3.resolve(baseDir, filePath);
|
|
785
|
+
if (!existsSync(absolutePath)) {
|
|
786
|
+
return {
|
|
787
|
+
success: false,
|
|
788
|
+
error: `File not found: ${filePath}`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
792
|
+
const lines = content.split("\n");
|
|
793
|
+
const patchLines = patch.split("\n");
|
|
794
|
+
let currentLine = 0;
|
|
795
|
+
let inHunk = false;
|
|
796
|
+
let hunkStartLine = 0;
|
|
797
|
+
const newLines = [];
|
|
798
|
+
for (const patchLine of patchLines) {
|
|
799
|
+
if (patchLine.startsWith("--- ") || patchLine.startsWith("+++ ")) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (patchLine.startsWith("@@ ")) {
|
|
803
|
+
const match = patchLine.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/);
|
|
804
|
+
if (match) {
|
|
805
|
+
hunkStartLine = parseInt(match[1], 10) - 1;
|
|
806
|
+
while (currentLine < hunkStartLine) {
|
|
807
|
+
newLines.push(lines[currentLine]);
|
|
808
|
+
currentLine++;
|
|
809
|
+
}
|
|
810
|
+
inHunk = true;
|
|
811
|
+
}
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (inHunk) {
|
|
815
|
+
if (patchLine.startsWith("-")) {
|
|
816
|
+
currentLine++;
|
|
817
|
+
} else if (patchLine.startsWith("+")) {
|
|
818
|
+
newLines.push(patchLine.slice(1));
|
|
819
|
+
} else if (patchLine.startsWith(" ")) {
|
|
820
|
+
const contextLine = patchLine.slice(1);
|
|
821
|
+
if (lines[currentLine] !== contextLine) {
|
|
822
|
+
return {
|
|
823
|
+
success: false,
|
|
824
|
+
error: `Patch context mismatch at line ${currentLine + 1}. Expected: "${contextLine}", found: "${lines[currentLine]}"`
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
newLines.push(lines[currentLine]);
|
|
828
|
+
currentLine++;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
while (currentLine < lines.length) {
|
|
833
|
+
newLines.push(lines[currentLine]);
|
|
834
|
+
currentLine++;
|
|
835
|
+
}
|
|
836
|
+
await writeFile(absolutePath, newLines.join("\n"), "utf-8");
|
|
837
|
+
return { success: true };
|
|
838
|
+
} catch (error) {
|
|
839
|
+
return {
|
|
840
|
+
success: false,
|
|
841
|
+
error: error instanceof Error ? error.message : String(error)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async function promptConfirmation(message) {
|
|
846
|
+
const rl = readline.createInterface({
|
|
847
|
+
input: process.stdin,
|
|
848
|
+
output: process.stdout
|
|
849
|
+
});
|
|
850
|
+
return new Promise((resolve3) => {
|
|
851
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
852
|
+
rl.close();
|
|
853
|
+
resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
async function applyPatches(findings, baseDir, options = {}) {
|
|
858
|
+
const results = [];
|
|
859
|
+
const patchableFindings = findings.filter(
|
|
860
|
+
(f) => f.remediation?.patch && f.remediation.patch.trim().length > 0
|
|
861
|
+
);
|
|
862
|
+
if (patchableFindings.length === 0) {
|
|
863
|
+
return {
|
|
864
|
+
totalPatchable: 0,
|
|
865
|
+
applied: 0,
|
|
866
|
+
failed: 0,
|
|
867
|
+
skipped: 0,
|
|
868
|
+
results: []
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
console.log(`
|
|
872
|
+
Found ${patchableFindings.length} finding(s) with patches.
|
|
873
|
+
`);
|
|
874
|
+
for (const finding of patchableFindings) {
|
|
875
|
+
const patch = finding.remediation.patch;
|
|
876
|
+
const firstEvidence = finding.evidence[0];
|
|
877
|
+
if (!firstEvidence) {
|
|
878
|
+
results.push({
|
|
879
|
+
findingId: finding.id,
|
|
880
|
+
file: "unknown",
|
|
881
|
+
success: false,
|
|
882
|
+
error: "No evidence found to determine target file",
|
|
883
|
+
patch
|
|
884
|
+
});
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const targetFile = firstEvidence.file;
|
|
888
|
+
if (!isUnifiedDiff(patch)) {
|
|
889
|
+
results.push({
|
|
890
|
+
findingId: finding.id,
|
|
891
|
+
file: targetFile,
|
|
892
|
+
success: false,
|
|
893
|
+
error: "Patch is not in unified diff format. Only standard git-style diffs are supported.",
|
|
894
|
+
patch
|
|
895
|
+
});
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
console.log(`\x1B[36m[${finding.ruleId}]\x1B[0m ${finding.title}`);
|
|
899
|
+
console.log(` File: ${targetFile}`);
|
|
900
|
+
console.log(` Severity: \x1B[33m${finding.severity.toUpperCase()}\x1B[0m`);
|
|
901
|
+
console.log(` Description: ${finding.description.slice(0, 100)}${finding.description.length > 100 ? "..." : ""}`);
|
|
902
|
+
console.log(`
|
|
903
|
+
Patch preview:`);
|
|
904
|
+
const patchLines = patch.split("\n").slice(0, 15);
|
|
905
|
+
for (const line of patchLines) {
|
|
906
|
+
if (line.startsWith("+")) {
|
|
907
|
+
console.log(` \x1B[32m${line}\x1B[0m`);
|
|
908
|
+
} else if (line.startsWith("-")) {
|
|
909
|
+
console.log(` \x1B[31m${line}\x1B[0m`);
|
|
910
|
+
} else {
|
|
911
|
+
console.log(` ${line}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (patch.split("\n").length > 15) {
|
|
915
|
+
console.log(` \x1B[90m... (${patch.split("\n").length - 15} more lines)\x1B[0m`);
|
|
916
|
+
}
|
|
917
|
+
console.log("");
|
|
918
|
+
if (options.dryRun) {
|
|
919
|
+
console.log(` \x1B[90m[DRY RUN] Would apply patch to ${targetFile}\x1B[0m
|
|
920
|
+
`);
|
|
921
|
+
results.push({
|
|
922
|
+
findingId: finding.id,
|
|
923
|
+
file: targetFile,
|
|
924
|
+
success: true,
|
|
925
|
+
patch
|
|
926
|
+
});
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
let shouldApply = options.force ?? false;
|
|
930
|
+
if (!options.force) {
|
|
931
|
+
shouldApply = await promptConfirmation(" Apply this patch?");
|
|
932
|
+
}
|
|
933
|
+
if (!shouldApply) {
|
|
934
|
+
console.log(` \x1B[90mSkipped\x1B[0m
|
|
935
|
+
`);
|
|
936
|
+
results.push({
|
|
937
|
+
findingId: finding.id,
|
|
938
|
+
file: targetFile,
|
|
939
|
+
success: false,
|
|
940
|
+
error: "User declined",
|
|
941
|
+
patch
|
|
942
|
+
});
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const result = await applyUnifiedDiff(targetFile, patch, baseDir);
|
|
946
|
+
if (result.success) {
|
|
947
|
+
console.log(` \x1B[32m\u2713 Patch applied successfully\x1B[0m
|
|
948
|
+
`);
|
|
949
|
+
results.push({
|
|
950
|
+
findingId: finding.id,
|
|
951
|
+
file: targetFile,
|
|
952
|
+
success: true,
|
|
953
|
+
patch
|
|
954
|
+
});
|
|
955
|
+
} else {
|
|
956
|
+
console.log(` \x1B[31m\u2717 Failed to apply patch: ${result.error}\x1B[0m
|
|
957
|
+
`);
|
|
958
|
+
results.push({
|
|
959
|
+
findingId: finding.id,
|
|
960
|
+
file: targetFile,
|
|
961
|
+
success: false,
|
|
962
|
+
error: result.error,
|
|
963
|
+
patch
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const applied = results.filter((r) => r.success).length;
|
|
968
|
+
const failed = results.filter((r) => !r.success && r.error !== "User declined").length;
|
|
969
|
+
const skipped = results.filter((r) => r.error === "User declined").length;
|
|
970
|
+
return {
|
|
971
|
+
totalPatchable: patchableFindings.length,
|
|
972
|
+
applied,
|
|
973
|
+
failed,
|
|
974
|
+
skipped,
|
|
975
|
+
results
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/utils/custom-rules-loader.ts
|
|
980
|
+
import { readFileSync as readFileSync2, statSync, readdirSync } from "fs";
|
|
981
|
+
import { join, extname } from "path";
|
|
982
|
+
import { parse as parseYAML } from "yaml";
|
|
983
|
+
function loadRuleFile(filePath) {
|
|
984
|
+
try {
|
|
985
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
986
|
+
const parsed = parseYAML(content);
|
|
987
|
+
const collectionResult = CustomRuleCollectionSchema.safeParse(parsed);
|
|
988
|
+
if (collectionResult.success) {
|
|
989
|
+
return collectionResult.data.rules.filter((rule) => rule.enabled !== false);
|
|
990
|
+
}
|
|
991
|
+
const ruleResult = CustomRuleSchema.safeParse(parsed);
|
|
992
|
+
if (ruleResult.success) {
|
|
993
|
+
return ruleResult.data.enabled !== false ? [ruleResult.data] : [];
|
|
994
|
+
}
|
|
995
|
+
throw new Error(
|
|
996
|
+
`Invalid rule format in ${filePath}:
|
|
997
|
+
${collectionResult.error?.message || ruleResult.error?.message}`
|
|
998
|
+
);
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
if (error instanceof Error) {
|
|
1001
|
+
throw new Error(`Failed to load rule file ${filePath}: ${error.message}`);
|
|
1002
|
+
}
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function loadRulesFromDirectory(dirPath) {
|
|
1007
|
+
const rules = [];
|
|
1008
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1009
|
+
try {
|
|
1010
|
+
const stat = statSync(dirPath);
|
|
1011
|
+
if (stat.isFile()) {
|
|
1012
|
+
const fileRules = loadRuleFile(dirPath);
|
|
1013
|
+
for (const rule of fileRules) {
|
|
1014
|
+
if (seenIds.has(rule.id)) {
|
|
1015
|
+
console.warn(
|
|
1016
|
+
`\x1B[33mWarning: Duplicate rule ID "${rule.id}" found, skipping duplicate\x1B[0m`
|
|
1017
|
+
);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
seenIds.add(rule.id);
|
|
1021
|
+
rules.push(rule);
|
|
1022
|
+
}
|
|
1023
|
+
return rules;
|
|
1024
|
+
}
|
|
1025
|
+
const files = readdirSync(dirPath);
|
|
1026
|
+
for (const file of files) {
|
|
1027
|
+
const ext = extname(file).toLowerCase();
|
|
1028
|
+
if (![".yaml", ".yml"].includes(ext)) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const filePath = join(dirPath, file);
|
|
1032
|
+
try {
|
|
1033
|
+
const fileRules = loadRuleFile(filePath);
|
|
1034
|
+
for (const rule of fileRules) {
|
|
1035
|
+
if (seenIds.has(rule.id)) {
|
|
1036
|
+
console.warn(
|
|
1037
|
+
`\x1B[33mWarning: Duplicate rule ID "${rule.id}" in ${file}, skipping duplicate\x1B[0m`
|
|
1038
|
+
);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
seenIds.add(rule.id);
|
|
1042
|
+
rules.push(rule);
|
|
1043
|
+
}
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
console.error(
|
|
1046
|
+
`\x1B[31mError loading ${file}: ${error instanceof Error ? error.message : String(error)}\x1B[0m`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return rules;
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
throw new Error(
|
|
1053
|
+
`Failed to load rules from ${dirPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function validateCustomRules(rules) {
|
|
1058
|
+
const valid = [];
|
|
1059
|
+
const errors = [];
|
|
1060
|
+
for (const rule of rules) {
|
|
1061
|
+
try {
|
|
1062
|
+
if (!rule.match.contains && !rule.match.not_contains && !rule.match.regex) {
|
|
1063
|
+
errors.push({
|
|
1064
|
+
ruleId: rule.id,
|
|
1065
|
+
error: "Rule must have at least one match condition (contains, not_contains, or regex)"
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (rule.match.regex) {
|
|
1070
|
+
try {
|
|
1071
|
+
new RegExp(rule.match.regex);
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
errors.push({
|
|
1074
|
+
ruleId: rule.id,
|
|
1075
|
+
error: `Invalid regex pattern: ${rule.match.regex}`
|
|
1076
|
+
});
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (rule.files?.file_type && rule.files.file_type.length === 0) {
|
|
1081
|
+
errors.push({
|
|
1082
|
+
ruleId: rule.id,
|
|
1083
|
+
error: "file_type array cannot be empty"
|
|
1084
|
+
});
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
valid.push(rule);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
errors.push({
|
|
1090
|
+
ruleId: rule.id,
|
|
1091
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { valid, errors };
|
|
1096
|
+
}
|
|
1097
|
+
function printRulesSummary(rules) {
|
|
1098
|
+
if (rules.length === 0) {
|
|
1099
|
+
console.log(" No custom rules loaded");
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
console.log(` Loaded ${rules.length} custom rule(s):`);
|
|
1103
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1104
|
+
for (const rule of rules) {
|
|
1105
|
+
const existing = byCategory.get(rule.category) || [];
|
|
1106
|
+
existing.push(rule);
|
|
1107
|
+
byCategory.set(rule.category, existing);
|
|
1108
|
+
}
|
|
1109
|
+
for (const [category, categoryRules] of byCategory) {
|
|
1110
|
+
console.log(` \x1B[36m${category}\x1B[0m: ${categoryRules.length} rule(s)`);
|
|
1111
|
+
for (const rule of categoryRules) {
|
|
1112
|
+
const severityColor = rule.severity === "critical" || rule.severity === "high" ? "\x1B[31m" : rule.severity === "medium" ? "\x1B[33m" : "\x1B[36m";
|
|
1113
|
+
console.log(
|
|
1114
|
+
` [${rule.id}] ${rule.title} (${severityColor}${rule.severity.toUpperCase()}\x1B[0m)`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/scanners/custom-rule-engine.ts
|
|
1121
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1122
|
+
import { basename, extname as extname2 } from "path";
|
|
1123
|
+
import micromatch from "micromatch";
|
|
1124
|
+
function matchesFileFilter(filePath, filter) {
|
|
1125
|
+
if (!filter) {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
const ext = extname2(filePath).slice(1).toLowerCase();
|
|
1129
|
+
const fileName = basename(filePath);
|
|
1130
|
+
if (filter.file_type && filter.file_type.length > 0) {
|
|
1131
|
+
const hasAny = filter.file_type.includes("any");
|
|
1132
|
+
if (!hasAny) {
|
|
1133
|
+
const matchesType = filter.file_type.some((type) => {
|
|
1134
|
+
if (type === "config") {
|
|
1135
|
+
return /\.(config|rc)\.(js|ts|json|yaml|yml)$/.test(fileName) || /^\..*rc$/.test(fileName);
|
|
1136
|
+
}
|
|
1137
|
+
if (type === "env") {
|
|
1138
|
+
return /^\.env/.test(fileName);
|
|
1139
|
+
}
|
|
1140
|
+
return ext === type || type === "tsx" && ext === "ts" || type === "jsx" && ext === "js";
|
|
1141
|
+
});
|
|
1142
|
+
if (!matchesType) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (filter.include && filter.include.length > 0) {
|
|
1148
|
+
if (!micromatch.isMatch(filePath, filter.include)) {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (filter.exclude && filter.exclude.length > 0) {
|
|
1153
|
+
if (micromatch.isMatch(filePath, filter.exclude)) {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (filter.directories && filter.directories.length > 0) {
|
|
1158
|
+
const matchesDir = filter.directories.some((dir) => {
|
|
1159
|
+
const normalizedDir = dir.replace(/\\/g, "/");
|
|
1160
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
1161
|
+
return normalizedPath.includes(normalizedDir);
|
|
1162
|
+
});
|
|
1163
|
+
if (!matchesDir) {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
function matchesContent(content, match, caseSensitive = false) {
|
|
1170
|
+
const lines = content.split("\n");
|
|
1171
|
+
const matchedLines = [];
|
|
1172
|
+
let snippet;
|
|
1173
|
+
const normalize = (str) => caseSensitive ? str : str.toLowerCase();
|
|
1174
|
+
if (match.contains) {
|
|
1175
|
+
const searchTerm = normalize(match.contains);
|
|
1176
|
+
let found = false;
|
|
1177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1178
|
+
if (normalize(lines[i]).includes(searchTerm)) {
|
|
1179
|
+
matchedLines.push(i + 1);
|
|
1180
|
+
if (!snippet) {
|
|
1181
|
+
snippet = lines[i].trim();
|
|
1182
|
+
}
|
|
1183
|
+
found = true;
|
|
1184
|
+
if (!match.all_must_match) {
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (!found) {
|
|
1190
|
+
return { matches: false, matchedLines: [] };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (match.not_contains) {
|
|
1194
|
+
const searchTerm = normalize(match.not_contains);
|
|
1195
|
+
const defaultAllMatch = match.all_must_match ?? true;
|
|
1196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1197
|
+
if (normalize(lines[i]).includes(searchTerm)) {
|
|
1198
|
+
if (defaultAllMatch) {
|
|
1199
|
+
return { matches: false, matchedLines: [] };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (matchedLines.length === 0) {
|
|
1204
|
+
matchedLines.push(1);
|
|
1205
|
+
snippet = lines[0]?.trim() || "";
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (match.regex) {
|
|
1209
|
+
try {
|
|
1210
|
+
const flags = caseSensitive ? "g" : "gi";
|
|
1211
|
+
const regex = new RegExp(match.regex, flags);
|
|
1212
|
+
let found = false;
|
|
1213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1214
|
+
if (regex.test(lines[i])) {
|
|
1215
|
+
matchedLines.push(i + 1);
|
|
1216
|
+
if (!snippet) {
|
|
1217
|
+
snippet = lines[i].trim();
|
|
1218
|
+
}
|
|
1219
|
+
found = true;
|
|
1220
|
+
if (!match.all_must_match) {
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (!found) {
|
|
1226
|
+
return { matches: false, matchedLines: [] };
|
|
1227
|
+
}
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
console.error(`Invalid regex: ${match.regex}`);
|
|
1230
|
+
return { matches: false, matchedLines: [] };
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
matches: matchedLines.length > 0,
|
|
1235
|
+
matchedLines,
|
|
1236
|
+
snippet
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function matchesContext(content, sourceFile, context, helpers) {
|
|
1240
|
+
if (!context) {
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
const normalizeContent = content.toLowerCase();
|
|
1244
|
+
if (context.requires_import && context.requires_import.length > 0) {
|
|
1245
|
+
const hasAllRequiredImports = context.requires_import.every((imp) => {
|
|
1246
|
+
return normalizeContent.includes(`from "${imp}"`) || normalizeContent.includes(`from '${imp}'`) || normalizeContent.includes(`require("${imp}")`) || normalizeContent.includes(`require('${imp}')`);
|
|
1247
|
+
});
|
|
1248
|
+
if (!hasAllRequiredImports) {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (context.excludes_import && context.excludes_import.length > 0) {
|
|
1253
|
+
const hasExcludedImport = context.excludes_import.some((imp) => {
|
|
1254
|
+
return normalizeContent.includes(`from "${imp}"`) || normalizeContent.includes(`from '${imp}'`) || normalizeContent.includes(`require("${imp}")`) || normalizeContent.includes(`require('${imp}')`);
|
|
1255
|
+
});
|
|
1256
|
+
if (hasExcludedImport) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (context.file_contains && context.file_contains.length > 0) {
|
|
1261
|
+
const hasAll = context.file_contains.every(
|
|
1262
|
+
(term) => normalizeContent.includes(term.toLowerCase())
|
|
1263
|
+
);
|
|
1264
|
+
if (!hasAll) {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (context.file_not_contains && context.file_not_contains.length > 0) {
|
|
1269
|
+
const hasAny = context.file_not_contains.some(
|
|
1270
|
+
(term) => normalizeContent.includes(term.toLowerCase())
|
|
1271
|
+
);
|
|
1272
|
+
if (hasAny) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (context.in_function && context.in_function.length > 0 && sourceFile) {
|
|
1277
|
+
try {
|
|
1278
|
+
const handlers = helpers.findRouteHandlers(sourceFile);
|
|
1279
|
+
if (handlers.length === 0) {
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
const hasMatchingHandler = handlers.some((handler) => {
|
|
1283
|
+
return context.in_function?.includes(handler.method) || context.in_function?.includes("route_handler") || context.in_function?.includes("any");
|
|
1284
|
+
});
|
|
1285
|
+
if (!hasMatchingHandler) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
function createScannerFromRule(rule) {
|
|
1294
|
+
return async (context) => {
|
|
1295
|
+
const { repoRoot, fileIndex, helpers } = context;
|
|
1296
|
+
const findings = [];
|
|
1297
|
+
let filesToScan = fileIndex.allSourceFiles;
|
|
1298
|
+
filesToScan = filesToScan.filter(
|
|
1299
|
+
(relPath) => matchesFileFilter(relPath, rule.files)
|
|
1300
|
+
);
|
|
1301
|
+
for (const relPath of filesToScan) {
|
|
1302
|
+
const absPath = resolvePath(repoRoot, relPath);
|
|
1303
|
+
try {
|
|
1304
|
+
const content = readFileSync3(absPath, "utf-8");
|
|
1305
|
+
let sourceFile = null;
|
|
1306
|
+
if (/\.(ts|tsx|js|jsx)$/.test(relPath)) {
|
|
1307
|
+
sourceFile = helpers.parseFile(absPath);
|
|
1308
|
+
}
|
|
1309
|
+
if (!matchesContext(content, sourceFile, rule.context, helpers)) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const matchResult = matchesContent(
|
|
1313
|
+
content,
|
|
1314
|
+
rule.match,
|
|
1315
|
+
rule.match.case_sensitive
|
|
1316
|
+
);
|
|
1317
|
+
if (!matchResult.matches) {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const evidence = matchResult.matchedLines.slice(0, 3).map((lineNum) => ({
|
|
1321
|
+
file: relPath,
|
|
1322
|
+
startLine: lineNum,
|
|
1323
|
+
endLine: lineNum,
|
|
1324
|
+
snippet: matchResult.snippet || content.split("\n")[lineNum - 1]?.trim() || "",
|
|
1325
|
+
label: `Match found`
|
|
1326
|
+
}));
|
|
1327
|
+
const fingerprint = generateFingerprint({
|
|
1328
|
+
ruleId: rule.id,
|
|
1329
|
+
file: relPath,
|
|
1330
|
+
line: matchResult.matchedLines[0] || 1,
|
|
1331
|
+
snippet: matchResult.snippet || ""
|
|
1332
|
+
});
|
|
1333
|
+
const finding = {
|
|
1334
|
+
id: generateFindingId({
|
|
1335
|
+
ruleId: rule.id,
|
|
1336
|
+
file: relPath,
|
|
1337
|
+
symbol: matchResult.snippet || "",
|
|
1338
|
+
startLine: matchResult.matchedLines[0] || 1
|
|
1339
|
+
}),
|
|
1340
|
+
ruleId: rule.id,
|
|
1341
|
+
title: rule.title,
|
|
1342
|
+
description: rule.description,
|
|
1343
|
+
severity: rule.severity,
|
|
1344
|
+
confidence: rule.confidence,
|
|
1345
|
+
category: rule.category,
|
|
1346
|
+
evidence,
|
|
1347
|
+
remediation: {
|
|
1348
|
+
recommendedFix: rule.recommended_fix,
|
|
1349
|
+
patch: rule.patch
|
|
1350
|
+
},
|
|
1351
|
+
links: rule.links,
|
|
1352
|
+
fingerprint
|
|
1353
|
+
};
|
|
1354
|
+
findings.push(finding);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return findings;
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function createScannersFromRules(rules) {
|
|
1363
|
+
return rules.map(createScannerFromRule);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
659
1366
|
// src/scanners/types.ts
|
|
660
1367
|
var SEVERITY_ORDER = {
|
|
661
1368
|
critical: 4,
|
|
@@ -1626,6 +2333,61 @@ async function buildScanContext(repoRoot, options = {}) {
|
|
|
1626
2333
|
};
|
|
1627
2334
|
}
|
|
1628
2335
|
|
|
2336
|
+
// src/scanners/helpers/patch-generator.ts
|
|
2337
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2338
|
+
function generateFunctionStartPatch(repoRoot, relPath, functionStartLine, codeToInsert, contextLines = 3) {
|
|
2339
|
+
try {
|
|
2340
|
+
const absPath = resolvePath(repoRoot, relPath);
|
|
2341
|
+
const content = readFileSync4(absPath, "utf-8");
|
|
2342
|
+
const lines = content.split("\n");
|
|
2343
|
+
let bodyStartLine = functionStartLine;
|
|
2344
|
+
for (let i = functionStartLine - 1; i < lines.length; i++) {
|
|
2345
|
+
if (lines[i].includes("{")) {
|
|
2346
|
+
bodyStartLine = i + 1;
|
|
2347
|
+
break;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const contextBefore = lines.slice(
|
|
2351
|
+
Math.max(0, bodyStartLine - contextLines),
|
|
2352
|
+
bodyStartLine
|
|
2353
|
+
);
|
|
2354
|
+
const contextAfter = lines.slice(
|
|
2355
|
+
bodyStartLine,
|
|
2356
|
+
Math.min(lines.length, bodyStartLine + contextLines)
|
|
2357
|
+
);
|
|
2358
|
+
const firstLineAfterBrace = lines[bodyStartLine];
|
|
2359
|
+
const indentation = firstLineAfterBrace?.match(/^(\s*)/)?.[1] || " ";
|
|
2360
|
+
const insertLines = codeToInsert.split("\n").map((line) => {
|
|
2361
|
+
if (line.trim() === "") return "";
|
|
2362
|
+
return indentation + line;
|
|
2363
|
+
});
|
|
2364
|
+
const oldStartLine = bodyStartLine - contextBefore.length + 1;
|
|
2365
|
+
const oldLineCount = contextBefore.length + contextAfter.length;
|
|
2366
|
+
const newLineCount = oldLineCount + insertLines.length;
|
|
2367
|
+
const diffLines = [];
|
|
2368
|
+
diffLines.push(`--- a/${relPath.replace(/\\/g, "/")}`);
|
|
2369
|
+
diffLines.push(`+++ b/${relPath.replace(/\\/g, "/")}`);
|
|
2370
|
+
diffLines.push(
|
|
2371
|
+
`@@ -${oldStartLine},${oldLineCount} +${oldStartLine},${newLineCount} @@`
|
|
2372
|
+
);
|
|
2373
|
+
for (const line of contextBefore) {
|
|
2374
|
+
diffLines.push(" " + line);
|
|
2375
|
+
}
|
|
2376
|
+
for (const line of insertLines) {
|
|
2377
|
+
diffLines.push("+" + line);
|
|
2378
|
+
}
|
|
2379
|
+
if (contextAfter.length > 0 && contextAfter[0].trim() !== "") {
|
|
2380
|
+
diffLines.push("+");
|
|
2381
|
+
}
|
|
2382
|
+
for (const line of contextAfter) {
|
|
2383
|
+
diffLines.push(" " + line);
|
|
2384
|
+
}
|
|
2385
|
+
return diffLines.join("\n");
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
return "";
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
1629
2391
|
// src/scanners/auth/unprotected-api-route.ts
|
|
1630
2392
|
var RULE_ID = "VC-AUTH-001";
|
|
1631
2393
|
var STATE_CHANGING_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
|
|
@@ -1689,6 +2451,19 @@ async function scanUnprotectedApiRoutes(context) {
|
|
|
1689
2451
|
route: routePath
|
|
1690
2452
|
});
|
|
1691
2453
|
const sinkOperations = sinks.map((s) => `${s.kind}.${s.operation}`).join(", ");
|
|
2454
|
+
const authCheckCode = `const session = await getServerSession(authOptions);
|
|
2455
|
+
if (!session) {
|
|
2456
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
2457
|
+
status: 401,
|
|
2458
|
+
headers: { "Content-Type": "application/json" }
|
|
2459
|
+
});
|
|
2460
|
+
}`;
|
|
2461
|
+
const patch = generateFunctionStartPatch(
|
|
2462
|
+
repoRoot,
|
|
2463
|
+
relPath,
|
|
2464
|
+
handler.startLine,
|
|
2465
|
+
authCheckCode
|
|
2466
|
+
);
|
|
1692
2467
|
findings.push({
|
|
1693
2468
|
id: generateFindingId({
|
|
1694
2469
|
ruleId: RULE_ID,
|
|
@@ -1704,14 +2479,8 @@ async function scanUnprotectedApiRoutes(context) {
|
|
|
1704
2479
|
evidence,
|
|
1705
2480
|
remediation: {
|
|
1706
2481
|
recommendedFix: `Add authentication to the ${handler.method} handler. Check for a valid session using getServerSession(), auth(), or similar before performing database operations.`,
|
|
1707
|
-
patch:
|
|
1708
|
-
|
|
1709
|
-
if (!session) {
|
|
1710
|
-
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
1711
|
-
status: 401,
|
|
1712
|
-
headers: { "Content-Type": "application/json" }
|
|
1713
|
-
});
|
|
1714
|
-
}`
|
|
2482
|
+
patch: patch || void 0
|
|
2483
|
+
// Only include patch if generation succeeded
|
|
1715
2484
|
},
|
|
1716
2485
|
links: {
|
|
1717
2486
|
owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/",
|
|
@@ -1814,19 +2583,8 @@ async function scanMiddlewareGap(context) {
|
|
|
1814
2583
|
description: `This Next.js project uses next-auth but has no middleware.ts file. API routes (${fileIndex.apiRouteFiles.length} found) may lack server-side authentication enforcement. While next-auth provides session management, middleware is recommended for edge-level protection.`,
|
|
1815
2584
|
evidence,
|
|
1816
2585
|
remediation: {
|
|
1817
|
-
recommendedFix: "Create a middleware.ts file that checks authentication for protected routes. See: https://next-auth.js.org/configuration/nextjs#middleware"
|
|
1818
|
-
patch
|
|
1819
|
-
import { withAuth } from "next-auth/middleware";
|
|
1820
|
-
|
|
1821
|
-
export default withAuth({
|
|
1822
|
-
callbacks: {
|
|
1823
|
-
authorized: ({ token }) => !!token,
|
|
1824
|
-
},
|
|
1825
|
-
});
|
|
1826
|
-
|
|
1827
|
-
export const config = {
|
|
1828
|
-
matcher: ["/api/:path*", "/dashboard/:path*"],
|
|
1829
|
-
};`
|
|
2586
|
+
recommendedFix: "Create a middleware.ts file that checks authentication for protected routes. Use next-auth's withAuth helper with a matcher config for /api/:path* and other protected routes. See: https://next-auth.js.org/configuration/nextjs#middleware"
|
|
2587
|
+
// No patch for file creation - apply-patches only handles modifications to existing files
|
|
1830
2588
|
},
|
|
1831
2589
|
links: {
|
|
1832
2590
|
owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
|
|
@@ -1888,6 +2646,7 @@ export const config = {
|
|
|
1888
2646
|
evidence,
|
|
1889
2647
|
remediation: {
|
|
1890
2648
|
recommendedFix: `Update the middleware matcher to include API routes. Example: matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/api/:path*']`
|
|
2649
|
+
// No patch for middleware matcher updates - requires understanding the full matcher pattern and what should be included/excluded
|
|
1891
2650
|
},
|
|
1892
2651
|
links: {
|
|
1893
2652
|
owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
|
|
@@ -1949,14 +2708,8 @@ async function scanIgnoredValidation(context) {
|
|
|
1949
2708
|
category: "validation",
|
|
1950
2709
|
evidence,
|
|
1951
2710
|
remediation: {
|
|
1952
|
-
recommendedFix: `Assign the validation result to a variable and use the validated data instead of the raw input
|
|
1953
|
-
patch
|
|
1954
|
-
schema.parse(data);
|
|
1955
|
-
|
|
1956
|
-
// Do:
|
|
1957
|
-
const validatedData = schema.parse(data);
|
|
1958
|
-
// Use validatedData for all subsequent operations` : `// Assign and use the validated result
|
|
1959
|
-
const validatedData = await schema.validate(data);`
|
|
2711
|
+
recommendedFix: `Assign the validation result to a variable and use the validated data instead of the raw input. For Zod: const validatedData = schema.parse(data). For Yup/Joi: const validatedData = await schema.validate(data). Then use validatedData for all operations.`
|
|
2712
|
+
// No patch for validation fixes - requires knowing actual variable names and how to replace raw body usage
|
|
1960
2713
|
},
|
|
1961
2714
|
links: {
|
|
1962
2715
|
cwe: "https://cwe.mitre.org/data/definitions/20.html"
|
|
@@ -2080,35 +2833,8 @@ async function scanClientSideOnlyValidation(context) {
|
|
|
2080
2833
|
category: "validation",
|
|
2081
2834
|
evidence,
|
|
2082
2835
|
remediation: {
|
|
2083
|
-
recommendedFix: `Add server-side validation using the same schema. Consider sharing schemas between frontend and backend
|
|
2084
|
-
patch
|
|
2085
|
-
// lib/schemas/user.ts
|
|
2086
|
-
import { z } from "zod";
|
|
2087
|
-
|
|
2088
|
-
export const createUserSchema = z.object({
|
|
2089
|
-
name: z.string().min(1),
|
|
2090
|
-
email: z.string().email(),
|
|
2091
|
-
});
|
|
2092
|
-
|
|
2093
|
-
// In your API route:
|
|
2094
|
-
import { createUserSchema } from "@/lib/schemas/user";
|
|
2095
|
-
|
|
2096
|
-
export async function POST(request: Request) {
|
|
2097
|
-
const body = await request.json();
|
|
2098
|
-
|
|
2099
|
-
// Server-side validation
|
|
2100
|
-
const result = createUserSchema.safeParse(body);
|
|
2101
|
-
if (!result.success) {
|
|
2102
|
-
return Response.json(
|
|
2103
|
-
{ error: result.error.flatten() },
|
|
2104
|
-
{ status: 400 }
|
|
2105
|
-
);
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// Use validated data
|
|
2109
|
-
const { name, email } = result.data;
|
|
2110
|
-
// ...
|
|
2111
|
-
}`
|
|
2836
|
+
recommendedFix: `Add server-side validation using the same schema. Consider sharing schemas between frontend and backend. Example with Zod: const result = schema.safeParse(body); if (!result.success) return Response.json({ error: result.error }, { status: 400 }); then use result.data for validated values.`
|
|
2837
|
+
// No patch for validation addition - requires knowing the validation schema structure and which fields to validate
|
|
2112
2838
|
},
|
|
2113
2839
|
links: {
|
|
2114
2840
|
cwe: "https://cwe.mitre.org/data/definitions/20.html",
|
|
@@ -2178,12 +2904,8 @@ async function scanSensitiveLogging(context) {
|
|
|
2178
2904
|
category: "privacy",
|
|
2179
2905
|
evidence,
|
|
2180
2906
|
remediation: {
|
|
2181
|
-
recommendedFix: `Remove sensitive data from log statements. Only log non-sensitive identifiers like user IDs, timestamps, or action types
|
|
2182
|
-
patch
|
|
2183
|
-
console.log("Login:", { email, password });
|
|
2184
|
-
|
|
2185
|
-
// Do:
|
|
2186
|
-
console.log("Login attempt:", { email, timestamp: Date.now() });`
|
|
2907
|
+
recommendedFix: `Remove sensitive data from log statements. Only log non-sensitive identifiers like user IDs, timestamps, or action types. Never log passwords, tokens, auth headers, secrets, or API keys.`
|
|
2908
|
+
// No patch for sensitive logging - requires understanding which data is needed for debugging vs which should be redacted
|
|
2187
2909
|
},
|
|
2188
2910
|
links: {
|
|
2189
2911
|
cwe: "https://cwe.mitre.org/data/definitions/532.html",
|
|
@@ -2260,20 +2982,8 @@ async function scanOverBroadResponse(context) {
|
|
|
2260
2982
|
category: "privacy",
|
|
2261
2983
|
evidence,
|
|
2262
2984
|
remediation: {
|
|
2263
|
-
recommendedFix: `Use Prisma's \`select\` clause to explicitly choose which fields to return. Never expose password hashes, tokens, or other sensitive data in API responses
|
|
2264
|
-
patch
|
|
2265
|
-
const users = await prisma.${query.model.toLowerCase()}.${query.operation}();
|
|
2266
|
-
|
|
2267
|
-
// Use select to return only needed fields:
|
|
2268
|
-
const users = await prisma.${query.model.toLowerCase()}.${query.operation}({
|
|
2269
|
-
select: {
|
|
2270
|
-
id: true,
|
|
2271
|
-
name: true,
|
|
2272
|
-
email: true,
|
|
2273
|
-
createdAt: true,
|
|
2274
|
-
// Explicitly omit: password, passwordHash, tokens, etc.
|
|
2275
|
-
},
|
|
2276
|
-
});`
|
|
2985
|
+
recommendedFix: `Use Prisma's \`select\` clause to explicitly choose which fields to return. Never expose password hashes, tokens, or other sensitive data in API responses. Example: prisma.user.findMany({ select: { id: true, name: true, email: true } }) - only include fields the API consumer needs.`
|
|
2986
|
+
// No patch for over-broad responses - requires understanding which fields are needed by the API consumer
|
|
2277
2987
|
},
|
|
2278
2988
|
links: {
|
|
2279
2989
|
cwe: "https://cwe.mitre.org/data/definitions/359.html",
|
|
@@ -2371,17 +3081,8 @@ async function scanDebugFlags(context) {
|
|
|
2371
3081
|
category: "config",
|
|
2372
3082
|
evidence,
|
|
2373
3083
|
remediation: {
|
|
2374
|
-
recommendedFix: `Ensure debug flags are only enabled in development. Use environment variables or NODE_ENV checks
|
|
2375
|
-
patch
|
|
2376
|
-
const isDev = process.env.NODE_ENV === 'development';
|
|
2377
|
-
|
|
2378
|
-
module.exports = {
|
|
2379
|
-
// Only enable in development
|
|
2380
|
-
...(isDev && { debug: true }),
|
|
2381
|
-
|
|
2382
|
-
// Or use environment variable:
|
|
2383
|
-
debug: process.env.DEBUG === 'true',
|
|
2384
|
-
};`
|
|
3084
|
+
recommendedFix: `Ensure debug flags are only enabled in development. Use environment variables or NODE_ENV checks. Example: ...(process.env.NODE_ENV === 'development' && { debug: true }) or debug: process.env.DEBUG === 'true'.`
|
|
3085
|
+
// No patch for debug flag fixes - requires understanding config structure and which flags to guard
|
|
2385
3086
|
},
|
|
2386
3087
|
links: {
|
|
2387
3088
|
cwe: "https://cwe.mitre.org/data/definitions/489.html",
|
|
@@ -2631,30 +3332,8 @@ async function scanSsrfProneFetch(context) {
|
|
|
2631
3332
|
category: "network",
|
|
2632
3333
|
evidence,
|
|
2633
3334
|
remediation: {
|
|
2634
|
-
recommendedFix: `Validate and sanitize the URL before use. Use an allowlist of permitted domains or URL patterns.
|
|
2635
|
-
patch
|
|
2636
|
-
const url = new URL(userProvidedUrl);
|
|
2637
|
-
|
|
2638
|
-
// Check against allowlist
|
|
2639
|
-
const allowedHosts = ["api.example.com", "cdn.example.com"];
|
|
2640
|
-
if (!allowedHosts.includes(url.hostname)) {
|
|
2641
|
-
throw new Error("URL not allowed");
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
// Block internal addresses
|
|
2645
|
-
const blockedPatterns = [
|
|
2646
|
-
/^localhost$/i,
|
|
2647
|
-
/^127\\.\\d+\\.\\d+\\.\\d+$/,
|
|
2648
|
-
/^10\\.\\d+\\.\\d+\\.\\d+$/,
|
|
2649
|
-
/^172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d+\\.\\d+$/,
|
|
2650
|
-
/^192\\.168\\.\\d+\\.\\d+$/,
|
|
2651
|
-
/^169\\.254\\.169\\.254$/, // AWS metadata
|
|
2652
|
-
];
|
|
2653
|
-
if (blockedPatterns.some(p => p.test(url.hostname))) {
|
|
2654
|
-
throw new Error("URL not allowed");
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
const response = await fetch(url.toString());`
|
|
3335
|
+
recommendedFix: `Validate and sanitize the URL before use. Use an allowlist of permitted domains or URL patterns. Block requests to internal networks (10.x.x.x, 172.16-31.x.x, 192.168.x.x), localhost (127.x.x.x), and cloud metadata endpoints (169.254.169.254). Parse with new URL() and validate url.hostname.`
|
|
3336
|
+
// No patch for SSRF validation - requires defining application-specific allowlist of permitted domains
|
|
2658
3337
|
},
|
|
2659
3338
|
links: {
|
|
2660
3339
|
cwe: "https://cwe.mitre.org/data/definitions/918.html",
|
|
@@ -2713,22 +3392,8 @@ async function scanOpenRedirect(context) {
|
|
|
2713
3392
|
category: "network",
|
|
2714
3393
|
evidence,
|
|
2715
3394
|
remediation: {
|
|
2716
|
-
recommendedFix: `Validate the redirect URL against an allowlist of permitted paths or domains. Never redirect to arbitrary user-provided URLs without validation
|
|
2717
|
-
patch
|
|
2718
|
-
const allowedPaths = ['/dashboard', '/profile', '/settings'];
|
|
2719
|
-
const redirectUrl = searchParams.get('next') || '/';
|
|
2720
|
-
|
|
2721
|
-
// Option 1: Only allow relative paths
|
|
2722
|
-
if (!redirectUrl.startsWith('/') || redirectUrl.startsWith('//')) {
|
|
2723
|
-
return NextResponse.redirect(new URL('/', request.url));
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
// Option 2: Allowlist check
|
|
2727
|
-
if (!allowedPaths.some(path => redirectUrl.startsWith(path))) {
|
|
2728
|
-
return NextResponse.redirect(new URL('/', request.url));
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
return NextResponse.redirect(new URL(redirectUrl, request.url));`
|
|
3395
|
+
recommendedFix: `Validate the redirect URL against an allowlist of permitted paths or domains. Never redirect to arbitrary user-provided URLs without validation. Options: (1) Only allow relative paths: check !url.startsWith('/') || url.startsWith('//'), (2) Use allowlist: check allowedPaths.some(path => url.startsWith(path)).`
|
|
3396
|
+
// No patch for redirect validation - requires defining application-specific allowlist of permitted paths/domains
|
|
2732
3397
|
},
|
|
2733
3398
|
links: {
|
|
2734
3399
|
cwe: "https://cwe.mitre.org/data/definitions/601.html",
|
|
@@ -2786,26 +3451,8 @@ async function scanCorsMisconfiguration(context) {
|
|
|
2786
3451
|
category: "network",
|
|
2787
3452
|
evidence,
|
|
2788
3453
|
remediation: {
|
|
2789
|
-
recommendedFix: `When using credentials, you must specify explicit allowed origins instead of "*". Use an allowlist of trusted domains
|
|
2790
|
-
patch
|
|
2791
|
-
// cors({ origin: "*", credentials: true })
|
|
2792
|
-
|
|
2793
|
-
// Use an allowlist:
|
|
2794
|
-
const allowedOrigins = [
|
|
2795
|
-
'https://app.example.com',
|
|
2796
|
-
'https://admin.example.com',
|
|
2797
|
-
];
|
|
2798
|
-
|
|
2799
|
-
app.use(cors({
|
|
2800
|
-
origin: (origin, callback) => {
|
|
2801
|
-
if (!origin || allowedOrigins.includes(origin)) {
|
|
2802
|
-
callback(null, true);
|
|
2803
|
-
} else {
|
|
2804
|
-
callback(new Error('Not allowed by CORS'));
|
|
2805
|
-
}
|
|
2806
|
-
},
|
|
2807
|
-
credentials: true,
|
|
2808
|
-
}));`
|
|
3454
|
+
recommendedFix: `When using credentials, you must specify explicit allowed origins instead of "*". Use an allowlist of trusted domains. Example: cors({ origin: (origin, callback) => { if (allowedOrigins.includes(origin)) callback(null, true); else callback(new Error('Not allowed')); }, credentials: true })`
|
|
3455
|
+
// No patch for CORS fixes - requires defining application-specific allowlist of trusted origins
|
|
2809
3456
|
},
|
|
2810
3457
|
links: {
|
|
2811
3458
|
cwe: "https://cwe.mitre.org/data/definitions/942.html",
|
|
@@ -2862,28 +3509,8 @@ async function scanMissingTimeout(context) {
|
|
|
2862
3509
|
category: "network",
|
|
2863
3510
|
evidence,
|
|
2864
3511
|
remediation: {
|
|
2865
|
-
recommendedFix: `Add a timeout to prevent indefinite hangs. For fetch, use AbortController
|
|
2866
|
-
patch
|
|
2867
|
-
const controller = new AbortController();
|
|
2868
|
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
2869
|
-
|
|
2870
|
-
try {
|
|
2871
|
-
const response = await fetch(url, {
|
|
2872
|
-
signal: controller.signal,
|
|
2873
|
-
});
|
|
2874
|
-
clearTimeout(timeoutId);
|
|
2875
|
-
return response;
|
|
2876
|
-
} catch (error) {
|
|
2877
|
-
if (error.name === 'AbortError') {
|
|
2878
|
-
throw new Error('Request timed out');
|
|
2879
|
-
}
|
|
2880
|
-
throw error;
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
// For axios:
|
|
2884
|
-
const response = await axios.get(url, {
|
|
2885
|
-
timeout: 5000,
|
|
2886
|
-
});`
|
|
3512
|
+
recommendedFix: `Add a timeout to prevent indefinite hangs. For fetch, use AbortController with setTimeout(() => controller.abort(), 5000) and handle AbortError. For axios, add timeout: 5000 to options. Choose timeout duration based on expected response times.`
|
|
3513
|
+
// No patch for timeout fixes - implementation varies by method (fetch vs axios) and requires choosing appropriate timeout duration
|
|
2887
3514
|
},
|
|
2888
3515
|
links: {
|
|
2889
3516
|
cwe: "https://cwe.mitre.org/data/definitions/400.html"
|
|
@@ -3153,19 +3780,8 @@ async function scanNextAuthNotEnforced(context) {
|
|
|
3153
3780
|
category: "auth",
|
|
3154
3781
|
evidence,
|
|
3155
3782
|
remediation: {
|
|
3156
|
-
recommendedFix: "Add authentication middleware or explicit auth checks to API routes. See https://next-auth.js.org/configuration/nextjs"
|
|
3157
|
-
patch
|
|
3158
|
-
import { withAuth } from "next-auth/middleware";
|
|
3159
|
-
|
|
3160
|
-
export default withAuth({
|
|
3161
|
-
callbacks: {
|
|
3162
|
-
authorized: ({ token }) => !!token,
|
|
3163
|
-
},
|
|
3164
|
-
});
|
|
3165
|
-
|
|
3166
|
-
export const config = {
|
|
3167
|
-
matcher: ["/api/:path*"],
|
|
3168
|
-
};`
|
|
3783
|
+
recommendedFix: "Add authentication middleware or explicit auth checks to API routes. Create middleware.ts using next-auth's withAuth helper with matcher for /api/:path*. See https://next-auth.js.org/configuration/nextjs"
|
|
3784
|
+
// No patch for file creation - apply-patches only handles modifications to existing files
|
|
3169
3785
|
},
|
|
3170
3786
|
links: {
|
|
3171
3787
|
owasp: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
|
|
@@ -3257,33 +3873,8 @@ async function scanMissingRateLimit(context) {
|
|
|
3257
3873
|
category: "middleware",
|
|
3258
3874
|
evidence,
|
|
3259
3875
|
remediation: {
|
|
3260
|
-
recommendedFix: `Add rate limiting to protect against abuse.
|
|
3261
|
-
patch
|
|
3262
|
-
import { Ratelimit } from "@upstash/ratelimit";
|
|
3263
|
-
import { kv } from "@vercel/kv";
|
|
3264
|
-
|
|
3265
|
-
const ratelimit = new Ratelimit({
|
|
3266
|
-
redis: kv,
|
|
3267
|
-
limiter: Ratelimit.slidingWindow(10, "60 s"), // 10 requests per minute
|
|
3268
|
-
});
|
|
3269
|
-
|
|
3270
|
-
export async function POST(request: Request) {
|
|
3271
|
-
const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
|
|
3272
|
-
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
|
|
3273
|
-
|
|
3274
|
-
if (!success) {
|
|
3275
|
-
return new Response("Too Many Requests", {
|
|
3276
|
-
status: 429,
|
|
3277
|
-
headers: {
|
|
3278
|
-
"X-RateLimit-Limit": limit.toString(),
|
|
3279
|
-
"X-RateLimit-Remaining": remaining.toString(),
|
|
3280
|
-
"X-RateLimit-Reset": reset.toString(),
|
|
3281
|
-
},
|
|
3282
|
-
});
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
|
-
// ... rest of handler
|
|
3286
|
-
}`
|
|
3876
|
+
recommendedFix: `Add rate limiting to protect against abuse. For serverless: use @upstash/ratelimit with Ratelimit.slidingWindow(10, "60 s") and check success before proceeding. For Express: use express-rate-limit middleware. Limit by IP address or user ID.`
|
|
3877
|
+
// No patch for rate limiting - implementation varies by infrastructure (serverless vs traditional) and requires setup (Redis connection, identifier strategy)
|
|
3287
3878
|
},
|
|
3288
3879
|
links: {
|
|
3289
3880
|
owasp: "https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html",
|
|
@@ -3345,21 +3936,8 @@ async function scanMathRandomTokens(context) {
|
|
|
3345
3936
|
category: "crypto",
|
|
3346
3937
|
evidence,
|
|
3347
3938
|
remediation: {
|
|
3348
|
-
recommendedFix: `Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values
|
|
3349
|
-
patch
|
|
3350
|
-
|
|
3351
|
-
// For tokens/keys (hex string):
|
|
3352
|
-
const token = randomBytes(32).toString('hex');
|
|
3353
|
-
|
|
3354
|
-
// For session IDs (URL-safe base64):
|
|
3355
|
-
const sessionId = randomBytes(24).toString('base64url');
|
|
3356
|
-
|
|
3357
|
-
// For UUIDs:
|
|
3358
|
-
const id = randomUUID();
|
|
3359
|
-
|
|
3360
|
-
// For numbers in a range (e.g., 6-digit code):
|
|
3361
|
-
const code = randomBytes(4).readUInt32BE() % 1000000;
|
|
3362
|
-
const resetCode = code.toString().padStart(6, '0');`
|
|
3939
|
+
recommendedFix: `Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values. For tokens/keys: randomBytes(32).toString('hex'). For session IDs: randomBytes(24).toString('base64url'). For UUIDs: randomUUID(). For numeric codes: randomBytes(4).readUInt32BE() % range.`
|
|
3940
|
+
// No patch for crypto random fixes - the correct replacement depends on the specific use case
|
|
3363
3941
|
},
|
|
3364
3942
|
links: {
|
|
3365
3943
|
cwe: "https://cwe.mitre.org/data/definitions/338.html",
|
|
@@ -3414,26 +3992,8 @@ async function scanJwtDecodeUnverified(context) {
|
|
|
3414
3992
|
category: "crypto",
|
|
3415
3993
|
evidence,
|
|
3416
3994
|
remediation: {
|
|
3417
|
-
recommendedFix: `Use jwt.verify() instead of jwt.decode() to ensure the token signature is valid
|
|
3418
|
-
patch
|
|
3419
|
-
|
|
3420
|
-
// WRONG - doesn't verify signature:
|
|
3421
|
-
// const payload = jwt.decode(token);
|
|
3422
|
-
|
|
3423
|
-
// CORRECT - verifies signature:
|
|
3424
|
-
try {
|
|
3425
|
-
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
|
3426
|
-
// Token is valid, payload can be trusted
|
|
3427
|
-
} catch (error) {
|
|
3428
|
-
// Token is invalid or expired
|
|
3429
|
-
throw new Error('Invalid token');
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
// If you need to read claims before verification (e.g., to get kid for key lookup):
|
|
3433
|
-
const header = jwt.decode(token, { complete: true })?.header;
|
|
3434
|
-
const kid = header?.kid;
|
|
3435
|
-
// ... look up the correct key ...
|
|
3436
|
-
const payload = jwt.verify(token, key); // MUST verify before trusting`
|
|
3995
|
+
recommendedFix: `Use jwt.verify() instead of jwt.decode() to ensure the token signature is valid. Example: jwt.verify(token, process.env.JWT_SECRET) with proper error handling. If you need to read claims before verification (e.g., to get kid for key lookup), decode the header first, look up the correct key, then verify with that key.`
|
|
3996
|
+
// No patch for JWT fixes - requires knowing secret location and error handling context
|
|
3437
3997
|
},
|
|
3438
3998
|
links: {
|
|
3439
3999
|
cwe: "https://cwe.mitre.org/data/definitions/347.html",
|
|
@@ -3474,30 +4034,19 @@ async function scanWeakHashing(context) {
|
|
|
3474
4034
|
});
|
|
3475
4035
|
let title;
|
|
3476
4036
|
let description;
|
|
3477
|
-
let
|
|
4037
|
+
let recommendedFix;
|
|
3478
4038
|
if (usage.algorithm.startsWith("bcrypt")) {
|
|
3479
4039
|
title = "Bcrypt with insufficient salt rounds";
|
|
3480
4040
|
description = `Bcrypt is configured with low salt rounds (${usage.algorithm}). The cost factor should be at least 10 (ideally 12+) to provide adequate protection against brute-force attacks. Lower values make password cracking significantly faster.`;
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
// Use at least 10 salt rounds (12 recommended):
|
|
3484
|
-
const SALT_ROUNDS = 12;
|
|
3485
|
-
|
|
3486
|
-
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);`;
|
|
4041
|
+
recommendedFix = "Increase bcrypt salt rounds to at least 10 (12 recommended): await bcrypt.hash(password, 12)";
|
|
3487
4042
|
} else if (usage.algorithm === "md5" || usage.algorithm === "sha1") {
|
|
3488
4043
|
title = `Weak hash algorithm (${usage.algorithm.toUpperCase()}) ${usage.isPasswordContext ? "for password" : "detected"}`;
|
|
3489
4044
|
description = `${usage.algorithm.toUpperCase()} is cryptographically broken and should not be used for security purposes${usage.isPasswordContext ? ", especially for passwords" : ""}. MD5 can be brute-forced or attacked with rainbow tables in seconds. SHA1 has known collision vulnerabilities.`;
|
|
3490
|
-
|
|
3491
|
-
import bcrypt from 'bcrypt';
|
|
3492
|
-
const hashedPassword = await bcrypt.hash(password, 12);
|
|
3493
|
-
|
|
3494
|
-
// For non-password hashing (integrity checks, etc.):
|
|
3495
|
-
import { createHash } from 'crypto';
|
|
3496
|
-
const hash = createHash('sha256').update(data).digest('hex');`;
|
|
4045
|
+
recommendedFix = usage.isPasswordContext ? "For passwords, use bcrypt: await bcrypt.hash(password, 12)" : "For non-password hashing, use SHA-256: createHash('sha256').update(data).digest('hex')";
|
|
3497
4046
|
} else {
|
|
3498
4047
|
title = `Weak cryptographic configuration: ${usage.algorithm}`;
|
|
3499
4048
|
description = `The cryptographic configuration "${usage.algorithm}" is considered weak and should be updated to current standards.`;
|
|
3500
|
-
|
|
4049
|
+
recommendedFix = "Use modern, secure algorithms and configurations";
|
|
3501
4050
|
}
|
|
3502
4051
|
findings.push({
|
|
3503
4052
|
id: generateFindingId({
|
|
@@ -3514,8 +4063,8 @@ const hash = createHash('sha256').update(data).digest('hex');`;
|
|
|
3514
4063
|
category: "crypto",
|
|
3515
4064
|
evidence,
|
|
3516
4065
|
remediation: {
|
|
3517
|
-
recommendedFix
|
|
3518
|
-
patch
|
|
4066
|
+
recommendedFix
|
|
4067
|
+
// No patch for crypto changes - too context-dependent
|
|
3519
4068
|
},
|
|
3520
4069
|
links: {
|
|
3521
4070
|
cwe: "https://cwe.mitre.org/data/definitions/328.html",
|
|
@@ -3582,36 +4131,8 @@ async function scanMissingUploadConstraints(context) {
|
|
|
3582
4131
|
category: "uploads",
|
|
3583
4132
|
evidence,
|
|
3584
4133
|
remediation: {
|
|
3585
|
-
recommendedFix: `Add both size limits and file type validation to your upload handler
|
|
3586
|
-
patch
|
|
3587
|
-
const upload = multer({
|
|
3588
|
-
limits: {
|
|
3589
|
-
fileSize: 5 * 1024 * 1024, // 5MB limit
|
|
3590
|
-
files: 1, // Single file
|
|
3591
|
-
},
|
|
3592
|
-
fileFilter: (req, file, cb) => {
|
|
3593
|
-
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
3594
|
-
if (allowedTypes.includes(file.mimetype)) {
|
|
3595
|
-
cb(null, true);
|
|
3596
|
-
} else {
|
|
3597
|
-
cb(new Error('Invalid file type'));
|
|
3598
|
-
}
|
|
3599
|
-
},
|
|
3600
|
-
});` : `// For Next.js formData, validate the file:
|
|
3601
|
-
const formData = await request.formData();
|
|
3602
|
-
const file = formData.get('file') as File;
|
|
3603
|
-
|
|
3604
|
-
// Check file size
|
|
3605
|
-
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
3606
|
-
if (file.size > MAX_SIZE) {
|
|
3607
|
-
return Response.json({ error: 'File too large' }, { status: 400 });
|
|
3608
|
-
}
|
|
3609
|
-
|
|
3610
|
-
// Check file type
|
|
3611
|
-
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
3612
|
-
if (!allowedTypes.includes(file.type)) {
|
|
3613
|
-
return Response.json({ error: 'Invalid file type' }, { status: 400 });
|
|
3614
|
-
}`
|
|
4134
|
+
recommendedFix: `Add both size limits and file type validation to your upload handler. For multer: add limits.fileSize and fileFilter callback. For Next.js formData: check file.size and file.type. Define allowed MIME types based on your use case (images, documents, etc.) and appropriate size limits.`
|
|
4135
|
+
// No patch for upload constraints - requires knowing appropriate size limits and allowed file types for the specific use case
|
|
3615
4136
|
},
|
|
3616
4137
|
links: {
|
|
3617
4138
|
cwe: "https://cwe.mitre.org/data/definitions/434.html",
|
|
@@ -3668,29 +4189,8 @@ async function scanPublicUploadPath(context) {
|
|
|
3668
4189
|
category: "uploads",
|
|
3669
4190
|
evidence,
|
|
3670
4191
|
remediation: {
|
|
3671
|
-
recommendedFix: `Store uploads outside the public directory and serve them through a controlled API endpoint. Always sanitize and generate safe filenames
|
|
3672
|
-
patch
|
|
3673
|
-
// fs.writeFile(path.join('public', filename), buffer);
|
|
3674
|
-
|
|
3675
|
-
// DO: Store outside public with generated names
|
|
3676
|
-
import { randomUUID } from 'crypto';
|
|
3677
|
-
import path from 'path';
|
|
3678
|
-
|
|
3679
|
-
// Generate safe filename
|
|
3680
|
-
const ext = path.extname(originalFilename).toLowerCase();
|
|
3681
|
-
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
|
|
3682
|
-
if (!allowedExtensions.includes(ext)) {
|
|
3683
|
-
throw new Error('Invalid file extension');
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
const safeFilename = \`\${randomUUID()}\${ext}\`;
|
|
3687
|
-
const uploadPath = path.join(process.cwd(), 'uploads', safeFilename);
|
|
3688
|
-
|
|
3689
|
-
// Write to non-public directory
|
|
3690
|
-
await fs.writeFile(uploadPath, buffer);
|
|
3691
|
-
|
|
3692
|
-
// Serve through API route:
|
|
3693
|
-
// GET /api/files/[id] -> Stream file from uploads directory`
|
|
4192
|
+
recommendedFix: `Store uploads outside the public directory and serve them through a controlled API endpoint. Always sanitize and generate safe filenames using randomUUID() + validated extension. Store in a non-public directory (e.g., uploads/) and serve through an API route that handles authorization and content-type headers.`
|
|
4193
|
+
// No patch for upload path fixes - requires restructuring file handling, creating API routes, and implementing authorization logic
|
|
3694
4194
|
},
|
|
3695
4195
|
links: {
|
|
3696
4196
|
cwe: "https://cwe.mitre.org/data/definitions/434.html",
|
|
@@ -4048,59 +4548,25 @@ function generateDescription(match, missing, costMultiplier, routePath) {
|
|
|
4048
4548
|
return `This endpoint (${routePath}) performs ${categoryDescriptions[match.category]}. Without proper controls, attackers can abuse this endpoint causing significant financial damage or service degradation. Estimated cost amplification: ${costMultiplier}x per request. Missing enforcement: ${missingText}.`;
|
|
4049
4549
|
}
|
|
4050
4550
|
function generateRemediation(category, missing) {
|
|
4051
|
-
const
|
|
4551
|
+
const recommendations = [];
|
|
4052
4552
|
if (missing.includes("auth")) {
|
|
4053
|
-
|
|
4054
|
-
const session = await getServerSession(authOptions);
|
|
4055
|
-
if (!session) {
|
|
4056
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
4057
|
-
}`);
|
|
4553
|
+
recommendations.push("authentication (e.g., getServerSession with 401 for unauthorized)");
|
|
4058
4554
|
}
|
|
4059
4555
|
if (missing.includes("rate_limit")) {
|
|
4060
|
-
|
|
4061
|
-
import { Ratelimit } from "@upstash/ratelimit";
|
|
4062
|
-
const ratelimit = new Ratelimit({
|
|
4063
|
-
redis: kv,
|
|
4064
|
-
limiter: Ratelimit.slidingWindow(10, "60 s"),
|
|
4065
|
-
});
|
|
4066
|
-
const { success } = await ratelimit.limit(userId);
|
|
4067
|
-
if (!success) {
|
|
4068
|
-
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
4069
|
-
}`);
|
|
4556
|
+
recommendations.push("rate limiting (e.g., @upstash/ratelimit with sliding window)");
|
|
4070
4557
|
}
|
|
4071
4558
|
if (missing.includes("request_size_limit")) {
|
|
4072
|
-
|
|
4073
|
-
const body = await request.json();
|
|
4074
|
-
const size = JSON.stringify(body).length;
|
|
4075
|
-
if (size > 100 * 1024) { // 100KB limit
|
|
4076
|
-
return Response.json({ error: "Request too large" }, { status: 413 });
|
|
4077
|
-
}`);
|
|
4559
|
+
recommendations.push("request size limits (check JSON.stringify(body).length, reject if > threshold)");
|
|
4078
4560
|
}
|
|
4079
4561
|
if (missing.includes("timeout")) {
|
|
4080
|
-
|
|
4081
|
-
const controller = new AbortController();
|
|
4082
|
-
const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
|
4083
|
-
try {
|
|
4084
|
-
const result = await expensiveOperation({ signal: controller.signal });
|
|
4085
|
-
} finally {
|
|
4086
|
-
clearTimeout(timeout);
|
|
4087
|
-
}`);
|
|
4562
|
+
recommendations.push("timeout enforcement (use AbortController with setTimeout)");
|
|
4088
4563
|
}
|
|
4089
4564
|
if (missing.includes("input_validation")) {
|
|
4090
|
-
|
|
4091
|
-
import { z } from "zod";
|
|
4092
|
-
const schema = z.object({
|
|
4093
|
-
prompt: z.string().max(4000),
|
|
4094
|
-
model: z.enum(["gpt-4", "gpt-3.5-turbo"]),
|
|
4095
|
-
});
|
|
4096
|
-
const { success, data } = schema.safeParse(body);
|
|
4097
|
-
if (!success) {
|
|
4098
|
-
return Response.json({ error: "Invalid input" }, { status: 400 });
|
|
4099
|
-
}`);
|
|
4565
|
+
recommendations.push("input validation (use Zod/Yup to validate and limit input size)");
|
|
4100
4566
|
}
|
|
4101
4567
|
return {
|
|
4102
|
-
recommendedFix: `Add the following enforcement controls to protect against compute abuse: ${
|
|
4103
|
-
patch
|
|
4568
|
+
recommendedFix: `Add the following enforcement controls to protect against compute abuse: ${recommendations.join("; ")}.`
|
|
4569
|
+
// No patch for compute abuse fixes - each requires different implementation based on the specific operation and infrastructure
|
|
4104
4570
|
};
|
|
4105
4571
|
}
|
|
4106
4572
|
|
|
@@ -6333,7 +6799,7 @@ var supplyChainPack = {
|
|
|
6333
6799
|
|
|
6334
6800
|
// src/phase3/proof-trace-builder.ts
|
|
6335
6801
|
import crypto3 from "crypto";
|
|
6336
|
-
import
|
|
6802
|
+
import path4 from "path";
|
|
6337
6803
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
6338
6804
|
var MAX_TRACE_DEPTH = 2;
|
|
6339
6805
|
function generateRouteId(routePath, method, file) {
|
|
@@ -6362,7 +6828,7 @@ function extractRoutePath6(routePart) {
|
|
|
6362
6828
|
function buildRouteMap(ctx) {
|
|
6363
6829
|
const routes = [];
|
|
6364
6830
|
for (const routeFile of ctx.fileIndex.routeFiles) {
|
|
6365
|
-
const absolutePath =
|
|
6831
|
+
const absolutePath = path4.join(ctx.repoRoot, routeFile);
|
|
6366
6832
|
const sourceFile = ctx.helpers.parseFile(absolutePath);
|
|
6367
6833
|
if (!sourceFile) continue;
|
|
6368
6834
|
const handlers = ctx.helpers.findRouteHandlers(sourceFile);
|
|
@@ -6387,7 +6853,7 @@ function buildMiddlewareMap(ctx) {
|
|
|
6387
6853
|
if (!ctx.fileIndex.middlewareFile) {
|
|
6388
6854
|
return middlewareList;
|
|
6389
6855
|
}
|
|
6390
|
-
const absolutePath =
|
|
6856
|
+
const absolutePath = path4.join(ctx.repoRoot, ctx.fileIndex.middlewareFile);
|
|
6391
6857
|
const sourceFile = ctx.helpers.parseFile(absolutePath);
|
|
6392
6858
|
if (!sourceFile) return middlewareList;
|
|
6393
6859
|
const relPath = ctx.fileIndex.middlewareFile.replace(/\\/g, "/");
|
|
@@ -6447,7 +6913,7 @@ function buildProofTrace(ctx, route) {
|
|
|
6447
6913
|
let authProven = false;
|
|
6448
6914
|
let validationProven = false;
|
|
6449
6915
|
const sourceFile = ctx.helpers.parseFile(
|
|
6450
|
-
|
|
6916
|
+
path4.join(ctx.repoRoot, route.file)
|
|
6451
6917
|
);
|
|
6452
6918
|
if (!sourceFile) {
|
|
6453
6919
|
return {
|
|
@@ -6534,13 +7000,13 @@ function buildProofTrace(ctx, route) {
|
|
|
6534
7000
|
}
|
|
6535
7001
|
function getLocalImports(sourceFile, repoRoot, currentFile) {
|
|
6536
7002
|
const imports = [];
|
|
6537
|
-
const currentDir =
|
|
7003
|
+
const currentDir = path4.dirname(path4.join(repoRoot, currentFile));
|
|
6538
7004
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
6539
7005
|
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
6540
7006
|
if (!moduleSpecifier.startsWith(".")) {
|
|
6541
7007
|
continue;
|
|
6542
7008
|
}
|
|
6543
|
-
let absolutePath =
|
|
7009
|
+
let absolutePath = path4.resolve(currentDir, moduleSpecifier);
|
|
6544
7010
|
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
6545
7011
|
let resolved = false;
|
|
6546
7012
|
for (const ext of extensions) {
|
|
@@ -6556,7 +7022,7 @@ function getLocalImports(sourceFile, repoRoot, currentFile) {
|
|
|
6556
7022
|
}
|
|
6557
7023
|
if (!resolved) {
|
|
6558
7024
|
for (const ext of extensions) {
|
|
6559
|
-
const indexPath =
|
|
7025
|
+
const indexPath = path4.join(absolutePath, `index${ext}`);
|
|
6560
7026
|
try {
|
|
6561
7027
|
if (sourceFile.getProject().getSourceFile(indexPath)) {
|
|
6562
7028
|
absolutePath = indexPath;
|
|
@@ -6596,7 +7062,7 @@ function traceImportedModule(ctx, importInfo, depth, needAuth, needValidation) {
|
|
|
6596
7062
|
if (!sourceFile) {
|
|
6597
7063
|
return result;
|
|
6598
7064
|
}
|
|
6599
|
-
const relPath =
|
|
7065
|
+
const relPath = path4.relative(ctx.repoRoot, importInfo.absolutePath).replace(/\\/g, "/");
|
|
6600
7066
|
if (needAuth) {
|
|
6601
7067
|
sourceFile.forEachDescendant((node) => {
|
|
6602
7068
|
if (result.authProven) return;
|
|
@@ -6701,7 +7167,7 @@ function buildAllProofTraces(ctx, routes) {
|
|
|
6701
7167
|
|
|
6702
7168
|
// src/phase3/intent-miner.ts
|
|
6703
7169
|
import crypto4 from "crypto";
|
|
6704
|
-
import
|
|
7170
|
+
import path5 from "path";
|
|
6705
7171
|
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
6706
7172
|
var INTENT_PATTERNS = [
|
|
6707
7173
|
// Auth patterns
|
|
@@ -6740,7 +7206,7 @@ function generateIntentId(type, file, line, evidence) {
|
|
|
6740
7206
|
function mineIntentClaims(ctx, sourceFile, routes) {
|
|
6741
7207
|
const claims = [];
|
|
6742
7208
|
const filePath = sourceFile.getFilePath();
|
|
6743
|
-
const relPath =
|
|
7209
|
+
const relPath = path5.relative(ctx.repoRoot, filePath).replace(/\\/g, "/");
|
|
6744
7210
|
claims.push(...mineFromComments(sourceFile, relPath, routes));
|
|
6745
7211
|
claims.push(...mineFromImports(sourceFile, relPath));
|
|
6746
7212
|
claims.push(...mineFromIdentifiers(sourceFile, relPath, routes));
|
|
@@ -6912,7 +7378,7 @@ function truncateEvidence(text) {
|
|
|
6912
7378
|
function mineAllIntentClaims(ctx, routes) {
|
|
6913
7379
|
const allClaims = [];
|
|
6914
7380
|
for (const file of ctx.fileIndex.allSourceFiles) {
|
|
6915
|
-
const absolutePath =
|
|
7381
|
+
const absolutePath = path5.join(ctx.repoRoot, file);
|
|
6916
7382
|
const sourceFile = ctx.helpers.parseFile(absolutePath);
|
|
6917
7383
|
if (!sourceFile) continue;
|
|
6918
7384
|
const claims = mineIntentClaims(ctx, sourceFile, routes);
|
|
@@ -7212,7 +7678,7 @@ function generateFingerprint3(route) {
|
|
|
7212
7678
|
|
|
7213
7679
|
// src/phase3/scanners/validation-claimed-missing.ts
|
|
7214
7680
|
import crypto7 from "crypto";
|
|
7215
|
-
import
|
|
7681
|
+
import path6 from "path";
|
|
7216
7682
|
var RULE_ID34 = "VC-HALL-012";
|
|
7217
7683
|
async function scanValidationClaimedMissing(ctx) {
|
|
7218
7684
|
const findings = [];
|
|
@@ -7229,7 +7695,7 @@ async function scanValidationClaimedMissing(ctx) {
|
|
|
7229
7695
|
claimsByFile.set(claim.location.file, existing);
|
|
7230
7696
|
}
|
|
7231
7697
|
for (const [file, claims] of claimsByFile) {
|
|
7232
|
-
const sourceFile = ctx.helpers.parseFile(
|
|
7698
|
+
const sourceFile = ctx.helpers.parseFile(path6.join(ctx.repoRoot, file));
|
|
7233
7699
|
if (!sourceFile) continue;
|
|
7234
7700
|
const fileRoutes = routes.filter((r) => r.file === file);
|
|
7235
7701
|
const handlers = ctx.helpers.findRouteHandlers(sourceFile);
|
|
@@ -7357,7 +7823,7 @@ function checkUnusedValidationImports(ctx, routes, claims) {
|
|
|
7357
7823
|
);
|
|
7358
7824
|
for (const claim of importClaims) {
|
|
7359
7825
|
const sourceFile = ctx.helpers.parseFile(
|
|
7360
|
-
|
|
7826
|
+
path6.join(ctx.repoRoot, claim.location.file)
|
|
7361
7827
|
);
|
|
7362
7828
|
if (!sourceFile) continue;
|
|
7363
7829
|
const hasSchemas = ctx.helpers.hasValidationSchemas(sourceFile);
|
|
@@ -7398,7 +7864,7 @@ function generateFingerprint4(route, issueType) {
|
|
|
7398
7864
|
|
|
7399
7865
|
// src/phase3/scanners/auth-by-ui-server-gap.ts
|
|
7400
7866
|
import crypto8 from "crypto";
|
|
7401
|
-
import
|
|
7867
|
+
import path7 from "path";
|
|
7402
7868
|
var RULE_ID35 = "VC-AUTH-010";
|
|
7403
7869
|
var CLIENT_AUTH_PATTERNS = [
|
|
7404
7870
|
// React/Next.js session hooks
|
|
@@ -7439,7 +7905,7 @@ async function scanAuthByUiServerGap(ctx) {
|
|
|
7439
7905
|
const sourceFile = ctx.helpers.parseFile(componentFile);
|
|
7440
7906
|
if (!sourceFile) continue;
|
|
7441
7907
|
const fullText = sourceFile.getFullText();
|
|
7442
|
-
const relPath =
|
|
7908
|
+
const relPath = path7.relative(ctx.repoRoot, componentFile).replace(/\\/g, "/");
|
|
7443
7909
|
const hasClientAuth = CLIENT_AUTH_PATTERNS.some((p) => p.test(fullText));
|
|
7444
7910
|
if (!hasClientAuth) continue;
|
|
7445
7911
|
const apiCalls = findApiCalls(fullText, relPath, sourceFile);
|
|
@@ -8210,7 +8676,7 @@ function sarifToJson(sarif, pretty = true) {
|
|
|
8210
8676
|
}
|
|
8211
8677
|
|
|
8212
8678
|
// src/utils/progress.ts
|
|
8213
|
-
import
|
|
8679
|
+
import path8 from "path";
|
|
8214
8680
|
var ANSI = {
|
|
8215
8681
|
reset: "\x1B[0m",
|
|
8216
8682
|
bold: "\x1B[1m",
|
|
@@ -8285,7 +8751,7 @@ var ScanProgress = class {
|
|
|
8285
8751
|
const packNum = `[${String(this.currentPack + 1).padStart(2)}/${this.packs.length}]`;
|
|
8286
8752
|
const filePercent = this.totalFiles > 0 ? Math.min(Math.round(this.filesProcessed / this.totalFiles * 100), 100) : 0;
|
|
8287
8753
|
const progressBar = createProgressBar(this.filesProcessed, this.totalFiles, 20);
|
|
8288
|
-
const fileName = this.currentFile ?
|
|
8754
|
+
const fileName = this.currentFile ? path8.basename(this.currentFile) : "";
|
|
8289
8755
|
const truncatedFile = truncateEnd(fileName, 28);
|
|
8290
8756
|
const statusText = `${ANSI.dim}${truncatedFile}${ANSI.reset} ${ANSI.cyan}${String(filePercent).padStart(3)}%${ANSI.reset}`;
|
|
8291
8757
|
const packName = truncateEnd(pack.name, 32);
|
|
@@ -8362,13 +8828,13 @@ function normalizePath(p) {
|
|
|
8362
8828
|
}
|
|
8363
8829
|
function getOutputPath(basePath, format, targetDir) {
|
|
8364
8830
|
let outputPath = basePath;
|
|
8365
|
-
if (!
|
|
8831
|
+
if (!path9.isAbsolute(outputPath)) {
|
|
8366
8832
|
outputPath = resolvePath(targetDir, outputPath);
|
|
8367
8833
|
}
|
|
8368
|
-
const ext =
|
|
8834
|
+
const ext = path9.extname(outputPath);
|
|
8369
8835
|
if (!ext) {
|
|
8370
8836
|
const filename = format === "sarif" ? "vibecheck-scan.sarif" : "vibecheck-scan.json";
|
|
8371
|
-
outputPath =
|
|
8837
|
+
outputPath = path9.join(outputPath, filename);
|
|
8372
8838
|
} else if (format === "sarif" && ext === ".json") {
|
|
8373
8839
|
outputPath = outputPath.replace(/\.json$/, ".sarif");
|
|
8374
8840
|
} else if (format === "json" && ext === ".sarif") {
|
|
@@ -8376,11 +8842,19 @@ function getOutputPath(basePath, format, targetDir) {
|
|
|
8376
8842
|
}
|
|
8377
8843
|
return outputPath;
|
|
8378
8844
|
}
|
|
8379
|
-
async function runScannersWithProgress(baseContext, totalFiles) {
|
|
8845
|
+
async function runScannersWithProgress(baseContext, totalFiles, customScanners = []) {
|
|
8380
8846
|
const allFindings = [];
|
|
8381
8847
|
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
8848
|
+
const packs = [...ALL_SCANNER_PACKS];
|
|
8849
|
+
if (customScanners.length > 0) {
|
|
8850
|
+
packs.push({
|
|
8851
|
+
id: "custom",
|
|
8852
|
+
name: "Custom Rules",
|
|
8853
|
+
scanners: customScanners
|
|
8854
|
+
});
|
|
8855
|
+
}
|
|
8382
8856
|
const progress = new ScanProgress(
|
|
8383
|
-
|
|
8857
|
+
packs.map((pack) => ({
|
|
8384
8858
|
id: pack.id,
|
|
8385
8859
|
name: pack.name,
|
|
8386
8860
|
scannerCount: pack.scanners.length
|
|
@@ -8405,8 +8879,8 @@ async function runScannersWithProgress(baseContext, totalFiles) {
|
|
|
8405
8879
|
prismaSchemaInfo: baseContext.prismaSchemaInfo
|
|
8406
8880
|
});
|
|
8407
8881
|
progress.start();
|
|
8408
|
-
for (let packIndex = 0; packIndex <
|
|
8409
|
-
const pack =
|
|
8882
|
+
for (let packIndex = 0; packIndex < packs.length; packIndex++) {
|
|
8883
|
+
const pack = packs[packIndex];
|
|
8410
8884
|
progress.startPack(packIndex);
|
|
8411
8885
|
for (let scannerIndex = 0; scannerIndex < pack.scanners.length; scannerIndex++) {
|
|
8412
8886
|
const scanner = pack.scanners[scannerIndex];
|
|
@@ -8588,7 +9062,31 @@ async function executeScan(targetDir, options) {
|
|
|
8588
9062
|
console.log(`\x1B[90m \u251C\u2500 API routes: ${initialContext.fileIndex.apiRouteFiles.length}\x1B[0m`);
|
|
8589
9063
|
console.log(`\x1B[90m \u251C\u2500 Config files: ${initialContext.fileIndex.configFiles.length}\x1B[0m`);
|
|
8590
9064
|
console.log(`\x1B[90m \u2514\u2500 Framework: ${initialContext.repoMeta.framework}\x1B[0m`);
|
|
8591
|
-
|
|
9065
|
+
let customScanners = [];
|
|
9066
|
+
if (options.rules) {
|
|
9067
|
+
const rulesSpinner = new Spinner("Loading custom rules");
|
|
9068
|
+
rulesSpinner.start();
|
|
9069
|
+
try {
|
|
9070
|
+
const customRules = loadRulesFromDirectory(options.rules);
|
|
9071
|
+
const validation = validateCustomRules(customRules);
|
|
9072
|
+
if (validation.errors.length > 0) {
|
|
9073
|
+
rulesSpinner.fail("Failed to validate custom rules");
|
|
9074
|
+
console.error("\n\x1B[31mCustom rule validation errors:\x1B[0m");
|
|
9075
|
+
for (const error of validation.errors) {
|
|
9076
|
+
console.error(` [${error.ruleId}] ${error.error}`);
|
|
9077
|
+
}
|
|
9078
|
+
return 1;
|
|
9079
|
+
}
|
|
9080
|
+
customScanners = createScannersFromRules(validation.valid);
|
|
9081
|
+
rulesSpinner.succeed(`Loaded ${validation.valid.length} custom rule(s)`);
|
|
9082
|
+
printRulesSummary(validation.valid);
|
|
9083
|
+
} catch (error) {
|
|
9084
|
+
rulesSpinner.fail("Failed to load custom rules");
|
|
9085
|
+
console.error(`\x1B[31m${error instanceof Error ? error.message : String(error)}\x1B[0m`);
|
|
9086
|
+
return 1;
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
const findings = await runScannersWithProgress(initialContext, totalFiles, customScanners);
|
|
8592
9090
|
const endTime = Date.now();
|
|
8593
9091
|
const scanDurationMs = endTime - startTime;
|
|
8594
9092
|
let phase3Data;
|
|
@@ -8712,13 +9210,13 @@ async function executeScan(targetDir, options) {
|
|
|
8712
9210
|
const outputFiles = [];
|
|
8713
9211
|
if (format === "json" || format === "both") {
|
|
8714
9212
|
const jsonPath = getOutputPath(options.out, "json", absoluteTarget);
|
|
8715
|
-
ensureDir(
|
|
9213
|
+
ensureDir(path9.dirname(jsonPath));
|
|
8716
9214
|
writeFileSync(jsonPath, JSON.stringify(artifact, null, 2));
|
|
8717
9215
|
outputFiles.push(jsonPath);
|
|
8718
9216
|
}
|
|
8719
9217
|
if (format === "sarif" || format === "both") {
|
|
8720
9218
|
const sarifPath = getOutputPath(options.out, "sarif", absoluteTarget);
|
|
8721
|
-
ensureDir(
|
|
9219
|
+
ensureDir(path9.dirname(sarifPath));
|
|
8722
9220
|
const sarifLog = toSarif(artifact);
|
|
8723
9221
|
writeFileSync(sarifPath, sarifToJson(sarifLog));
|
|
8724
9222
|
outputFiles.push(sarifPath);
|
|
@@ -8730,6 +9228,39 @@ async function executeScan(targetDir, options) {
|
|
|
8730
9228
|
}
|
|
8731
9229
|
}
|
|
8732
9230
|
printSummary(artifact, options);
|
|
9231
|
+
if (options.applyFixes) {
|
|
9232
|
+
console.log("\n" + "=".repeat(60));
|
|
9233
|
+
console.log("Applying Patches");
|
|
9234
|
+
console.log("=".repeat(60));
|
|
9235
|
+
const patchSummary = await applyPatches(
|
|
9236
|
+
artifact.findings,
|
|
9237
|
+
absoluteTarget,
|
|
9238
|
+
{
|
|
9239
|
+
force: options.force,
|
|
9240
|
+
dryRun: false
|
|
9241
|
+
}
|
|
9242
|
+
);
|
|
9243
|
+
console.log("\n" + "-".repeat(60));
|
|
9244
|
+
console.log("Patch Summary");
|
|
9245
|
+
console.log("-".repeat(60));
|
|
9246
|
+
console.log(`Total patchable findings: ${patchSummary.totalPatchable}`);
|
|
9247
|
+
console.log(`\x1B[32mSuccessfully applied: ${patchSummary.applied}\x1B[0m`);
|
|
9248
|
+
if (patchSummary.failed > 0) {
|
|
9249
|
+
console.log(`\x1B[31mFailed: ${patchSummary.failed}\x1B[0m`);
|
|
9250
|
+
}
|
|
9251
|
+
if (patchSummary.skipped > 0) {
|
|
9252
|
+
console.log(`\x1B[90mSkipped: ${patchSummary.skipped}\x1B[0m`);
|
|
9253
|
+
}
|
|
9254
|
+
if (patchSummary.failed > 0) {
|
|
9255
|
+
console.log("\nFailed patches:");
|
|
9256
|
+
for (const result of patchSummary.results) {
|
|
9257
|
+
if (!result.success && result.error !== "User declined") {
|
|
9258
|
+
console.log(` \x1B[31m\u2717\x1B[0m ${result.file}: ${result.error}`);
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9261
|
+
}
|
|
9262
|
+
console.log("");
|
|
9263
|
+
}
|
|
8733
9264
|
if (shouldFail(findings, failOn)) {
|
|
8734
9265
|
console.log(
|
|
8735
9266
|
`\x1B[31mFailing: Found findings with severity >= ${failOn}\x1B[0m`
|
|
@@ -8748,7 +9279,7 @@ function registerScanCommand(program2) {
|
|
|
8748
9279
|
).option(
|
|
8749
9280
|
"-o, --out <path>",
|
|
8750
9281
|
"Output file or directory path",
|
|
8751
|
-
|
|
9282
|
+
path9.join(DEFAULT_OUTPUT_DIR, DEFAULT_OUTPUT_FILE)
|
|
8752
9283
|
).option(
|
|
8753
9284
|
"-f, --format <format>",
|
|
8754
9285
|
"Output format: json, sarif, or both",
|
|
@@ -8783,6 +9314,15 @@ function registerScanCommand(program2) {
|
|
|
8783
9314
|
).option(
|
|
8784
9315
|
"--no-emit-traces",
|
|
8785
9316
|
"Exclude proof traces from output"
|
|
9317
|
+
).option(
|
|
9318
|
+
"--apply-fixes",
|
|
9319
|
+
"Apply patches from findings after scan (requires confirmation unless --force is used)"
|
|
9320
|
+
).option(
|
|
9321
|
+
"--force",
|
|
9322
|
+
"Skip confirmation prompts when applying patches (use with --apply-fixes)"
|
|
9323
|
+
).option(
|
|
9324
|
+
"-r, --rules <path>",
|
|
9325
|
+
"Path to directory containing custom YAML rules or a single YAML rule file"
|
|
8786
9326
|
).addHelpText(
|
|
8787
9327
|
"after",
|
|
8788
9328
|
`
|
|
@@ -8805,6 +9345,10 @@ Examples:
|
|
|
8805
9345
|
$ vibecheck scan --include-tests Include test files
|
|
8806
9346
|
$ vibecheck scan --no-emit-intents Skip intent mining
|
|
8807
9347
|
$ vibecheck scan --no-emit-traces Skip proof trace building
|
|
9348
|
+
$ vibecheck scan --apply-fixes Apply patches from findings (with confirmation)
|
|
9349
|
+
$ vibecheck scan --apply-fixes --force Apply patches without confirmation prompts
|
|
9350
|
+
$ vibecheck scan --rules ./custom-rules Load custom YAML rules from directory
|
|
9351
|
+
$ vibecheck scan -r my-rule.yaml Load a single custom YAML rule file
|
|
8808
9352
|
|
|
8809
9353
|
Windows-safe output paths:
|
|
8810
9354
|
$ vibecheck scan --out vibecheck-scan.json Relative path (recommended)
|
|
@@ -8830,7 +9374,10 @@ Default excludes:
|
|
|
8830
9374
|
emitTraces: cmdOptions.emitTraces !== false,
|
|
8831
9375
|
// default true
|
|
8832
9376
|
exclude: cmdOptions.exclude,
|
|
8833
|
-
includeTests: Boolean(cmdOptions.includeTests)
|
|
9377
|
+
includeTests: Boolean(cmdOptions.includeTests),
|
|
9378
|
+
applyFixes: Boolean(cmdOptions.applyFixes),
|
|
9379
|
+
force: Boolean(cmdOptions.force),
|
|
9380
|
+
rules: cmdOptions.rules
|
|
8834
9381
|
};
|
|
8835
9382
|
const exitCode = await executeScan(targetDir, options);
|
|
8836
9383
|
process.exit(exitCode);
|
|
@@ -9351,7 +9898,7 @@ function registerDemoArtifactCommand(program2) {
|
|
|
9351
9898
|
}
|
|
9352
9899
|
|
|
9353
9900
|
// src/commands/intent.ts
|
|
9354
|
-
import
|
|
9901
|
+
import path10 from "path";
|
|
9355
9902
|
async function executeIntent(targetDir, options) {
|
|
9356
9903
|
const absoluteTarget = resolvePath(targetDir);
|
|
9357
9904
|
console.log(`Mining intent map: ${absoluteTarget}`);
|
|
@@ -9386,7 +9933,7 @@ Completed in ${endTime - startTime}ms`);
|
|
|
9386
9933
|
options.repoName
|
|
9387
9934
|
);
|
|
9388
9935
|
let outputPath = options.out;
|
|
9389
|
-
if (!
|
|
9936
|
+
if (!path10.isAbsolute(outputPath)) {
|
|
9390
9937
|
outputPath = resolvePath(absoluteTarget, outputPath);
|
|
9391
9938
|
}
|
|
9392
9939
|
if (options.format === "json") {
|
|
@@ -9512,63 +10059,63 @@ function registerIntentCommand(program2) {
|
|
|
9512
10059
|
}
|
|
9513
10060
|
|
|
9514
10061
|
// src/commands/evaluate.ts
|
|
9515
|
-
import
|
|
10062
|
+
import path11 from "path";
|
|
9516
10063
|
|
|
9517
10064
|
// ../policy/dist/schemas/waiver.js
|
|
9518
|
-
import { z as
|
|
9519
|
-
var WaiverMatchSchema =
|
|
10065
|
+
import { z as z8 } from "zod";
|
|
10066
|
+
var WaiverMatchSchema = z8.object({
|
|
9520
10067
|
/** Exact fingerprint match */
|
|
9521
|
-
fingerprint:
|
|
10068
|
+
fingerprint: z8.string().optional(),
|
|
9522
10069
|
/** Rule ID (exact or prefix like "VC-AUTH-*") */
|
|
9523
|
-
ruleId:
|
|
10070
|
+
ruleId: z8.string().optional(),
|
|
9524
10071
|
/** Path glob pattern for evidence file matching */
|
|
9525
|
-
pathPattern:
|
|
10072
|
+
pathPattern: z8.string().optional()
|
|
9526
10073
|
});
|
|
9527
|
-
var WaiverSchema =
|
|
10074
|
+
var WaiverSchema = z8.object({
|
|
9528
10075
|
/** Unique waiver ID */
|
|
9529
|
-
id:
|
|
10076
|
+
id: z8.string(),
|
|
9530
10077
|
/** Match criteria */
|
|
9531
10078
|
match: WaiverMatchSchema.refine((m) => m.fingerprint || m.ruleId, { message: "Waiver must specify fingerprint or ruleId" }),
|
|
9532
10079
|
/** Justification for the waiver */
|
|
9533
|
-
reason:
|
|
10080
|
+
reason: z8.string().min(1),
|
|
9534
10081
|
/** Who created this waiver */
|
|
9535
|
-
createdBy:
|
|
10082
|
+
createdBy: z8.string().min(1),
|
|
9536
10083
|
/** When the waiver was created */
|
|
9537
|
-
createdAt:
|
|
10084
|
+
createdAt: z8.string().datetime(),
|
|
9538
10085
|
/** Optional expiration date */
|
|
9539
|
-
expiresAt:
|
|
10086
|
+
expiresAt: z8.string().datetime().optional(),
|
|
9540
10087
|
/** Optional ticket/issue reference */
|
|
9541
|
-
ticketRef:
|
|
10088
|
+
ticketRef: z8.string().optional()
|
|
9542
10089
|
});
|
|
9543
|
-
var WaiversFileSchema =
|
|
10090
|
+
var WaiversFileSchema = z8.object({
|
|
9544
10091
|
/** Schema version */
|
|
9545
|
-
version:
|
|
10092
|
+
version: z8.literal("0.1"),
|
|
9546
10093
|
/** List of waivers */
|
|
9547
|
-
waivers:
|
|
10094
|
+
waivers: z8.array(WaiverSchema)
|
|
9548
10095
|
});
|
|
9549
10096
|
|
|
9550
10097
|
// ../policy/dist/schemas/policy-config.js
|
|
9551
|
-
import { z as
|
|
9552
|
-
var ProfileNameSchema =
|
|
9553
|
-
var ThresholdsSchema =
|
|
10098
|
+
import { z as z9 } from "zod";
|
|
10099
|
+
var ProfileNameSchema = z9.enum(["startup", "strict", "compliance-lite"]);
|
|
10100
|
+
var ThresholdsSchema = z9.object({
|
|
9554
10101
|
/** Minimum severity to trigger FAIL status */
|
|
9555
10102
|
failOnSeverity: SeveritySchema.default("high"),
|
|
9556
10103
|
/** Minimum severity to trigger WARN status */
|
|
9557
10104
|
warnOnSeverity: SeveritySchema.default("medium"),
|
|
9558
10105
|
/** Minimum confidence (0-1) for a finding to trigger FAIL */
|
|
9559
|
-
minConfidenceForFail:
|
|
10106
|
+
minConfidenceForFail: z9.number().min(0).max(1).default(0.7),
|
|
9560
10107
|
/** Minimum confidence (0-1) for a finding to trigger WARN */
|
|
9561
|
-
minConfidenceForWarn:
|
|
10108
|
+
minConfidenceForWarn: z9.number().min(0).max(1).default(0.5),
|
|
9562
10109
|
/** Special lower confidence threshold for critical findings */
|
|
9563
|
-
minConfidenceCritical:
|
|
10110
|
+
minConfidenceCritical: z9.number().min(0).max(1).default(0.5),
|
|
9564
10111
|
/** Maximum number of findings before auto-fail (0 = unlimited) */
|
|
9565
|
-
maxFindings:
|
|
10112
|
+
maxFindings: z9.number().int().min(0).default(0),
|
|
9566
10113
|
/** Maximum number of critical findings before auto-fail (0 = unlimited) */
|
|
9567
|
-
maxCritical:
|
|
10114
|
+
maxCritical: z9.number().int().min(0).default(0),
|
|
9568
10115
|
/** Maximum number of high findings before auto-fail (0 = unlimited) */
|
|
9569
|
-
maxHigh:
|
|
10116
|
+
maxHigh: z9.number().int().min(0).default(0)
|
|
9570
10117
|
});
|
|
9571
|
-
var OverrideActionSchema =
|
|
10118
|
+
var OverrideActionSchema = z9.enum([
|
|
9572
10119
|
"ignore",
|
|
9573
10120
|
// Skip this finding entirely
|
|
9574
10121
|
"downgrade",
|
|
@@ -9580,62 +10127,62 @@ var OverrideActionSchema = z8.enum([
|
|
|
9580
10127
|
"fail"
|
|
9581
10128
|
// Always fail on this
|
|
9582
10129
|
]);
|
|
9583
|
-
var OverrideSchema =
|
|
10130
|
+
var OverrideSchema = z9.object({
|
|
9584
10131
|
/** Rule ID pattern (exact or prefix like "VC-AUTH-*") */
|
|
9585
|
-
ruleId:
|
|
10132
|
+
ruleId: z9.string().optional(),
|
|
9586
10133
|
/** Category to match */
|
|
9587
10134
|
category: CategorySchema.optional(),
|
|
9588
10135
|
/** Path pattern for evidence file matching (glob-like) */
|
|
9589
|
-
pathPattern:
|
|
10136
|
+
pathPattern: z9.string().optional(),
|
|
9590
10137
|
/** Action to take when matched */
|
|
9591
10138
|
action: OverrideActionSchema,
|
|
9592
10139
|
/** Override severity when action is downgrade/upgrade */
|
|
9593
10140
|
severity: SeveritySchema.optional(),
|
|
9594
10141
|
/** Comment explaining the override */
|
|
9595
|
-
comment:
|
|
10142
|
+
comment: z9.string().optional()
|
|
9596
10143
|
});
|
|
9597
|
-
var RegressionPolicySchema =
|
|
10144
|
+
var RegressionPolicySchema = z9.object({
|
|
9598
10145
|
/** Fail on any new high/critical findings */
|
|
9599
|
-
failOnNewHighCritical:
|
|
10146
|
+
failOnNewHighCritical: z9.boolean().default(true),
|
|
9600
10147
|
/** Fail on any severity regression (e.g., medium became high) */
|
|
9601
|
-
failOnSeverityRegression:
|
|
10148
|
+
failOnSeverityRegression: z9.boolean().default(false),
|
|
9602
10149
|
/** Fail on net increase in findings */
|
|
9603
|
-
failOnNetIncrease:
|
|
10150
|
+
failOnNetIncrease: z9.boolean().default(false),
|
|
9604
10151
|
/** Warn on any new findings */
|
|
9605
|
-
warnOnNewFindings:
|
|
10152
|
+
warnOnNewFindings: z9.boolean().default(true),
|
|
9606
10153
|
/** Fail on protection removed from routes (auth/validation coverage decreased) */
|
|
9607
|
-
failOnProtectionRemoved:
|
|
10154
|
+
failOnProtectionRemoved: z9.boolean().default(false),
|
|
9608
10155
|
/** Warn on protection removed from routes */
|
|
9609
|
-
warnOnProtectionRemoved:
|
|
10156
|
+
warnOnProtectionRemoved: z9.boolean().default(true),
|
|
9610
10157
|
/** Fail on any semantic regression (coverage decrease, severity group increase) */
|
|
9611
|
-
failOnSemanticRegression:
|
|
10158
|
+
failOnSemanticRegression: z9.boolean().default(false)
|
|
9612
10159
|
});
|
|
9613
|
-
var PolicyConfigSchema =
|
|
10160
|
+
var PolicyConfigSchema = z9.object({
|
|
9614
10161
|
/** Profile name for presets */
|
|
9615
10162
|
profile: ProfileNameSchema.optional(),
|
|
9616
10163
|
/** Threshold configuration */
|
|
9617
10164
|
thresholds: ThresholdsSchema.default({}),
|
|
9618
10165
|
/** Override rules */
|
|
9619
|
-
overrides:
|
|
10166
|
+
overrides: z9.array(OverrideSchema).default([]),
|
|
9620
10167
|
/** Regression policy */
|
|
9621
10168
|
regression: RegressionPolicySchema.default({})
|
|
9622
10169
|
});
|
|
9623
|
-
var ConfigFileSchema =
|
|
10170
|
+
var ConfigFileSchema = z9.object({
|
|
9624
10171
|
/** Policy configuration */
|
|
9625
10172
|
policy: PolicyConfigSchema.optional(),
|
|
9626
10173
|
/** Path to waivers file */
|
|
9627
|
-
waiversPath:
|
|
10174
|
+
waiversPath: z9.string().optional()
|
|
9628
10175
|
});
|
|
9629
10176
|
|
|
9630
10177
|
// ../policy/dist/schemas/policy-report.js
|
|
9631
|
-
import { z as
|
|
10178
|
+
import { z as z10 } from "zod";
|
|
9632
10179
|
var POLICY_REPORT_VERSION = "0.1";
|
|
9633
|
-
var PolicyStatusSchema =
|
|
9634
|
-
var PolicyReasonSchema =
|
|
10180
|
+
var PolicyStatusSchema = z10.enum(["pass", "warn", "fail"]);
|
|
10181
|
+
var PolicyReasonSchema = z10.object({
|
|
9635
10182
|
/** The status this reason contributes to */
|
|
9636
10183
|
status: PolicyStatusSchema,
|
|
9637
10184
|
/** Machine-readable reason code */
|
|
9638
|
-
code:
|
|
10185
|
+
code: z10.enum([
|
|
9639
10186
|
"severity_threshold",
|
|
9640
10187
|
"confidence_threshold",
|
|
9641
10188
|
"count_threshold",
|
|
@@ -9651,47 +10198,47 @@ var PolicyReasonSchema = z9.object({
|
|
|
9651
10198
|
"semantic_regression"
|
|
9652
10199
|
]),
|
|
9653
10200
|
/** Human-readable description */
|
|
9654
|
-
message:
|
|
10201
|
+
message: z10.string(),
|
|
9655
10202
|
/** Optional finding IDs that triggered this */
|
|
9656
|
-
findingIds:
|
|
10203
|
+
findingIds: z10.array(z10.string()).optional(),
|
|
9657
10204
|
/** Optional details */
|
|
9658
|
-
details:
|
|
10205
|
+
details: z10.record(z10.unknown()).optional()
|
|
9659
10206
|
});
|
|
9660
|
-
var PolicySummaryCountsSchema =
|
|
10207
|
+
var PolicySummaryCountsSchema = z10.object({
|
|
9661
10208
|
/** Total findings after filtering */
|
|
9662
|
-
total:
|
|
10209
|
+
total: z10.number().int(),
|
|
9663
10210
|
/** By severity */
|
|
9664
|
-
bySeverity:
|
|
9665
|
-
critical:
|
|
9666
|
-
high:
|
|
9667
|
-
medium:
|
|
9668
|
-
low:
|
|
9669
|
-
info:
|
|
10211
|
+
bySeverity: z10.object({
|
|
10212
|
+
critical: z10.number().int(),
|
|
10213
|
+
high: z10.number().int(),
|
|
10214
|
+
medium: z10.number().int(),
|
|
10215
|
+
low: z10.number().int(),
|
|
10216
|
+
info: z10.number().int()
|
|
9670
10217
|
}),
|
|
9671
10218
|
/** By category */
|
|
9672
|
-
byCategory:
|
|
10219
|
+
byCategory: z10.record(CategorySchema, z10.number().int()),
|
|
9673
10220
|
/** Count of waived findings */
|
|
9674
|
-
waived:
|
|
10221
|
+
waived: z10.number().int(),
|
|
9675
10222
|
/** Count of ignored by override */
|
|
9676
|
-
ignored:
|
|
10223
|
+
ignored: z10.number().int()
|
|
9677
10224
|
});
|
|
9678
|
-
var ProtectionRegressionSchema =
|
|
10225
|
+
var ProtectionRegressionSchema = z10.object({
|
|
9679
10226
|
/** Route identifier (e.g., "/api/users:POST") */
|
|
9680
|
-
routeId:
|
|
10227
|
+
routeId: z10.string(),
|
|
9681
10228
|
/** File containing the route */
|
|
9682
|
-
file:
|
|
10229
|
+
file: z10.string(),
|
|
9683
10230
|
/** HTTP method */
|
|
9684
|
-
method:
|
|
10231
|
+
method: z10.string(),
|
|
9685
10232
|
/** Type of protection that was removed */
|
|
9686
|
-
protectionType:
|
|
10233
|
+
protectionType: z10.enum(["auth", "validation", "rate-limit", "middleware"]),
|
|
9687
10234
|
/** Description of what changed */
|
|
9688
|
-
description:
|
|
10235
|
+
description: z10.string(),
|
|
9689
10236
|
/** Related finding fingerprints */
|
|
9690
|
-
relatedFingerprints:
|
|
10237
|
+
relatedFingerprints: z10.array(z10.string()).optional()
|
|
9691
10238
|
});
|
|
9692
|
-
var SemanticRegressionSchema =
|
|
10239
|
+
var SemanticRegressionSchema = z10.object({
|
|
9693
10240
|
/** Type of semantic regression */
|
|
9694
|
-
type:
|
|
10241
|
+
type: z10.enum([
|
|
9695
10242
|
"protection_removed",
|
|
9696
10243
|
// Auth/validation removed from route
|
|
9697
10244
|
"coverage_decreased",
|
|
@@ -9702,68 +10249,68 @@ var SemanticRegressionSchema = z9.object({
|
|
|
9702
10249
|
/** Severity of this regression */
|
|
9703
10250
|
severity: SeveritySchema,
|
|
9704
10251
|
/** Human-readable description */
|
|
9705
|
-
description:
|
|
10252
|
+
description: z10.string(),
|
|
9706
10253
|
/** Route or fingerprint group affected */
|
|
9707
|
-
affectedId:
|
|
10254
|
+
affectedId: z10.string(),
|
|
9708
10255
|
/** Detailed evidence */
|
|
9709
|
-
details:
|
|
10256
|
+
details: z10.record(z10.unknown()).optional()
|
|
9710
10257
|
});
|
|
9711
|
-
var RegressionSummarySchema =
|
|
10258
|
+
var RegressionSummarySchema = z10.object({
|
|
9712
10259
|
/** Baseline artifact path or identifier */
|
|
9713
|
-
baselineId:
|
|
10260
|
+
baselineId: z10.string(),
|
|
9714
10261
|
/** When baseline was generated */
|
|
9715
|
-
baselineGeneratedAt:
|
|
10262
|
+
baselineGeneratedAt: z10.string(),
|
|
9716
10263
|
/** New findings (not in baseline) */
|
|
9717
|
-
newFindings:
|
|
9718
|
-
findingId:
|
|
9719
|
-
fingerprint:
|
|
10264
|
+
newFindings: z10.array(z10.object({
|
|
10265
|
+
findingId: z10.string(),
|
|
10266
|
+
fingerprint: z10.string(),
|
|
9720
10267
|
severity: SeveritySchema,
|
|
9721
|
-
ruleId:
|
|
9722
|
-
title:
|
|
10268
|
+
ruleId: z10.string(),
|
|
10269
|
+
title: z10.string()
|
|
9723
10270
|
})),
|
|
9724
10271
|
/** Resolved findings (in baseline but not current) */
|
|
9725
|
-
resolvedFindings:
|
|
9726
|
-
fingerprint:
|
|
10272
|
+
resolvedFindings: z10.array(z10.object({
|
|
10273
|
+
fingerprint: z10.string(),
|
|
9727
10274
|
severity: SeveritySchema,
|
|
9728
|
-
ruleId:
|
|
9729
|
-
title:
|
|
10275
|
+
ruleId: z10.string(),
|
|
10276
|
+
title: z10.string()
|
|
9730
10277
|
})),
|
|
9731
10278
|
/** Persisting findings (in both) */
|
|
9732
|
-
persistingCount:
|
|
10279
|
+
persistingCount: z10.number().int(),
|
|
9733
10280
|
/** Severity regressions (same fingerprint but higher severity) */
|
|
9734
|
-
severityRegressions:
|
|
9735
|
-
fingerprint:
|
|
9736
|
-
ruleId:
|
|
10281
|
+
severityRegressions: z10.array(z10.object({
|
|
10282
|
+
fingerprint: z10.string(),
|
|
10283
|
+
ruleId: z10.string(),
|
|
9737
10284
|
previousSeverity: SeveritySchema,
|
|
9738
10285
|
currentSeverity: SeveritySchema,
|
|
9739
|
-
title:
|
|
10286
|
+
title: z10.string()
|
|
9740
10287
|
})),
|
|
9741
10288
|
/** Net change in finding count */
|
|
9742
|
-
netChange:
|
|
10289
|
+
netChange: z10.number().int(),
|
|
9743
10290
|
/** Protection regressions (auth/validation removed from routes) */
|
|
9744
|
-
protectionRegressions:
|
|
10291
|
+
protectionRegressions: z10.array(ProtectionRegressionSchema).optional(),
|
|
9745
10292
|
/** Semantic regressions (abstract security property degradations) */
|
|
9746
|
-
semanticRegressions:
|
|
10293
|
+
semanticRegressions: z10.array(SemanticRegressionSchema).optional()
|
|
9747
10294
|
});
|
|
9748
|
-
var WaivedFindingSchema =
|
|
10295
|
+
var WaivedFindingSchema = z10.object({
|
|
9749
10296
|
/** The finding that was waived */
|
|
9750
|
-
finding:
|
|
9751
|
-
id:
|
|
9752
|
-
fingerprint:
|
|
9753
|
-
ruleId:
|
|
10297
|
+
finding: z10.object({
|
|
10298
|
+
id: z10.string(),
|
|
10299
|
+
fingerprint: z10.string(),
|
|
10300
|
+
ruleId: z10.string(),
|
|
9754
10301
|
severity: SeveritySchema,
|
|
9755
|
-
title:
|
|
10302
|
+
title: z10.string()
|
|
9756
10303
|
}),
|
|
9757
10304
|
/** The waiver that matched */
|
|
9758
10305
|
waiver: WaiverSchema,
|
|
9759
10306
|
/** Whether waiver is expired */
|
|
9760
|
-
expired:
|
|
10307
|
+
expired: z10.boolean()
|
|
9761
10308
|
});
|
|
9762
|
-
var PolicyReportSchema =
|
|
10309
|
+
var PolicyReportSchema = z10.object({
|
|
9763
10310
|
/** Report schema version */
|
|
9764
|
-
policyVersion:
|
|
10311
|
+
policyVersion: z10.literal(POLICY_REPORT_VERSION),
|
|
9765
10312
|
/** When evaluation was performed */
|
|
9766
|
-
evaluatedAt:
|
|
10313
|
+
evaluatedAt: z10.string().datetime(),
|
|
9767
10314
|
/** Profile name used */
|
|
9768
10315
|
profileName: ProfileNameSchema.nullable(),
|
|
9769
10316
|
/** Final status */
|
|
@@ -9771,36 +10318,36 @@ var PolicyReportSchema = z9.object({
|
|
|
9771
10318
|
/** Thresholds applied */
|
|
9772
10319
|
thresholds: ThresholdsSchema,
|
|
9773
10320
|
/** Overrides applied */
|
|
9774
|
-
overrides:
|
|
10321
|
+
overrides: z10.array(OverrideSchema),
|
|
9775
10322
|
/** Regression policy applied */
|
|
9776
10323
|
regressionPolicy: RegressionPolicySchema,
|
|
9777
10324
|
/** Summary counts */
|
|
9778
10325
|
summary: PolicySummaryCountsSchema,
|
|
9779
10326
|
/** Reasons for the status */
|
|
9780
|
-
reasons:
|
|
10327
|
+
reasons: z10.array(PolicyReasonSchema),
|
|
9781
10328
|
/** Regression summary (if baseline provided) */
|
|
9782
10329
|
regression: RegressionSummarySchema.optional(),
|
|
9783
10330
|
/** Waived findings */
|
|
9784
|
-
waivedFindings:
|
|
10331
|
+
waivedFindings: z10.array(WaivedFindingSchema),
|
|
9785
10332
|
/** Active (non-waived, non-ignored) findings included in evaluation */
|
|
9786
|
-
activeFindings:
|
|
9787
|
-
id:
|
|
9788
|
-
fingerprint:
|
|
9789
|
-
ruleId:
|
|
10333
|
+
activeFindings: z10.array(z10.object({
|
|
10334
|
+
id: z10.string(),
|
|
10335
|
+
fingerprint: z10.string(),
|
|
10336
|
+
ruleId: z10.string(),
|
|
9790
10337
|
severity: SeveritySchema,
|
|
9791
10338
|
originalSeverity: SeveritySchema.optional(),
|
|
9792
|
-
confidence:
|
|
9793
|
-
title:
|
|
10339
|
+
confidence: z10.number(),
|
|
10340
|
+
title: z10.string(),
|
|
9794
10341
|
category: CategorySchema,
|
|
9795
|
-
evidencePaths:
|
|
10342
|
+
evidencePaths: z10.array(z10.string())
|
|
9796
10343
|
})),
|
|
9797
10344
|
/** Recommended exit code (0 = pass/warn, 1 = fail) */
|
|
9798
|
-
exitCode:
|
|
10345
|
+
exitCode: z10.union([z10.literal(0), z10.literal(1)]),
|
|
9799
10346
|
/** Source artifact info */
|
|
9800
|
-
artifact:
|
|
9801
|
-
path:
|
|
9802
|
-
generatedAt:
|
|
9803
|
-
repoName:
|
|
10347
|
+
artifact: z10.object({
|
|
10348
|
+
path: z10.string().optional(),
|
|
10349
|
+
generatedAt: z10.string(),
|
|
10350
|
+
repoName: z10.string().optional()
|
|
9804
10351
|
})
|
|
9805
10352
|
});
|
|
9806
10353
|
|
|
@@ -9928,7 +10475,7 @@ var PROFILE_DESCRIPTIONS = {
|
|
|
9928
10475
|
};
|
|
9929
10476
|
|
|
9930
10477
|
// ../policy/dist/waivers.js
|
|
9931
|
-
import
|
|
10478
|
+
import micromatch2 from "micromatch";
|
|
9932
10479
|
function matchRuleId(ruleId, pattern) {
|
|
9933
10480
|
if (pattern.endsWith("*")) {
|
|
9934
10481
|
const prefix = pattern.slice(0, -1);
|
|
@@ -9937,7 +10484,7 @@ function matchRuleId(ruleId, pattern) {
|
|
|
9937
10484
|
return ruleId === pattern;
|
|
9938
10485
|
}
|
|
9939
10486
|
function matchPathPattern(evidencePaths, pattern) {
|
|
9940
|
-
return evidencePaths.some((
|
|
10487
|
+
return evidencePaths.some((path15) => micromatch2.isMatch(path15, pattern));
|
|
9941
10488
|
}
|
|
9942
10489
|
function isWaiverExpired(waiver, now = /* @__PURE__ */ new Date()) {
|
|
9943
10490
|
if (!waiver.expiresAt) {
|
|
@@ -10748,7 +11295,7 @@ async function executeEvaluate(options) {
|
|
|
10748
11295
|
});
|
|
10749
11296
|
if (options.out) {
|
|
10750
11297
|
const outPath = resolvePath(options.out);
|
|
10751
|
-
ensureDir(
|
|
11298
|
+
ensureDir(path11.dirname(outPath));
|
|
10752
11299
|
writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
10753
11300
|
if (!options.quiet) {
|
|
10754
11301
|
console.log(`Policy report written to: ${outPath}`);
|
|
@@ -10826,7 +11373,7 @@ Examples:
|
|
|
10826
11373
|
}
|
|
10827
11374
|
|
|
10828
11375
|
// src/commands/waivers.ts
|
|
10829
|
-
import
|
|
11376
|
+
import path12 from "path";
|
|
10830
11377
|
var DEFAULT_WAIVERS_FILE = "vibecheck-waivers.json";
|
|
10831
11378
|
function loadWaiversFile(filepath) {
|
|
10832
11379
|
const absolutePath = resolvePath(filepath);
|
|
@@ -10846,7 +11393,7 @@ function loadWaiversFile(filepath) {
|
|
|
10846
11393
|
}
|
|
10847
11394
|
function saveWaiversFile(filepath, file) {
|
|
10848
11395
|
const absolutePath = resolvePath(filepath);
|
|
10849
|
-
ensureDir(
|
|
11396
|
+
ensureDir(path12.dirname(absolutePath));
|
|
10850
11397
|
writeFileSync(absolutePath, JSON.stringify(file, null, 2));
|
|
10851
11398
|
}
|
|
10852
11399
|
function executeWaiversInit(filepath, force) {
|
|
@@ -11019,13 +11566,13 @@ Examples:
|
|
|
11019
11566
|
}
|
|
11020
11567
|
|
|
11021
11568
|
// src/commands/view.ts
|
|
11022
|
-
import { existsSync as
|
|
11023
|
-
import { resolve as resolve2, join as
|
|
11569
|
+
import { existsSync as existsSync4 } from "fs";
|
|
11570
|
+
import { resolve as resolve2, join as join4 } from "path";
|
|
11024
11571
|
|
|
11025
11572
|
// src/utils/static-server.ts
|
|
11026
11573
|
import { createServer } from "http";
|
|
11027
|
-
import { existsSync, readFileSync as
|
|
11028
|
-
import { join, extname } from "path";
|
|
11574
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
|
|
11575
|
+
import { join as join2, extname as extname3 } from "path";
|
|
11029
11576
|
var MIME_TYPES = {
|
|
11030
11577
|
".html": "text/html; charset=utf-8",
|
|
11031
11578
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -11050,7 +11597,7 @@ var MIME_TYPES = {
|
|
|
11050
11597
|
".map": "application/json"
|
|
11051
11598
|
};
|
|
11052
11599
|
function getMimeType(filePath) {
|
|
11053
|
-
const ext =
|
|
11600
|
+
const ext = extname3(filePath).toLowerCase();
|
|
11054
11601
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
11055
11602
|
}
|
|
11056
11603
|
function resolveFilePath(staticDir, urlPath) {
|
|
@@ -11064,49 +11611,49 @@ function resolveFilePath(staticDir, urlPath) {
|
|
|
11064
11611
|
if (queryIndex !== -1) {
|
|
11065
11612
|
decodedPath = decodedPath.substring(0, queryIndex);
|
|
11066
11613
|
}
|
|
11067
|
-
let filePath =
|
|
11068
|
-
const normalizedPath =
|
|
11614
|
+
let filePath = join2(staticDir, decodedPath === "/" ? "index.html" : decodedPath);
|
|
11615
|
+
const normalizedPath = join2(filePath);
|
|
11069
11616
|
if (!normalizedPath.startsWith(staticDir)) {
|
|
11070
11617
|
return null;
|
|
11071
11618
|
}
|
|
11072
|
-
if (
|
|
11073
|
-
const stat =
|
|
11619
|
+
if (existsSync2(filePath)) {
|
|
11620
|
+
const stat = statSync2(filePath);
|
|
11074
11621
|
if (stat.isDirectory()) {
|
|
11075
|
-
filePath =
|
|
11076
|
-
if (!
|
|
11622
|
+
filePath = join2(filePath, "index.html");
|
|
11623
|
+
if (!existsSync2(filePath)) {
|
|
11077
11624
|
return null;
|
|
11078
11625
|
}
|
|
11079
11626
|
}
|
|
11080
11627
|
return filePath;
|
|
11081
11628
|
}
|
|
11082
|
-
if (
|
|
11629
|
+
if (existsSync2(filePath + ".html")) {
|
|
11083
11630
|
return filePath + ".html";
|
|
11084
11631
|
}
|
|
11085
11632
|
const segments = decodedPath.split("/").filter(Boolean);
|
|
11086
11633
|
if (segments.length >= 2) {
|
|
11087
11634
|
const parentPath = segments.slice(0, -1).join("/");
|
|
11088
|
-
const parentDir =
|
|
11089
|
-
if (
|
|
11635
|
+
const parentDir = join2(staticDir, parentPath);
|
|
11636
|
+
if (existsSync2(parentDir) && statSync2(parentDir).isDirectory()) {
|
|
11090
11637
|
try {
|
|
11091
|
-
const files =
|
|
11638
|
+
const files = readdirSync2(parentDir);
|
|
11092
11639
|
const htmlFile = files.find((f) => f.endsWith(".html"));
|
|
11093
11640
|
if (htmlFile) {
|
|
11094
|
-
return
|
|
11641
|
+
return join2(parentDir, htmlFile);
|
|
11095
11642
|
}
|
|
11096
11643
|
} catch {
|
|
11097
11644
|
}
|
|
11098
11645
|
}
|
|
11099
|
-
const parentHtml =
|
|
11100
|
-
if (
|
|
11646
|
+
const parentHtml = join2(staticDir, parentPath + ".html");
|
|
11647
|
+
if (existsSync2(parentHtml)) {
|
|
11101
11648
|
return parentHtml;
|
|
11102
11649
|
}
|
|
11103
|
-
const parentIndexHtml =
|
|
11104
|
-
if (
|
|
11650
|
+
const parentIndexHtml = join2(staticDir, parentPath, "index.html");
|
|
11651
|
+
if (existsSync2(parentIndexHtml)) {
|
|
11105
11652
|
return parentIndexHtml;
|
|
11106
11653
|
}
|
|
11107
11654
|
}
|
|
11108
|
-
const indexPath =
|
|
11109
|
-
if (
|
|
11655
|
+
const indexPath = join2(staticDir, "index.html");
|
|
11656
|
+
if (existsSync2(indexPath)) {
|
|
11110
11657
|
return indexPath;
|
|
11111
11658
|
}
|
|
11112
11659
|
return null;
|
|
@@ -11120,7 +11667,7 @@ function handleRequest(staticDir, req, res) {
|
|
|
11120
11667
|
return;
|
|
11121
11668
|
}
|
|
11122
11669
|
try {
|
|
11123
|
-
const content =
|
|
11670
|
+
const content = readFileSync5(filePath);
|
|
11124
11671
|
const contentType = getMimeType(filePath);
|
|
11125
11672
|
res.writeHead(200, {
|
|
11126
11673
|
"Content-Type": contentType,
|
|
@@ -11140,9 +11687,9 @@ async function startStaticServer(options) {
|
|
|
11140
11687
|
const server = createServer((req, res) => {
|
|
11141
11688
|
const urlPath = req.url || "/";
|
|
11142
11689
|
if (urlPath === "/__vibecheck__/artifact" || urlPath === "/__vibecheck__/artifact.json") {
|
|
11143
|
-
if (artifactPath &&
|
|
11690
|
+
if (artifactPath && existsSync2(artifactPath)) {
|
|
11144
11691
|
try {
|
|
11145
|
-
const content =
|
|
11692
|
+
const content = readFileSync5(artifactPath);
|
|
11146
11693
|
res.writeHead(200, {
|
|
11147
11694
|
"Content-Type": "application/json",
|
|
11148
11695
|
"Content-Length": content.length,
|
|
@@ -11222,22 +11769,22 @@ function isPortAvailable(port) {
|
|
|
11222
11769
|
}
|
|
11223
11770
|
|
|
11224
11771
|
// src/utils/viewer-cache.ts
|
|
11225
|
-
import { existsSync as
|
|
11226
|
-
import { join as
|
|
11772
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync6, rmSync } from "fs";
|
|
11773
|
+
import { join as join3 } from "path";
|
|
11227
11774
|
import { homedir } from "os";
|
|
11228
11775
|
import { execSync as execSync2 } from "child_process";
|
|
11229
11776
|
import { extract } from "tar";
|
|
11230
|
-
var CACHE_DIR =
|
|
11231
|
-
var VIEWER_DIR =
|
|
11232
|
-
var VIEWER_DIST_DIR =
|
|
11233
|
-
var VERSION_FILE =
|
|
11777
|
+
var CACHE_DIR = join3(homedir(), ".vibecheck");
|
|
11778
|
+
var VIEWER_DIR = join3(CACHE_DIR, "viewer");
|
|
11779
|
+
var VIEWER_DIST_DIR = join3(VIEWER_DIR, "dist");
|
|
11780
|
+
var VERSION_FILE = join3(VIEWER_DIR, ".version");
|
|
11234
11781
|
var VIEWER_PACKAGE = "@quantracode/vibecheck-viewer";
|
|
11235
11782
|
function getInstalledVersion() {
|
|
11236
|
-
if (!
|
|
11783
|
+
if (!existsSync3(VERSION_FILE)) {
|
|
11237
11784
|
return null;
|
|
11238
11785
|
}
|
|
11239
11786
|
try {
|
|
11240
|
-
return
|
|
11787
|
+
return readFileSync6(VERSION_FILE, "utf-8").trim();
|
|
11241
11788
|
} catch {
|
|
11242
11789
|
return null;
|
|
11243
11790
|
}
|
|
@@ -11266,7 +11813,7 @@ async function downloadPackage(version) {
|
|
|
11266
11813
|
stdio: ["pipe", "pipe", "pipe"]
|
|
11267
11814
|
}
|
|
11268
11815
|
).trim();
|
|
11269
|
-
if (
|
|
11816
|
+
if (existsSync3(VIEWER_DIR)) {
|
|
11270
11817
|
rmSync(VIEWER_DIR, { recursive: true, force: true });
|
|
11271
11818
|
}
|
|
11272
11819
|
mkdirSync(VIEWER_DIR, { recursive: true });
|
|
@@ -11274,7 +11821,7 @@ async function downloadPackage(version) {
|
|
|
11274
11821
|
if (!response.ok) {
|
|
11275
11822
|
throw new Error(`Failed to download viewer: ${response.statusText}`);
|
|
11276
11823
|
}
|
|
11277
|
-
const tarPath =
|
|
11824
|
+
const tarPath = join3(CACHE_DIR, "viewer.tgz");
|
|
11278
11825
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
11279
11826
|
writeFileSync3(tarPath, buffer);
|
|
11280
11827
|
await extract({
|
|
@@ -11292,7 +11839,7 @@ async function ensureViewer(forceUpdate = false) {
|
|
|
11292
11839
|
try {
|
|
11293
11840
|
latestVersion = await getLatestVersion();
|
|
11294
11841
|
} catch (error) {
|
|
11295
|
-
if (installedVersion &&
|
|
11842
|
+
if (installedVersion && existsSync3(join3(VIEWER_DIST_DIR, "index.html"))) {
|
|
11296
11843
|
return {
|
|
11297
11844
|
path: VIEWER_DIST_DIR,
|
|
11298
11845
|
version: installedVersion
|
|
@@ -11300,7 +11847,7 @@ async function ensureViewer(forceUpdate = false) {
|
|
|
11300
11847
|
}
|
|
11301
11848
|
throw error;
|
|
11302
11849
|
}
|
|
11303
|
-
const needsUpdate = forceUpdate || !installedVersion || installedVersion !== latestVersion || !
|
|
11850
|
+
const needsUpdate = forceUpdate || !installedVersion || installedVersion !== latestVersion || !existsSync3(join3(VIEWER_DIST_DIR, "index.html"));
|
|
11304
11851
|
if (needsUpdate) {
|
|
11305
11852
|
console.log(
|
|
11306
11853
|
installedVersion ? `Updating viewer from ${installedVersion} to ${latestVersion}...` : `Installing viewer v${latestVersion}...`
|
|
@@ -11315,7 +11862,7 @@ async function ensureViewer(forceUpdate = false) {
|
|
|
11315
11862
|
};
|
|
11316
11863
|
}
|
|
11317
11864
|
function clearViewerCache() {
|
|
11318
|
-
if (
|
|
11865
|
+
if (existsSync3(VIEWER_DIR)) {
|
|
11319
11866
|
rmSync(VIEWER_DIR, { recursive: true, force: true });
|
|
11320
11867
|
console.log("Viewer cache cleared.");
|
|
11321
11868
|
} else {
|
|
@@ -11425,7 +11972,7 @@ Check your network connection and try again.`);
|
|
|
11425
11972
|
`);
|
|
11426
11973
|
process.exit(1);
|
|
11427
11974
|
}
|
|
11428
|
-
if (!
|
|
11975
|
+
if (!existsSync4(join4(viewerInfo.path, "index.html"))) {
|
|
11429
11976
|
console.error(`\x1B[31mError: Viewer files not found.\x1B[0m`);
|
|
11430
11977
|
console.error(`Try reinstalling: vibecheck view --update
|
|
11431
11978
|
`);
|
|
@@ -11434,7 +11981,7 @@ Check your network connection and try again.`);
|
|
|
11434
11981
|
let artifactPath;
|
|
11435
11982
|
if (options.artifact) {
|
|
11436
11983
|
artifactPath = resolve2(options.artifact);
|
|
11437
|
-
if (!
|
|
11984
|
+
if (!existsSync4(artifactPath)) {
|
|
11438
11985
|
console.log(
|
|
11439
11986
|
`\x1B[33mWarning: Artifact file not found: ${options.artifact}\x1B[0m
|
|
11440
11987
|
`
|
|
@@ -11450,7 +11997,7 @@ Check your network connection and try again.`);
|
|
|
11450
11997
|
];
|
|
11451
11998
|
for (const p of commonPaths) {
|
|
11452
11999
|
const fullPath = resolve2(p);
|
|
11453
|
-
if (
|
|
12000
|
+
if (existsSync4(fullPath)) {
|
|
11454
12001
|
artifactPath = fullPath;
|
|
11455
12002
|
console.log(`\x1B[90mAuto-detected artifact: ${p}\x1B[0m`);
|
|
11456
12003
|
break;
|
|
@@ -11515,11 +12062,11 @@ Workflow:
|
|
|
11515
12062
|
}
|
|
11516
12063
|
|
|
11517
12064
|
// src/commands/license.ts
|
|
11518
|
-
import { readFileSync as
|
|
12065
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
11519
12066
|
import { homedir as homedir2 } from "os";
|
|
11520
|
-
import { join as
|
|
12067
|
+
import { join as join5 } from "path";
|
|
11521
12068
|
import chalk from "chalk";
|
|
11522
|
-
import
|
|
12069
|
+
import readline2 from "readline";
|
|
11523
12070
|
|
|
11524
12071
|
// ../license/dist/types.js
|
|
11525
12072
|
var PLAN_FEATURES = {
|
|
@@ -11733,17 +12280,17 @@ function inspectLicense(licenseKey) {
|
|
|
11733
12280
|
}
|
|
11734
12281
|
|
|
11735
12282
|
// src/commands/license.ts
|
|
11736
|
-
var CONFIG_DIR =
|
|
11737
|
-
var LICENSE_FILE =
|
|
12283
|
+
var CONFIG_DIR = join5(homedir2(), ".vibecheck");
|
|
12284
|
+
var LICENSE_FILE = join5(CONFIG_DIR, "license.key");
|
|
11738
12285
|
function ensureConfigDir() {
|
|
11739
|
-
if (!
|
|
12286
|
+
if (!existsSync5(CONFIG_DIR)) {
|
|
11740
12287
|
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
11741
12288
|
}
|
|
11742
12289
|
}
|
|
11743
12290
|
function getStoredLicenseKey() {
|
|
11744
12291
|
try {
|
|
11745
|
-
if (
|
|
11746
|
-
return
|
|
12292
|
+
if (existsSync5(LICENSE_FILE)) {
|
|
12293
|
+
return readFileSync7(LICENSE_FILE, "utf-8").trim();
|
|
11747
12294
|
}
|
|
11748
12295
|
} catch {
|
|
11749
12296
|
}
|
|
@@ -11755,7 +12302,7 @@ function storeLicenseKey(licenseKey) {
|
|
|
11755
12302
|
}
|
|
11756
12303
|
function clearLicenseKey() {
|
|
11757
12304
|
try {
|
|
11758
|
-
if (
|
|
12305
|
+
if (existsSync5(LICENSE_FILE)) {
|
|
11759
12306
|
unlinkSync(LICENSE_FILE);
|
|
11760
12307
|
return true;
|
|
11761
12308
|
}
|
|
@@ -11764,7 +12311,7 @@ function clearLicenseKey() {
|
|
|
11764
12311
|
return false;
|
|
11765
12312
|
}
|
|
11766
12313
|
function promptInput(question) {
|
|
11767
|
-
const rl =
|
|
12314
|
+
const rl = readline2.createInterface({
|
|
11768
12315
|
input: process.stdin,
|
|
11769
12316
|
output: process.stdout
|
|
11770
12317
|
});
|
|
@@ -11935,11 +12482,11 @@ async function createLicenseAction(options) {
|
|
|
11935
12482
|
}
|
|
11936
12483
|
let privateKey;
|
|
11937
12484
|
if (options.key) {
|
|
11938
|
-
if (!
|
|
12485
|
+
if (!existsSync5(options.key)) {
|
|
11939
12486
|
console.error(chalk.red(`Error: Key file not found: ${options.key}`));
|
|
11940
12487
|
process.exit(1);
|
|
11941
12488
|
}
|
|
11942
|
-
privateKey =
|
|
12489
|
+
privateKey = readFileSync7(options.key, "utf-8").trim();
|
|
11943
12490
|
} else if (process.env[options.keyEnv]) {
|
|
11944
12491
|
privateKey = process.env[options.keyEnv];
|
|
11945
12492
|
}
|
|
@@ -12111,7 +12658,7 @@ async function keygenAction() {
|
|
|
12111
12658
|
}
|
|
12112
12659
|
|
|
12113
12660
|
// src/commands/verify-determinism.ts
|
|
12114
|
-
import
|
|
12661
|
+
import path13 from "path";
|
|
12115
12662
|
import crypto9 from "crypto";
|
|
12116
12663
|
function normalizeArtifact(artifact) {
|
|
12117
12664
|
const normalized = JSON.parse(JSON.stringify(artifact));
|
|
@@ -12371,8 +12918,8 @@ async function executeVerifyDeterminism(targetDir, options) {
|
|
|
12371
12918
|
printCertificate(certificate, verbose);
|
|
12372
12919
|
if (out) {
|
|
12373
12920
|
const outPath = resolvePath(out);
|
|
12374
|
-
ensureDir(
|
|
12375
|
-
const certPath = outPath.endsWith(".json") ? outPath :
|
|
12921
|
+
ensureDir(path13.dirname(outPath));
|
|
12922
|
+
const certPath = outPath.endsWith(".json") ? outPath : path13.join(outPath, "determinism-cert.json");
|
|
12376
12923
|
writeFileSync(certPath, JSON.stringify(certificate, null, 2));
|
|
12377
12924
|
console.log(`Certificate written to: ${certPath}
|
|
12378
12925
|
`);
|
|
@@ -12418,7 +12965,7 @@ What it does:
|
|
|
12418
12965
|
}
|
|
12419
12966
|
|
|
12420
12967
|
// src/commands/badge.ts
|
|
12421
|
-
import
|
|
12968
|
+
import path14 from "path";
|
|
12422
12969
|
var COLORS = {
|
|
12423
12970
|
brightgreen: "#4c1",
|
|
12424
12971
|
green: "#97ca00",
|
|
@@ -12653,7 +13200,7 @@ async function executeBadge(options) {
|
|
|
12653
13200
|
const writtenFiles = [];
|
|
12654
13201
|
for (const badge of badges) {
|
|
12655
13202
|
const svg = generateSvgBadge(badge, style);
|
|
12656
|
-
const filePath =
|
|
13203
|
+
const filePath = path14.join(absoluteOutPath, badge.filename);
|
|
12657
13204
|
writeFileSync(filePath, svg);
|
|
12658
13205
|
writtenFiles.push(filePath);
|
|
12659
13206
|
console.log(` \x1B[32m\u2713\x1B[0m ${badge.filename} (${badge.label}: ${badge.message})`);
|
|
@@ -12662,8 +13209,8 @@ async function executeBadge(options) {
|
|
|
12662
13209
|
\x1B[32m${writtenFiles.length} badges generated\x1B[0m
|
|
12663
13210
|
`);
|
|
12664
13211
|
console.log(`\x1B[90mUsage in README.md:\x1B[0m`);
|
|
12665
|
-
console.log(` }/vibecheck-status.svg)`);
|
|
13213
|
+
console.log(` }/vibecheck-score.svg)
|
|
12667
13214
|
`);
|
|
12668
13215
|
return 0;
|
|
12669
13216
|
}
|