@securitychecks/cli 0.2.0 → 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/LICENSE +190 -87
- package/README.md +69 -4
- package/dist/index.js +49153 -682
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +101 -2
- package/dist/lib.js +432 -17
- package/dist/lib.js.map +1 -1
- package/package.json +8 -7
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
1480
|
-
for (const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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" ?
|
|
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
|