cto-ai-cli 3.1.0 → 3.2.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/DOCS.md +151 -0
- package/README.md +124 -15
- package/dist/action/index.js +23 -1
- package/dist/api/dashboard.js +23 -1
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +23 -1
- package/dist/api/server.js.map +1 -1
- package/dist/cli/score.js +341 -2
- package/dist/cli/v2/index.js +23 -1
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.js +23 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/govern/index.d.ts +25 -2
- package/dist/govern/index.js +155 -1
- package/dist/govern/index.js.map +1 -1
- package/dist/interact/index.js +23 -1
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +23 -1
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/score.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/score.ts
|
|
4
4
|
import { resolve as resolve4, join as join4 } from "path";
|
|
5
|
-
import { mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, appendFileSync } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/engine/analyzer.ts
|
|
8
8
|
import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
|
|
@@ -737,7 +737,29 @@ var BUILTIN_PATTERNS = [
|
|
|
737
737
|
{ type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
|
|
738
738
|
{ type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
|
|
739
739
|
// Environment variables with secrets
|
|
740
|
-
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
|
|
740
|
+
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
|
|
741
|
+
// Stripe
|
|
742
|
+
{ type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
|
|
743
|
+
{ type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
|
|
744
|
+
{ type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
|
|
745
|
+
// Slack
|
|
746
|
+
{ type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
|
|
747
|
+
{ type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
|
|
748
|
+
{ type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
|
|
749
|
+
// Google
|
|
750
|
+
{ type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
|
|
751
|
+
{ type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
|
|
752
|
+
// Azure
|
|
753
|
+
{ type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
|
|
754
|
+
// Twilio
|
|
755
|
+
{ type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
|
|
756
|
+
// SendGrid
|
|
757
|
+
{ type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
|
|
758
|
+
// JWT
|
|
759
|
+
{ type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
|
|
760
|
+
// PII
|
|
761
|
+
{ type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
|
|
762
|
+
{ type: "pii", source: "\\b\\d{3}[-.]?\\d{2}[-.]?\\d{4}\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
|
|
741
763
|
];
|
|
742
764
|
function buildPatterns(customPatterns = []) {
|
|
743
765
|
const patterns = BUILTIN_PATTERNS.map((def) => ({
|
|
@@ -827,6 +849,136 @@ function deduplicateFindings(findings) {
|
|
|
827
849
|
return true;
|
|
828
850
|
});
|
|
829
851
|
}
|
|
852
|
+
function shannonEntropy(str) {
|
|
853
|
+
const freq = /* @__PURE__ */ new Map();
|
|
854
|
+
for (const ch of str) {
|
|
855
|
+
freq.set(ch, (freq.get(ch) || 0) + 1);
|
|
856
|
+
}
|
|
857
|
+
let entropy = 0;
|
|
858
|
+
for (const count of freq.values()) {
|
|
859
|
+
const p = count / str.length;
|
|
860
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
861
|
+
}
|
|
862
|
+
return entropy;
|
|
863
|
+
}
|
|
864
|
+
var HIGH_ENTROPY_RE = /['"]([a-zA-Z0-9+/=_\-]{30,})['"]|=\s*['"]?([a-zA-Z0-9+/=_\-]{30,})['"]?/g;
|
|
865
|
+
var ENTROPY_SKIP = [
|
|
866
|
+
/^[a-f0-9]{32,}$/i,
|
|
867
|
+
// hex hashes
|
|
868
|
+
/^[A-Z_]{30,}$/,
|
|
869
|
+
// all-caps constants
|
|
870
|
+
/^[a-z_]{30,}$/,
|
|
871
|
+
// all-lowercase identifiers
|
|
872
|
+
/^[a-zA-Z0-9+/]+=+$/,
|
|
873
|
+
// base64 padding
|
|
874
|
+
/^[a-z]+[A-Z][a-zA-Z]+$/,
|
|
875
|
+
// camelCase identifiers
|
|
876
|
+
/sha\d+-/i
|
|
877
|
+
// integrity hashes (sha256-, sha512-)
|
|
878
|
+
];
|
|
879
|
+
function scanContentForHighEntropy(content, filePath, threshold = 5) {
|
|
880
|
+
const findings = [];
|
|
881
|
+
const lines = content.split("\n");
|
|
882
|
+
for (let i = 0; i < lines.length; i++) {
|
|
883
|
+
const line = lines[i];
|
|
884
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("#") || line.trim().startsWith("*")) continue;
|
|
885
|
+
HIGH_ENTROPY_RE.lastIndex = 0;
|
|
886
|
+
let match;
|
|
887
|
+
while ((match = HIGH_ENTROPY_RE.exec(line)) !== null) {
|
|
888
|
+
const value = match[1] || match[2];
|
|
889
|
+
if (!value || value.length < 40) continue;
|
|
890
|
+
if (isTemplateOrPlaceholder(value)) continue;
|
|
891
|
+
if (ENTROPY_SKIP.some((p) => p.test(value))) continue;
|
|
892
|
+
const entropy = shannonEntropy(value);
|
|
893
|
+
if (entropy >= threshold) {
|
|
894
|
+
findings.push({
|
|
895
|
+
type: "high-entropy",
|
|
896
|
+
file: filePath,
|
|
897
|
+
line: i + 1,
|
|
898
|
+
match: value,
|
|
899
|
+
redacted: redactSecret(value),
|
|
900
|
+
severity: entropy >= 5 ? "high" : "medium"
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return deduplicateFindings(findings);
|
|
906
|
+
}
|
|
907
|
+
async function auditProject(projectPath, filePaths, options = {}) {
|
|
908
|
+
const { customPatterns = [], entropyThreshold = 4.5, includePII = true } = options;
|
|
909
|
+
const allFindings = [];
|
|
910
|
+
const filesWithSecrets = /* @__PURE__ */ new Set();
|
|
911
|
+
for (const fp of filePaths) {
|
|
912
|
+
try {
|
|
913
|
+
const content = await readFile3(fp, "utf-8");
|
|
914
|
+
const relPath = relative3(resolve3(projectPath), resolve3(fp));
|
|
915
|
+
const isTestFile = /\.(test|spec|mock)\.[jt]sx?$/.test(relPath) || relPath.includes("__tests__");
|
|
916
|
+
const isDtsFile = relPath.endsWith(".d.ts");
|
|
917
|
+
let findings = scanContentForSecrets(content, relPath, customPatterns);
|
|
918
|
+
if (!includePII) {
|
|
919
|
+
findings = findings.filter((f) => f.type !== "pii");
|
|
920
|
+
}
|
|
921
|
+
const entropyFindings = isTestFile || isDtsFile ? [] : scanContentForHighEntropy(content, relPath, entropyThreshold);
|
|
922
|
+
const combined = [...findings, ...entropyFindings];
|
|
923
|
+
if (combined.length > 0) {
|
|
924
|
+
filesWithSecrets.add(relPath);
|
|
925
|
+
allFindings.push(...combined);
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
allFindings.sort((a, b) => {
|
|
931
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
932
|
+
return order[a.severity] - order[b.severity];
|
|
933
|
+
});
|
|
934
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
935
|
+
const byType = {};
|
|
936
|
+
for (const f of allFindings) {
|
|
937
|
+
bySeverity[f.severity]++;
|
|
938
|
+
byType[f.type] = (byType[f.type] || 0) + 1;
|
|
939
|
+
}
|
|
940
|
+
const recommendations = [];
|
|
941
|
+
if (bySeverity.critical > 0) {
|
|
942
|
+
recommendations.push("CRITICAL: Rotate all detected credentials immediately. They may already be compromised.");
|
|
943
|
+
}
|
|
944
|
+
if (byType["password"] > 0) {
|
|
945
|
+
recommendations.push("Move passwords to environment variables or a secrets manager (AWS Secrets Manager, Vault, etc.).");
|
|
946
|
+
}
|
|
947
|
+
if (byType["api-key"] > 0 || byType["aws-key"] > 0) {
|
|
948
|
+
recommendations.push("Use environment variables for API keys. Never commit them to source control.");
|
|
949
|
+
}
|
|
950
|
+
if (byType["connection-string"] > 0) {
|
|
951
|
+
recommendations.push("Database connection strings should use environment variables, not hardcoded values.");
|
|
952
|
+
}
|
|
953
|
+
if (byType["private-key"] > 0) {
|
|
954
|
+
recommendations.push("Private keys should NEVER be in source code. Use a key management service.");
|
|
955
|
+
}
|
|
956
|
+
if (byType["pii"] > 0) {
|
|
957
|
+
recommendations.push("PII detected. Review for GDPR/CCPA compliance. Consider data anonymization.");
|
|
958
|
+
}
|
|
959
|
+
if (byType["high-entropy"] > 0) {
|
|
960
|
+
recommendations.push("High-entropy strings detected that may be secrets. Review manually.");
|
|
961
|
+
}
|
|
962
|
+
if (allFindings.length > 0) {
|
|
963
|
+
recommendations.push("Add a .gitignore entry for .env files if not already present.");
|
|
964
|
+
recommendations.push("Run `npx cto-ai-cli --audit` regularly or add to CI pipeline.");
|
|
965
|
+
}
|
|
966
|
+
if (allFindings.length === 0) {
|
|
967
|
+
recommendations.push("No secrets detected. Great job keeping your codebase clean!");
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
findings: allFindings,
|
|
971
|
+
summary: {
|
|
972
|
+
totalFiles: filePaths.length,
|
|
973
|
+
filesScanned: filePaths.length,
|
|
974
|
+
filesWithSecrets: filesWithSecrets.size,
|
|
975
|
+
totalFindings: allFindings.length,
|
|
976
|
+
bySeverity,
|
|
977
|
+
byType
|
|
978
|
+
},
|
|
979
|
+
recommendations
|
|
980
|
+
};
|
|
981
|
+
}
|
|
830
982
|
|
|
831
983
|
// src/engine/pruner.ts
|
|
832
984
|
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
@@ -1887,6 +2039,7 @@ async function main() {
|
|
|
1887
2039
|
const fixMode = args.includes("--fix");
|
|
1888
2040
|
const reportMode = args.includes("--report");
|
|
1889
2041
|
const compareMode = args.includes("--compare");
|
|
2042
|
+
const auditMode = args.includes("--audit");
|
|
1890
2043
|
const helpMode = args.includes("--help") || args.includes("-h");
|
|
1891
2044
|
const contextIdx = args.indexOf("--context");
|
|
1892
2045
|
const contextTask = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
|
|
@@ -1904,6 +2057,7 @@ async function main() {
|
|
|
1904
2057
|
npx cto-ai-cli --context "your task" Generate task-specific context
|
|
1905
2058
|
npx cto-ai-cli --report Generate shareable markdown report
|
|
1906
2059
|
npx cto-ai-cli --compare Compare your score vs popular projects
|
|
2060
|
+
npx cto-ai-cli --audit Security audit: detect secrets & PII
|
|
1907
2061
|
npx cto-ai-cli --json Output as JSON (for CI/scripts)
|
|
1908
2062
|
|
|
1909
2063
|
What it does:
|
|
@@ -1964,6 +2118,9 @@ async function main() {
|
|
|
1964
2118
|
if (compareMode) {
|
|
1965
2119
|
runCompare(score);
|
|
1966
2120
|
}
|
|
2121
|
+
if (auditMode) {
|
|
2122
|
+
await runAudit(projectPath, analysis);
|
|
2123
|
+
}
|
|
1967
2124
|
console.log("");
|
|
1968
2125
|
console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
|
|
1969
2126
|
console.log("");
|
|
@@ -2365,6 +2522,188 @@ function renderCompareBar(pct) {
|
|
|
2365
2522
|
const empty = width - filled;
|
|
2366
2523
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
2367
2524
|
}
|
|
2525
|
+
async function runAudit(projectPath, analysis) {
|
|
2526
|
+
console.log("");
|
|
2527
|
+
console.log(" \u{1F50D} Running security audit...");
|
|
2528
|
+
console.log("");
|
|
2529
|
+
const filePaths = analysis.files.map((f) => f.path);
|
|
2530
|
+
const result = await auditProject(projectPath, filePaths, { includePII: true });
|
|
2531
|
+
const { summary, findings, recommendations } = result;
|
|
2532
|
+
const statusIcon = summary.bySeverity.critical > 0 ? "\u{1F534}" : summary.bySeverity.high > 0 ? "\u{1F7E0}" : summary.totalFindings > 0 ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
2533
|
+
const statusText = summary.bySeverity.critical > 0 ? "CRITICAL ISSUES FOUND" : summary.bySeverity.high > 0 ? "HIGH-SEVERITY ISSUES FOUND" : summary.totalFindings > 0 ? "MINOR ISSUES FOUND" : "ALL CLEAR";
|
|
2534
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
2535
|
+
console.log(" \u2551 \u2551");
|
|
2536
|
+
console.log(` \u2551 ${statusIcon} Security Audit: ${statusText.padEnd(28)} \u2551`);
|
|
2537
|
+
console.log(" \u2551 \u2551");
|
|
2538
|
+
console.log(` \u2551 Files scanned: ${summary.filesScanned.toString().padEnd(30)} \u2551`);
|
|
2539
|
+
console.log(` \u2551 Files affected: ${summary.filesWithSecrets.toString().padEnd(30)} \u2551`);
|
|
2540
|
+
console.log(` \u2551 Total findings: ${summary.totalFindings.toString().padEnd(30)} \u2551`);
|
|
2541
|
+
console.log(" \u2551 \u2551");
|
|
2542
|
+
console.log(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
2543
|
+
console.log(" \u2551 \u2551");
|
|
2544
|
+
if (summary.bySeverity.critical > 0) {
|
|
2545
|
+
console.log(` \u2551 \u{1F534} Critical: ${summary.bySeverity.critical.toString().padEnd(33)} \u2551`);
|
|
2546
|
+
}
|
|
2547
|
+
if (summary.bySeverity.high > 0) {
|
|
2548
|
+
console.log(` \u2551 \u{1F7E0} High: ${summary.bySeverity.high.toString().padEnd(33)} \u2551`);
|
|
2549
|
+
}
|
|
2550
|
+
if (summary.bySeverity.medium > 0) {
|
|
2551
|
+
console.log(` \u2551 \u{1F7E1} Medium: ${summary.bySeverity.medium.toString().padEnd(33)} \u2551`);
|
|
2552
|
+
}
|
|
2553
|
+
if (summary.bySeverity.low > 0) {
|
|
2554
|
+
console.log(` \u2551 \u{1F535} Low: ${summary.bySeverity.low.toString().padEnd(33)} \u2551`);
|
|
2555
|
+
}
|
|
2556
|
+
if (summary.totalFindings === 0) {
|
|
2557
|
+
console.log(" \u2551 \u2705 No secrets or PII detected \u2551");
|
|
2558
|
+
}
|
|
2559
|
+
console.log(" \u2551 \u2551");
|
|
2560
|
+
if (Object.keys(summary.byType).length > 0) {
|
|
2561
|
+
console.log(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
2562
|
+
console.log(" \u2551 \u2551");
|
|
2563
|
+
console.log(" \u2551 By type: \u2551");
|
|
2564
|
+
for (const [type, count] of Object.entries(summary.byType)) {
|
|
2565
|
+
const label = type.padEnd(18);
|
|
2566
|
+
console.log(` \u2551 ${label} ${count.toString().padEnd(28)} \u2551`);
|
|
2567
|
+
}
|
|
2568
|
+
console.log(" \u2551 \u2551");
|
|
2569
|
+
}
|
|
2570
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
2571
|
+
if (findings.length > 0) {
|
|
2572
|
+
console.log("");
|
|
2573
|
+
console.log(" Findings:");
|
|
2574
|
+
console.log("");
|
|
2575
|
+
const shown = findings.slice(0, 15);
|
|
2576
|
+
for (const f of shown) {
|
|
2577
|
+
const icon = f.severity === "critical" ? "\u{1F534}" : f.severity === "high" ? "\u{1F7E0}" : f.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2578
|
+
const sev = f.severity.toUpperCase().padEnd(8);
|
|
2579
|
+
console.log(` ${icon} ${sev} ${f.file}:${f.line}`);
|
|
2580
|
+
console.log(` ${f.type}: ${f.redacted}`);
|
|
2581
|
+
}
|
|
2582
|
+
if (findings.length > 15) {
|
|
2583
|
+
console.log(` ... and ${findings.length - 15} more (see .cto/audit/ for full report)`);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (recommendations.length > 0) {
|
|
2587
|
+
console.log("");
|
|
2588
|
+
console.log(" Recommendations:");
|
|
2589
|
+
console.log("");
|
|
2590
|
+
for (const rec of recommendations) {
|
|
2591
|
+
const icon = rec.startsWith("CRITICAL") ? "\u{1F6A8}" : "\u{1F4A1}";
|
|
2592
|
+
console.log(` ${icon} ${rec}`);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
const ctoDir = join4(projectPath, ".cto");
|
|
2596
|
+
const auditDir = join4(ctoDir, "audit");
|
|
2597
|
+
mkdirSync(auditDir, { recursive: true });
|
|
2598
|
+
const now = /* @__PURE__ */ new Date();
|
|
2599
|
+
const dateStr = now.toISOString().split("T")[0];
|
|
2600
|
+
const logFile = join4(auditDir, `${dateStr}.jsonl`);
|
|
2601
|
+
const logEntry = {
|
|
2602
|
+
timestamp: now.toISOString(),
|
|
2603
|
+
version: "3.2.0",
|
|
2604
|
+
summary: {
|
|
2605
|
+
filesScanned: summary.filesScanned,
|
|
2606
|
+
filesWithSecrets: summary.filesWithSecrets,
|
|
2607
|
+
totalFindings: summary.totalFindings,
|
|
2608
|
+
bySeverity: summary.bySeverity,
|
|
2609
|
+
byType: summary.byType
|
|
2610
|
+
},
|
|
2611
|
+
findings: findings.map((f) => ({
|
|
2612
|
+
type: f.type,
|
|
2613
|
+
file: f.file,
|
|
2614
|
+
line: f.line,
|
|
2615
|
+
severity: f.severity,
|
|
2616
|
+
redacted: f.redacted
|
|
2617
|
+
}))
|
|
2618
|
+
};
|
|
2619
|
+
appendFileSync(logFile, JSON.stringify(logEntry) + "\n");
|
|
2620
|
+
let report = `# Security Audit Report
|
|
2621
|
+
|
|
2622
|
+
`;
|
|
2623
|
+
report += `> Generated by cto-ai-cli on ${now.toISOString()}
|
|
2624
|
+
|
|
2625
|
+
`;
|
|
2626
|
+
report += `## Summary
|
|
2627
|
+
|
|
2628
|
+
`;
|
|
2629
|
+
report += `| Metric | Value |
|
|
2630
|
+
`;
|
|
2631
|
+
report += `|--------|-------|
|
|
2632
|
+
`;
|
|
2633
|
+
report += `| Files scanned | ${summary.filesScanned} |
|
|
2634
|
+
`;
|
|
2635
|
+
report += `| Files with issues | ${summary.filesWithSecrets} |
|
|
2636
|
+
`;
|
|
2637
|
+
report += `| Total findings | ${summary.totalFindings} |
|
|
2638
|
+
`;
|
|
2639
|
+
report += `| Critical | ${summary.bySeverity.critical} |
|
|
2640
|
+
`;
|
|
2641
|
+
report += `| High | ${summary.bySeverity.high} |
|
|
2642
|
+
`;
|
|
2643
|
+
report += `| Medium | ${summary.bySeverity.medium} |
|
|
2644
|
+
|
|
2645
|
+
`;
|
|
2646
|
+
if (findings.length > 0) {
|
|
2647
|
+
report += `## Findings
|
|
2648
|
+
|
|
2649
|
+
`;
|
|
2650
|
+
report += `| Severity | Type | File | Line | Redacted |
|
|
2651
|
+
`;
|
|
2652
|
+
report += `|----------|------|------|------|----------|
|
|
2653
|
+
`;
|
|
2654
|
+
for (const f of findings) {
|
|
2655
|
+
report += `| ${f.severity} | ${f.type} | ${f.file} | ${f.line} | \`${f.redacted.slice(0, 30)}\` |
|
|
2656
|
+
`;
|
|
2657
|
+
}
|
|
2658
|
+
report += "\n";
|
|
2659
|
+
}
|
|
2660
|
+
if (recommendations.length > 0) {
|
|
2661
|
+
report += `## Recommendations
|
|
2662
|
+
|
|
2663
|
+
`;
|
|
2664
|
+
for (const rec of recommendations) {
|
|
2665
|
+
report += `- ${rec}
|
|
2666
|
+
`;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
writeFileSync(join4(auditDir, "report.md"), report);
|
|
2670
|
+
const envSecrets = findings.filter(
|
|
2671
|
+
(f) => f.type === "env-variable" || f.type === "password" || f.type === "api-key" || f.type === "aws-key" || f.type === "connection-string"
|
|
2672
|
+
);
|
|
2673
|
+
if (envSecrets.length > 0) {
|
|
2674
|
+
const envVarNames = /* @__PURE__ */ new Set();
|
|
2675
|
+
for (const f of envSecrets) {
|
|
2676
|
+
const varMatch = f.match.match(/^([A-Z_][A-Z0-9_]*)\s*[:=]/i);
|
|
2677
|
+
if (varMatch) {
|
|
2678
|
+
envVarNames.add(varMatch[1].toUpperCase());
|
|
2679
|
+
} else {
|
|
2680
|
+
const name = f.type.toUpperCase().replace(/-/g, "_");
|
|
2681
|
+
envVarNames.add(name);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (envVarNames.size > 0) {
|
|
2685
|
+
let envExample = "# Environment variables \u2014 NEVER commit real values\n";
|
|
2686
|
+
envExample += "# Generated by cto-ai-cli --audit\n\n";
|
|
2687
|
+
for (const name of envVarNames) {
|
|
2688
|
+
envExample += `${name}=your_${name.toLowerCase()}_here
|
|
2689
|
+
`;
|
|
2690
|
+
}
|
|
2691
|
+
writeFileSync(join4(ctoDir, ".env.example"), envExample);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
console.log("");
|
|
2695
|
+
console.log(" \u{1F4C1} Audit artifacts:");
|
|
2696
|
+
console.log(` \u{1F4CB} .cto/audit/${dateStr}.jsonl Audit log (append-only)`);
|
|
2697
|
+
console.log(" \u{1F4CA} .cto/audit/report.md Full report");
|
|
2698
|
+
if (envSecrets.length > 0) {
|
|
2699
|
+
console.log(" \u{1F4DD} .cto/.env.example Template for environment variables");
|
|
2700
|
+
}
|
|
2701
|
+
console.log("");
|
|
2702
|
+
if (process.env.CI && (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0)) {
|
|
2703
|
+
console.log(" \u274C CI mode: Failing due to critical/high severity findings.");
|
|
2704
|
+
process.exit(1);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2368
2707
|
function formatTokens(n) {
|
|
2369
2708
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2370
2709
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
package/dist/cli/v2/index.js
CHANGED
|
@@ -1336,7 +1336,29 @@ var BUILTIN_PATTERNS = [
|
|
|
1336
1336
|
{ type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
|
|
1337
1337
|
{ type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
|
|
1338
1338
|
// Environment variables with secrets
|
|
1339
|
-
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
|
|
1339
|
+
{ type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
|
|
1340
|
+
// Stripe
|
|
1341
|
+
{ type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
|
|
1342
|
+
{ type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
|
|
1343
|
+
{ type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
|
|
1344
|
+
// Slack
|
|
1345
|
+
{ type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
|
|
1346
|
+
{ type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
|
|
1347
|
+
{ type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
|
|
1348
|
+
// Google
|
|
1349
|
+
{ type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
|
|
1350
|
+
{ type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
|
|
1351
|
+
// Azure
|
|
1352
|
+
{ type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
|
|
1353
|
+
// Twilio
|
|
1354
|
+
{ type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
|
|
1355
|
+
// SendGrid
|
|
1356
|
+
{ type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
|
|
1357
|
+
// JWT
|
|
1358
|
+
{ type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
|
|
1359
|
+
// PII
|
|
1360
|
+
{ type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
|
|
1361
|
+
{ type: "pii", source: "\\b\\d{3}[-.]?\\d{2}[-.]?\\d{4}\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
|
|
1340
1362
|
];
|
|
1341
1363
|
function buildPatterns(customPatterns = []) {
|
|
1342
1364
|
const patterns = BUILTIN_PATTERNS.map((def) => ({
|