@securitychecks/cli 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AuditResult, Finding, CollectorArtifact, Severity, CheckResult, Artifact } from '@securitychecks/collector';
1
+ import { AuditResult, Finding, CollectorArtifact, Severity, CheckResult, Artifact, InvariantDefinition } from '@securitychecks/collector';
2
2
 
3
3
  /**
4
4
  * Audit API - Programmatic interface for running the staff check
@@ -904,6 +904,16 @@ type InvariantLike = {
904
904
  */
905
905
  declare function getStaffQuestion(invariantId: string): string | null;
906
906
  declare function generateTestSkeleton(invariant: InvariantLike | null | undefined, framework: TestFramework, context?: string): string;
907
+ /**
908
+ * Generates a structured AI fix prompt from a finding.
909
+ * Designed to be copy-pasted into any AI coding assistant.
910
+ */
911
+ declare function generateFixPrompt(invariant: InvariantLike | null | undefined, options?: {
912
+ file?: string;
913
+ line?: number;
914
+ message?: string;
915
+ framework?: TestFramework;
916
+ }): string;
907
917
 
908
918
  type Grade = 'A' | 'B' | 'C' | 'F';
909
919
  interface ReadinessScore {
@@ -927,4 +937,93 @@ declare function computeReadinessScore(summary: {
927
937
  }): ReadinessScore;
928
938
  declare function formatScoreForCli(rs: ReadinessScore): string;
929
939
 
930
- export { type AggregateCalibrationConfig, type AggregateCalibrationData, type AggregateCalibrationResult, type AttackPath, type AttackStep, type AuditOptions, BASELINE_SCHEMA_VERSION, type BaselineEntry, type BaselineFile, CLIError, type CategorizationResult, type CategorizedFinding, type CloudEvaluateOptions, type CloudEvaluateResult, type CompoundingEffect, type CorrelatedFinding, type CorrelationResult, type CorrelationStats, type CorrelationTelemetryConfig, type ErrorCode, ErrorCodes, ErrorMessages, ErrorRemediation, type EvaluationProgressCallback, type FrameworkBaseline, type Grade, type InvariantLike, type InvariantStats, type PatternStats, type ReadinessScore, SUPPORTED_SCHEMA_RANGE, type ScanTelemetry, type SchemaValidationResult, type SharedContext, type TelemetryConfig, type TestFramework, WAIVER_SCHEMA_VERSION, type WaiverEntry, type WaiverFile, addToBaseline, addWaiver, attachFindingId, attachFindingIds, audit, buildTelemetry, calculateRelativeSeverity, categorizeFindings, checkCloudHealth, clearAggregateCache, computeReadinessScore, correlateFindings, evaluateCloud, extractIdentityPayload, fetchAggregateCalibration, formatAggregateCalibrationSummary, formatCorrelatedFinding, formatCorrelationStats, formatScoreForCli, generateFindingId, generateTestSkeleton, getCIExitCode, getCISummary, getCloudInvariants, getCurrentSchemaVersion, getExpiringWaivers, getFrameworkBaseline, getScoreGrade, getSkippedPatterns, getStaffQuestion, getValidWaiver, getVerifiedCorrelations, hasCollisions, isAggregateCalibrationDisabled, isCLIError, isCloudEvalAvailable, isInBaseline, isTelemetryDisabled, loadBaseline, loadWaivers, pruneBaseline, pruneExpiredWaivers, reportCorrelationFeedback, reportCorrelations, reportTelemetry, resolveCollisions, saveBaseline, saveWaivers, shouldSkipPattern, validateSchemaVersion, wrapError };
940
+ /**
941
+ * Posture Report — invariant-by-invariant security posture view.
942
+ *
943
+ * Transforms an AuditResult into a posture view showing every invariant
944
+ * checked, whether it passed, and exportable proof for auditors/CI.
945
+ */
946
+
947
+ interface PostureReportEntry {
948
+ invariantId: string;
949
+ name: string;
950
+ category: string;
951
+ severity: Severity;
952
+ status: 'passed' | 'failed' | 'skipped';
953
+ findingCount: number;
954
+ violations?: Array<{
955
+ file: string;
956
+ line: number;
957
+ message: string;
958
+ }>;
959
+ }
960
+ interface PostureSummary {
961
+ invariantsChecked: number;
962
+ passed: number;
963
+ failed: number;
964
+ skipped: number;
965
+ coveragePercent: number;
966
+ byCategory: Record<string, {
967
+ total: number;
968
+ passed: number;
969
+ failed: number;
970
+ }>;
971
+ bySeverity: Record<string, {
972
+ total: number;
973
+ passed: number;
974
+ failed: number;
975
+ }>;
976
+ }
977
+ interface PostureProofArtifact {
978
+ version: '1.0';
979
+ metadata: {
980
+ generatedAt: string;
981
+ generatedBy: string;
982
+ cliVersion: string;
983
+ targetPath: string;
984
+ scanDurationMs: number;
985
+ filesAnalyzed: number;
986
+ scope: {
987
+ totalInvariantsAvailable: number;
988
+ invariantsChecked: number;
989
+ onlyFilter?: string[];
990
+ skipFilter?: string[];
991
+ };
992
+ };
993
+ score: {
994
+ value: number;
995
+ grade: string;
996
+ hasP0: boolean;
997
+ };
998
+ summary: PostureSummary;
999
+ invariants: PostureReportEntry[];
1000
+ }
1001
+ interface PostureOptions {
1002
+ only?: string[];
1003
+ skip?: string[];
1004
+ }
1005
+ /**
1006
+ * Build posture entries from an AuditResult + full invariant catalog.
1007
+ *
1008
+ * - Invariants with CheckResults → passed/failed based on findings.
1009
+ * - Invariants not in results → "skipped" (honest about scope).
1010
+ * - Respects --only / --skip filters.
1011
+ */
1012
+ declare function buildPostureEntries(result: AuditResult, allInvariants: InvariantDefinition[], options?: PostureOptions): PostureReportEntry[];
1013
+ /**
1014
+ * Aggregate posture entries into a summary.
1015
+ */
1016
+ declare function computePostureSummary(entries: PostureReportEntry[]): PostureSummary;
1017
+ /**
1018
+ * Build the exportable proof artifact (JSON).
1019
+ */
1020
+ declare function buildPostureProofArtifact(result: AuditResult, readiness: ReadinessScore, entries: PostureReportEntry[], summary: PostureSummary, allInvariantsCount: number, options?: PostureOptions): PostureProofArtifact;
1021
+ /**
1022
+ * Render the posture report to the terminal.
1023
+ *
1024
+ * Grouped by category, severity badges color-coded, PASS/FAIL with count.
1025
+ * No individual violation details — that's what the default view is for.
1026
+ */
1027
+ declare function displayPosture(entries: PostureReportEntry[], summary: PostureSummary, readiness: ReadinessScore): void;
1028
+
1029
+ export { type AggregateCalibrationConfig, type AggregateCalibrationData, type AggregateCalibrationResult, type AttackPath, type AttackStep, type AuditOptions, BASELINE_SCHEMA_VERSION, type BaselineEntry, type BaselineFile, CLIError, type CategorizationResult, type CategorizedFinding, type CloudEvaluateOptions, type CloudEvaluateResult, type CompoundingEffect, type CorrelatedFinding, type CorrelationResult, type CorrelationStats, type CorrelationTelemetryConfig, type ErrorCode, ErrorCodes, ErrorMessages, ErrorRemediation, type EvaluationProgressCallback, type FrameworkBaseline, type Grade, type InvariantLike, type InvariantStats, type PatternStats, type PostureProofArtifact, type PostureReportEntry, type PostureSummary, type ReadinessScore, SUPPORTED_SCHEMA_RANGE, type ScanTelemetry, type SchemaValidationResult, type SharedContext, type TelemetryConfig, type TestFramework, WAIVER_SCHEMA_VERSION, type WaiverEntry, type WaiverFile, addToBaseline, addWaiver, attachFindingId, attachFindingIds, audit, buildPostureEntries, buildPostureProofArtifact, buildTelemetry, calculateRelativeSeverity, categorizeFindings, checkCloudHealth, clearAggregateCache, computePostureSummary, computeReadinessScore, correlateFindings, displayPosture, evaluateCloud, extractIdentityPayload, fetchAggregateCalibration, formatAggregateCalibrationSummary, formatCorrelatedFinding, formatCorrelationStats, formatScoreForCli, generateFindingId, generateFixPrompt, generateTestSkeleton, getCIExitCode, getCISummary, getCloudInvariants, getCurrentSchemaVersion, getExpiringWaivers, getFrameworkBaseline, getScoreGrade, getSkippedPatterns, getStaffQuestion, getValidWaiver, getVerifiedCorrelations, hasCollisions, isAggregateCalibrationDisabled, isCLIError, isCloudEvalAvailable, isInBaseline, isTelemetryDisabled, loadBaseline, loadWaivers, pruneBaseline, pruneExpiredWaivers, reportCorrelationFeedback, reportCorrelations, reportTelemetry, resolveCollisions, saveBaseline, saveWaivers, shouldSkipPattern, validateSchemaVersion, wrapError };
package/dist/lib.js CHANGED
@@ -5,7 +5,7 @@ import { homedir } from 'os';
5
5
  import { join, dirname } from 'path';
6
6
  import { gzipSync } from 'zlib';
7
7
  import { randomUUID, createHash } from 'crypto';
8
- import pc from 'picocolors';
8
+ import pc2 from 'picocolors';
9
9
 
10
10
  // src/audit.ts
11
11
  var CONFIG_DIR = join(homedir(), ".securitychecks");
@@ -1070,7 +1070,7 @@ function createEmptyWaiverFile(version = "0.0.0") {
1070
1070
  entries: {}
1071
1071
  };
1072
1072
  }
1073
- var CLI_VERSION = "0.2.2";
1073
+ var CLI_VERSION = "0.3.0";
1074
1074
  var SCHECK_DIR = ".scheck";
1075
1075
  var BASELINE_FILE = "baseline.json";
1076
1076
  var WAIVER_FILE = "waivers.json";
@@ -1448,6 +1448,132 @@ var COMPOUNDING_RULES = [
1448
1448
  impact: "high",
1449
1449
  timeWindow: "Until cache expires"
1450
1450
  }
1451
+ },
1452
+ // SQL Injection + Tenant Isolation = Cross-tenant data access
1453
+ {
1454
+ invariants: ["DATAFLOW.UNTRUSTED.SQL_QUERY", "AUTHZ.TENANT.ISOLATION"],
1455
+ effect: {
1456
+ description: "SQL injection bypasses tenant isolation allowing cross-tenant data exfiltration",
1457
+ riskMultiplier: 3,
1458
+ signals: ["sqli", "tenant_bypass", "cross_tenant_data"]
1459
+ },
1460
+ attackPathTemplate: {
1461
+ title: "Cross-Tenant Data Exfiltration via SQL Injection",
1462
+ exploitability: "medium",
1463
+ impact: "critical"
1464
+ }
1465
+ },
1466
+ // XSS + No HttpOnly Cookie = Session Hijacking
1467
+ {
1468
+ invariants: ["XSS.DOM.SINK", "SESSION.COOKIE.NO_HTTPONLY"],
1469
+ effect: {
1470
+ description: "XSS can steal session cookies when HttpOnly flag is missing, enabling session hijacking",
1471
+ riskMultiplier: 2.3,
1472
+ signals: ["xss", "cookie_theft", "session_hijack"]
1473
+ },
1474
+ attackPathTemplate: {
1475
+ title: "Session Hijacking via XSS Cookie Theft",
1476
+ exploitability: "easy",
1477
+ impact: "critical"
1478
+ }
1479
+ },
1480
+ // IDOR + Missing Service Auth = Enumeration attack
1481
+ {
1482
+ invariants: ["IDOR.SEQUENTIAL_ID", "AUTHZ.SERVICE_LAYER.ENFORCED"],
1483
+ effect: {
1484
+ description: "Sequential IDs make resource enumeration trivial when service-layer auth is missing",
1485
+ riskMultiplier: 2.2,
1486
+ signals: ["idor", "enumeration", "auth_bypass"]
1487
+ },
1488
+ attackPathTemplate: {
1489
+ title: "Resource Enumeration via Sequential IDs",
1490
+ exploitability: "easy",
1491
+ impact: "high"
1492
+ }
1493
+ },
1494
+ // Payment No Idempotency + Race Condition = Double Charges
1495
+ {
1496
+ invariants: ["PAYMENT.NO_IDEMPOTENCY", "RACE.BALANCE_CHECK"],
1497
+ effect: {
1498
+ description: "Non-idempotent payment endpoint + race condition on balance allows double charges and overdraft",
1499
+ riskMultiplier: 2.5,
1500
+ signals: ["double_charge", "race_condition", "overdraft"]
1501
+ },
1502
+ attackPathTemplate: {
1503
+ title: "Double Charge via Payment Race Condition",
1504
+ exploitability: "medium",
1505
+ impact: "critical"
1506
+ }
1507
+ },
1508
+ // MFA Bypass + Password Reset No Expire = Account Takeover
1509
+ {
1510
+ invariants: ["AUTH.MFA_BYPASS", "AUTH.PASSWORD_RESET_NO_EXPIRE"],
1511
+ effect: {
1512
+ description: "MFA bypass combined with non-expiring password reset tokens enables full account takeover",
1513
+ riskMultiplier: 2.8,
1514
+ signals: ["mfa_bypass", "token_reuse", "account_takeover"]
1515
+ },
1516
+ attackPathTemplate: {
1517
+ title: "Account Takeover via MFA Bypass",
1518
+ exploitability: "medium",
1519
+ impact: "critical"
1520
+ }
1521
+ },
1522
+ // Middleware Bypass + Missing Service Auth = Unprotected Access
1523
+ {
1524
+ invariants: ["NEXTJS.MIDDLEWARE_BYPASS", "AUTHZ.SERVICE_LAYER.ENFORCED"],
1525
+ effect: {
1526
+ description: "Middleware bypass combined with missing service-layer auth leaves routes completely unprotected",
1527
+ riskMultiplier: 2.4,
1528
+ signals: ["middleware_bypass", "auth_gap", "unprotected_route"]
1529
+ },
1530
+ attackPathTemplate: {
1531
+ title: "Complete Auth Bypass via Middleware Gap",
1532
+ exploitability: "easy",
1533
+ impact: "critical"
1534
+ }
1535
+ },
1536
+ // Billing + Client Feature Flag = Free Premium Access
1537
+ {
1538
+ invariants: ["BILLING.SERVER_ENFORCED", "FEATURE.FLAG_CLIENT_CONTROLLED"],
1539
+ effect: {
1540
+ description: "Client-controlled feature flags combined with missing server billing enforcement allows free premium access",
1541
+ riskMultiplier: 2.3,
1542
+ signals: ["billing_bypass", "client_flag", "premium_theft"]
1543
+ },
1544
+ attackPathTemplate: {
1545
+ title: "Free Premium Access via Client Feature Flag",
1546
+ exploitability: "easy",
1547
+ impact: "high"
1548
+ }
1549
+ },
1550
+ // XXE + SSRF = Internal Network Compromise
1551
+ {
1552
+ invariants: ["XXE.EXTERNAL_ENTITY", "SSRF.URL"],
1553
+ effect: {
1554
+ description: "XXE enables fetching internal URLs via SSRF, accessing cloud metadata and internal services",
1555
+ riskMultiplier: 3,
1556
+ signals: ["xxe", "ssrf", "internal_access", "metadata_leak"]
1557
+ },
1558
+ attackPathTemplate: {
1559
+ title: "Internal Network Access via XXE-SSRF Chain",
1560
+ exploitability: "medium",
1561
+ impact: "critical"
1562
+ }
1563
+ },
1564
+ // Soft Delete Bypass + IDOR = Accessing Deleted Data
1565
+ {
1566
+ invariants: ["DATA.SOFT_DELETE_BYPASS", "IDOR.SEQUENTIAL_ID"],
1567
+ effect: {
1568
+ description: "Soft-deleted records remain accessible via sequential ID enumeration",
1569
+ riskMultiplier: 2.1,
1570
+ signals: ["soft_delete", "idor", "deleted_data_leak"]
1571
+ },
1572
+ attackPathTemplate: {
1573
+ title: "Deleted Data Access via IDOR Enumeration",
1574
+ exploitability: "easy",
1575
+ impact: "high"
1576
+ }
1451
1577
  }
1452
1578
  ];
1453
1579
  function correlateFindings(results, artifact) {
@@ -1463,9 +1589,10 @@ function correlateFindings(results, artifact) {
1463
1589
  }
1464
1590
  };
1465
1591
  }
1466
- const groups = groupFindingsByLocation(allFindings);
1467
1592
  const correlations = [];
1468
1593
  let severityEscalations = 0;
1594
+ const usedFindingIds = /* @__PURE__ */ new Set();
1595
+ const groups = groupFindingsByLocation(allFindings);
1469
1596
  for (const group of groups.values()) {
1470
1597
  if (group.length < 2) continue;
1471
1598
  const correlation = findCorrelation(group);
@@ -1474,20 +1601,63 @@ function correlateFindings(results, artifact) {
1474
1601
  if (severityToNumber(correlation.adjustedSeverity) > severityToNumber(correlation.primary.severity)) {
1475
1602
  severityEscalations++;
1476
1603
  }
1604
+ usedFindingIds.add(findingId(correlation.primary));
1605
+ for (const r of correlation.related) {
1606
+ usedFindingIds.add(findingId(r));
1607
+ }
1477
1608
  }
1478
1609
  }
1479
- const correlatedFindingIds = /* @__PURE__ */ new Set();
1480
- for (const c of correlations) {
1481
- correlatedFindingIds.add(findingId(c.primary));
1482
- for (const r of c.related) {
1483
- correlatedFindingIds.add(findingId(r));
1610
+ const allInvariantIds = new Set(allFindings.map((f) => f.invariantId));
1611
+ for (const rule of COMPOUNDING_RULES) {
1612
+ const matchedInvariants = rule.invariants.filter((inv) => allInvariantIds.has(inv));
1613
+ if (matchedInvariants.length < 2) continue;
1614
+ const representatives = [];
1615
+ for (const inv of matchedInvariants) {
1616
+ const finding = allFindings.find(
1617
+ (f) => f.invariantId === inv && !usedFindingIds.has(findingId(f))
1618
+ );
1619
+ if (finding) representatives.push(finding);
1620
+ }
1621
+ if (representatives.length < 2) continue;
1622
+ representatives.sort(
1623
+ (a, b) => severityToNumber(b.severity) - severityToNumber(a.severity)
1624
+ );
1625
+ const primary = representatives[0];
1626
+ const related = representatives.slice(1);
1627
+ const evidence = primary.evidence[0];
1628
+ const adjustedSeverity = calculateAdjustedSeverity(
1629
+ primary.severity,
1630
+ rule.effect.riskMultiplier
1631
+ );
1632
+ let attackPath;
1633
+ if (rule.attackPathTemplate) {
1634
+ attackPath = buildAttackPath(rule.attackPathTemplate, representatives);
1635
+ }
1636
+ correlations.push({
1637
+ primary,
1638
+ related,
1639
+ sharedContext: {
1640
+ file: evidence?.file,
1641
+ functionName: evidence?.symbol,
1642
+ findingCount: representatives.length
1643
+ },
1644
+ compoundingEffect: rule.effect,
1645
+ adjustedSeverity,
1646
+ attackPath
1647
+ });
1648
+ if (severityToNumber(adjustedSeverity) > severityToNumber(primary.severity)) {
1649
+ severityEscalations++;
1650
+ }
1651
+ usedFindingIds.add(findingId(primary));
1652
+ for (const r of related) {
1653
+ usedFindingIds.add(findingId(r));
1484
1654
  }
1485
1655
  }
1486
1656
  return {
1487
1657
  correlations,
1488
1658
  stats: {
1489
1659
  totalFindings: allFindings.length,
1490
- correlatedFindings: correlatedFindingIds.size,
1660
+ correlatedFindings: usedFindingIds.size,
1491
1661
  correlationGroups: correlations.length,
1492
1662
  severityEscalations
1493
1663
  }
@@ -1683,7 +1853,7 @@ function formatCorrelationStats(result) {
1683
1853
  return `
1684
1854
  Correlation Analysis:
1685
1855
  Total findings: ${stats.totalFindings}
1686
- Correlated: ${stats.correlatedFindings} (${Math.round(stats.correlatedFindings / stats.totalFindings * 100)}%)
1856
+ Correlated: ${stats.correlatedFindings} (${stats.totalFindings > 0 ? Math.round(stats.correlatedFindings / stats.totalFindings * 100) : 0}%)
1687
1857
  Correlation groups: ${stats.correlationGroups}
1688
1858
  Severity escalations: ${stats.severityEscalations}
1689
1859
  `.trim();
@@ -1719,7 +1889,7 @@ function toObservation(correlation, framework) {
1719
1889
  signals: correlation.compoundingEffect.signals
1720
1890
  },
1721
1891
  meta: {
1722
- clientVersion: "0.2.2",
1892
+ clientVersion: "0.3.0",
1723
1893
  requestId: randomUUID(),
1724
1894
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1725
1895
  }
@@ -1737,7 +1907,7 @@ async function reportCorrelations(result, config, framework) {
1737
1907
  correlations: observations,
1738
1908
  summary: result.stats,
1739
1909
  meta: {
1740
- clientVersion: "0.2.2",
1910
+ clientVersion: "0.3.0",
1741
1911
  framework
1742
1912
  }
1743
1913
  };
@@ -1749,7 +1919,7 @@ async function reportCorrelations(result, config, framework) {
1749
1919
  headers: {
1750
1920
  "Content-Type": "application/json",
1751
1921
  ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` },
1752
- "X-Client-Version": "0.2.2"
1922
+ "X-Client-Version": "0.3.0"
1753
1923
  },
1754
1924
  body: JSON.stringify(payload),
1755
1925
  signal: controller.signal
@@ -1825,7 +1995,7 @@ function buildTelemetry(result, options) {
1825
1995
  } : void 0,
1826
1996
  meta: {
1827
1997
  duration: result.duration,
1828
- clientVersion: "0.2.2",
1998
+ clientVersion: "0.3.0",
1829
1999
  mode: options.mode ?? (ciProvider ? "ci" : "manual"),
1830
2000
  ciProvider
1831
2001
  },
@@ -2056,7 +2226,11 @@ function getStaffQuestion(invariantId) {
2056
2226
  "DATAFLOW.UNTRUSTED.FILE_ACCESS": "Can user input control file paths or write locations here?",
2057
2227
  "DATAFLOW.UNTRUSTED.RESPONSE": "Can user input drive redirects or HTML output without sanitization?",
2058
2228
  "SECRETS.HARDCODED": "If this repo were accidentally made public, what credentials would be exposed?",
2059
- "CRYPTO.ALGORITHM.STRONG": "Is this encryption strong enough for the data it protects?"
2229
+ "CRYPTO.ALGORITHM.STRONG": "Is this encryption strong enough for the data it protects?",
2230
+ "CONFIG.ENV_HARDCODED": "What happens when this runs in staging vs production?",
2231
+ "CONFIG.HEALTH_CHECK_MISSING": "How does the load balancer know this instance is healthy?",
2232
+ "CONFIG.ERROR_STACK_LEAK": "If this endpoint throws, what does the user see?",
2233
+ "CONFIG.GRACEFUL_SHUTDOWN_MISSING": "What happens to in-flight requests when this pod is terminated?"
2060
2234
  };
2061
2235
  return questions[invariantId] ?? null;
2062
2236
  }
@@ -2159,6 +2333,61 @@ ${testFn}('sends side effects only after successful commit', async () => {
2159
2333
  // Assert: email was sent
2160
2334
  expect(emailSpy).toHaveBeenCalledOnce();
2161
2335
  });
2336
+ `);
2337
+ case "CONFIG.ENV_HARDCODED":
2338
+ return wrap(`
2339
+ ${testFn}('does not contain hardcoded environment values', () => {
2340
+ // Grep source files for hardcoded URLs and connection strings
2341
+ const sourceFiles = getSourceFiles('src/');
2342
+ const hardcodedPatterns = [
2343
+ /localhost:\\d+/,
2344
+ /127\\.0\\.0\\.1:\\d+/,
2345
+ /mongodb:\\/\\//,
2346
+ /postgres:\\/\\//,
2347
+ /redis:\\/\\//,
2348
+ ];
2349
+
2350
+ for (const file of sourceFiles) {
2351
+ for (const pattern of hardcodedPatterns) {
2352
+ expect(file.content).not.toMatch(pattern);
2353
+ }
2354
+ }
2355
+ });
2356
+ `);
2357
+ case "CONFIG.HEALTH_CHECK_MISSING":
2358
+ return wrap(`
2359
+ ${testFn}('exposes a health check endpoint', async () => {
2360
+ const response = await request(app).get('/health');
2361
+ expect(response.status).toBe(200);
2362
+ });
2363
+ `);
2364
+ case "CONFIG.ERROR_STACK_LEAK":
2365
+ return wrap(`
2366
+ ${testFn}('does not leak stack traces in production errors', async () => {
2367
+ // Arrange: force an internal error
2368
+ const response = await request(app)
2369
+ .get('/api/will-throw')
2370
+ .set('Accept', 'application/json');
2371
+
2372
+ // Assert: response body does not contain stack trace
2373
+ expect(response.body.stack).toBeUndefined();
2374
+ expect(JSON.stringify(response.body)).not.toMatch(/at \\w+\\s*\\(/);
2375
+ });
2376
+ `);
2377
+ case "CONFIG.GRACEFUL_SHUTDOWN_MISSING":
2378
+ return wrap(`
2379
+ ${testFn}('handles SIGTERM gracefully', async () => {
2380
+ // Arrange: start server and make in-flight request
2381
+ const server = startServer();
2382
+ const inflightRequest = fetch(server.url + '/slow-endpoint');
2383
+
2384
+ // Act: send SIGTERM
2385
+ process.kill(server.pid, 'SIGTERM');
2386
+
2387
+ // Assert: in-flight request completes successfully
2388
+ const response = await inflightRequest;
2389
+ expect(response.ok).toBe(true);
2390
+ });
2162
2391
  `);
2163
2392
  default: {
2164
2393
  const proof = invariant.requiredProof ? invariant.requiredProof : "(see invariant docs)";
@@ -2175,6 +2404,43 @@ ${testFn}('enforces ${invariant.id}', async () => {
2175
2404
  }
2176
2405
  }
2177
2406
  }
2407
+ function generateFixPrompt(invariant, options) {
2408
+ if (!invariant) return "Unknown invariant";
2409
+ const framework = options?.framework || "vitest";
2410
+ const severity = invariant.severity || "P1";
2411
+ const staffQuestion = getStaffQuestion(invariant.id);
2412
+ const testSkeleton = generateTestSkeleton(invariant, framework);
2413
+ const lines = [
2414
+ `## Security Issue: ${invariant.name} [${severity}]`,
2415
+ ""
2416
+ ];
2417
+ lines.push("### What was found");
2418
+ if (options?.file) {
2419
+ lines.push(`File: ${options.file}${options.line ? `:${options.line}` : ""}`);
2420
+ }
2421
+ if (options?.message) {
2422
+ lines.push(options.message);
2423
+ }
2424
+ if (!options?.file && !options?.message) {
2425
+ lines.push(`Invariant \`${invariant.id}\` is violated.`);
2426
+ }
2427
+ lines.push("");
2428
+ lines.push("### What's required");
2429
+ lines.push(invariant.requiredProof || `Satisfy the ${invariant.id} invariant.`);
2430
+ lines.push("");
2431
+ if (staffQuestion) {
2432
+ lines.push("### Staff engineer asks");
2433
+ lines.push(`"${staffQuestion}"`);
2434
+ lines.push("");
2435
+ }
2436
+ lines.push("### Test to prove the fix");
2437
+ lines.push("```typescript");
2438
+ lines.push(testSkeleton);
2439
+ lines.push("```");
2440
+ lines.push("");
2441
+ lines.push("Fix the code to satisfy this invariant, then run the test to verify.");
2442
+ return lines.join("\n");
2443
+ }
2178
2444
  function indent(text, spaces) {
2179
2445
  const prefix = " ".repeat(spaces);
2180
2446
  return text.split("\n").map((line) => line ? `${prefix}${line}` : line).join("\n");
@@ -2208,10 +2474,159 @@ function computeReadinessScore(summary) {
2208
2474
  };
2209
2475
  }
2210
2476
  function formatScoreForCli(rs) {
2211
- const gradeColor = rs.grade === "A" ? pc.green : rs.grade === "B" ? pc.yellow : rs.grade === "C" ? pc.red : pc.red;
2477
+ const gradeColor = rs.grade === "A" ? pc2.green : rs.grade === "B" ? pc2.yellow : rs.grade === "C" ? pc2.red : pc2.red;
2212
2478
  return gradeColor(`Score: ${rs.score}/100 (${rs.grade})`);
2213
2479
  }
2480
+ function buildPostureEntries(result, allInvariants, options) {
2481
+ let invariantsInScope = allInvariants;
2482
+ if (options?.only && options.only.length > 0) {
2483
+ const onlySet = new Set(options.only);
2484
+ invariantsInScope = invariantsInScope.filter((inv) => onlySet.has(inv.id));
2485
+ }
2486
+ if (options?.skip && options.skip.length > 0) {
2487
+ const skipSet = new Set(options.skip);
2488
+ invariantsInScope = invariantsInScope.filter((inv) => !skipSet.has(inv.id));
2489
+ }
2490
+ const resultMap = /* @__PURE__ */ new Map();
2491
+ for (const r of result.results) {
2492
+ resultMap.set(r.invariantId, r);
2493
+ }
2494
+ return invariantsInScope.map((inv) => {
2495
+ const checkResult = resultMap.get(inv.id);
2496
+ if (!checkResult) {
2497
+ return {
2498
+ invariantId: inv.id,
2499
+ name: inv.name,
2500
+ category: inv.category,
2501
+ severity: inv.severity,
2502
+ status: "skipped",
2503
+ findingCount: 0
2504
+ };
2505
+ }
2506
+ const failed = checkResult.findings.length > 0;
2507
+ const violations = failed ? checkResult.findings.flatMap(
2508
+ (f) => f.evidence.map((e) => ({
2509
+ file: e.file,
2510
+ line: e.line,
2511
+ message: f.message
2512
+ }))
2513
+ ) : void 0;
2514
+ return {
2515
+ invariantId: inv.id,
2516
+ name: inv.name,
2517
+ category: inv.category,
2518
+ severity: inv.severity,
2519
+ status: failed ? "failed" : "passed",
2520
+ findingCount: checkResult.findings.length,
2521
+ violations
2522
+ };
2523
+ });
2524
+ }
2525
+ function computePostureSummary(entries) {
2526
+ const passed = entries.filter((e) => e.status === "passed").length;
2527
+ const failed = entries.filter((e) => e.status === "failed").length;
2528
+ const skipped = entries.filter((e) => e.status === "skipped").length;
2529
+ const checked = passed + failed;
2530
+ const byCategory = {};
2531
+ const bySeverity = {};
2532
+ for (const entry of entries) {
2533
+ if (!byCategory[entry.category]) {
2534
+ byCategory[entry.category] = { total: 0, passed: 0, failed: 0 };
2535
+ }
2536
+ const cat = byCategory[entry.category];
2537
+ cat.total++;
2538
+ if (entry.status === "passed") cat.passed++;
2539
+ if (entry.status === "failed") cat.failed++;
2540
+ if (!bySeverity[entry.severity]) {
2541
+ bySeverity[entry.severity] = { total: 0, passed: 0, failed: 0 };
2542
+ }
2543
+ const sev = bySeverity[entry.severity];
2544
+ sev.total++;
2545
+ if (entry.status === "passed") sev.passed++;
2546
+ if (entry.status === "failed") sev.failed++;
2547
+ }
2548
+ return {
2549
+ invariantsChecked: checked,
2550
+ passed,
2551
+ failed,
2552
+ skipped,
2553
+ coveragePercent: entries.length > 0 ? Math.round(100 * checked / entries.length) : 0,
2554
+ byCategory,
2555
+ bySeverity
2556
+ };
2557
+ }
2558
+ function buildPostureProofArtifact(result, readiness, entries, summary, allInvariantsCount, options) {
2559
+ const cliVersion = "0.3.0";
2560
+ const filesAnalyzed = result.artifact?.services?.length ?? 0;
2561
+ return {
2562
+ version: "1.0",
2563
+ metadata: {
2564
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2565
+ generatedBy: "securitychecks-cli",
2566
+ cliVersion,
2567
+ targetPath: result.targetPath,
2568
+ scanDurationMs: result.duration,
2569
+ filesAnalyzed,
2570
+ scope: {
2571
+ totalInvariantsAvailable: allInvariantsCount,
2572
+ invariantsChecked: summary.invariantsChecked,
2573
+ ...options?.only && options.only.length > 0 ? { onlyFilter: options.only } : {},
2574
+ ...options?.skip && options.skip.length > 0 ? { skipFilter: options.skip } : {}
2575
+ }
2576
+ },
2577
+ score: {
2578
+ value: readiness.score,
2579
+ grade: readiness.grade,
2580
+ hasP0: readiness.hasP0
2581
+ },
2582
+ summary,
2583
+ invariants: entries
2584
+ };
2585
+ }
2586
+ function displayPosture(entries, summary, readiness) {
2587
+ console.log(pc2.bold("\nPosture Report\n"));
2588
+ const byCategory = /* @__PURE__ */ new Map();
2589
+ for (const entry of entries) {
2590
+ if (!byCategory.has(entry.category)) {
2591
+ byCategory.set(entry.category, []);
2592
+ }
2593
+ byCategory.get(entry.category).push(entry);
2594
+ }
2595
+ for (const [category, catEntries] of byCategory) {
2596
+ const checked2 = catEntries.filter((e) => e.status !== "skipped").length;
2597
+ const label = category.charAt(0).toUpperCase() + category.slice(1);
2598
+ console.log(` ${pc2.bold(label)} (${checked2} checked)`);
2599
+ const severityOrder = { P0: 0, P1: 1, P2: 2 };
2600
+ const sorted = [...catEntries].sort(
2601
+ (a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3)
2602
+ );
2603
+ for (const entry of sorted) {
2604
+ const sevColor = entry.severity === "P0" ? pc2.red : entry.severity === "P1" ? pc2.yellow : pc2.dim;
2605
+ const sevBadge = sevColor(`[${entry.severity}]`);
2606
+ const id = entry.invariantId.padEnd(44);
2607
+ if (entry.status === "skipped") {
2608
+ console.log(` ${sevBadge} ${pc2.dim(id)}${pc2.dim("SKIP")}`);
2609
+ } else if (entry.status === "passed") {
2610
+ console.log(` ${sevBadge} ${id}${pc2.green("PASS")}`);
2611
+ } else {
2612
+ const count = entry.findingCount;
2613
+ const violationLabel = count === 1 ? "1 violation" : `${count} violations`;
2614
+ console.log(` ${sevBadge} ${id}${pc2.red("FAIL")} ${pc2.red(violationLabel)}`);
2615
+ }
2616
+ }
2617
+ console.log("");
2618
+ }
2619
+ const total = entries.length;
2620
+ const checked = summary.invariantsChecked;
2621
+ const coverage = summary.coveragePercent;
2622
+ console.log(` Checked: ${checked}/${total} invariants (${coverage}%)`);
2623
+ console.log(` Passed: ${summary.passed}/${checked} Failed: ${summary.failed}/${checked}`);
2624
+ console.log("");
2625
+ const gradeColor = readiness.grade === "A" ? pc2.green : readiness.grade === "B" ? pc2.yellow : pc2.red;
2626
+ console.log(` ${gradeColor(`Score: ${readiness.score}/100 (${readiness.grade})`)}`);
2627
+ console.log("");
2628
+ }
2214
2629
 
2215
- export { BASELINE_SCHEMA_VERSION, CLIError, ErrorCodes, ErrorMessages, ErrorRemediation, SUPPORTED_SCHEMA_RANGE, WAIVER_SCHEMA_VERSION, addToBaseline, addWaiver, attachFindingId, attachFindingIds, audit, buildTelemetry, calculateRelativeSeverity, categorizeFindings, checkCloudHealth, clearAggregateCache, computeReadinessScore, correlateFindings, evaluateCloud, extractIdentityPayload, fetchAggregateCalibration, formatAggregateCalibrationSummary, formatCorrelatedFinding, formatCorrelationStats, formatScoreForCli, generateFindingId, generateTestSkeleton, getCIExitCode, getCISummary, getCloudInvariants, getCurrentSchemaVersion, getExpiringWaivers, getFrameworkBaseline, getScoreGrade, getSkippedPatterns, getStaffQuestion, getValidWaiver, getVerifiedCorrelations, hasCollisions, isAggregateCalibrationDisabled, isCLIError, isCloudEvalAvailable, isInBaseline, isTelemetryDisabled, loadBaseline, loadWaivers, pruneBaseline, pruneExpiredWaivers, reportCorrelationFeedback, reportCorrelations, reportTelemetry, resolveCollisions, saveBaseline, saveWaivers, shouldSkipPattern, validateSchemaVersion, wrapError };
2630
+ export { BASELINE_SCHEMA_VERSION, CLIError, ErrorCodes, ErrorMessages, ErrorRemediation, SUPPORTED_SCHEMA_RANGE, WAIVER_SCHEMA_VERSION, addToBaseline, addWaiver, attachFindingId, attachFindingIds, audit, buildPostureEntries, buildPostureProofArtifact, buildTelemetry, calculateRelativeSeverity, categorizeFindings, checkCloudHealth, clearAggregateCache, computePostureSummary, computeReadinessScore, correlateFindings, displayPosture, evaluateCloud, extractIdentityPayload, fetchAggregateCalibration, formatAggregateCalibrationSummary, formatCorrelatedFinding, formatCorrelationStats, formatScoreForCli, generateFindingId, generateFixPrompt, generateTestSkeleton, getCIExitCode, getCISummary, getCloudInvariants, getCurrentSchemaVersion, getExpiringWaivers, getFrameworkBaseline, getScoreGrade, getSkippedPatterns, getStaffQuestion, getValidWaiver, getVerifiedCorrelations, hasCollisions, isAggregateCalibrationDisabled, isCLIError, isCloudEvalAvailable, isInBaseline, isTelemetryDisabled, loadBaseline, loadWaivers, pruneBaseline, pruneExpiredWaivers, reportCorrelationFeedback, reportCorrelations, reportTelemetry, resolveCollisions, saveBaseline, saveWaivers, shouldSkipPattern, validateSchemaVersion, wrapError };
2216
2631
  //# sourceMappingURL=lib.js.map
2217
2632
  //# sourceMappingURL=lib.js.map