great-cto 2.31.0 → 2.33.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/bootstrap.js +0 -50
- package/dist/ci.js +10 -141
- package/dist/companion.js +5 -1
- package/dist/detect.js +14 -1
- package/dist/main.js +13 -175
- package/dist/mcp.js +4 -67
- package/dist/report.js +7 -3
- package/dist/upgrade.js +13 -0
- package/package.json +1 -2
- package/agentshield-rules/cost-runaway.yaml +0 -149
- package/agentshield-rules/prompt-injection.yaml +0 -117
- package/agentshield-rules/rag-poisoning.yaml +0 -113
- package/agentshield-rules/secrets-in-prompts.yaml +0 -90
- package/agentshield-rules/ssrf-in-tools.yaml +0 -99
- package/dist/agentshield/index.js +0 -15
- package/dist/agentshield/rules-loader.js +0 -213
- package/dist/agentshield/sarif.js +0 -80
- package/dist/agentshield/scanner.js +0 -244
- package/dist/agentshield/types.js +0 -10
package/dist/bootstrap.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Safe: will NOT overwrite an existing PROJECT.md.
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
5
|
import { dim, success, warn } from "./ui.js";
|
|
7
6
|
import { suggestJurisdictions } from "./jurisdictions.js";
|
|
8
7
|
import { compileFlow, renderFlowMd } from "./flow.js";
|
|
@@ -146,55 +145,6 @@ when you actually start work:
|
|
|
146
145
|
`;
|
|
147
146
|
writeFileSync(projectMd, content, "utf-8");
|
|
148
147
|
success(`created .great_cto/PROJECT.md ${dim(`(archetype: ${archetype})`)}`);
|
|
149
|
-
// Write ~/.great_cto/guardrails.yml example if it doesn't exist yet.
|
|
150
|
-
// This is the user-level agentshield config — applies across all projects.
|
|
151
|
-
const globalGreatCtoDir = join(homedir(), ".great_cto");
|
|
152
|
-
const guardrailsPath = join(globalGreatCtoDir, "guardrails.yml");
|
|
153
|
-
if (!existsSync(guardrailsPath)) {
|
|
154
|
-
mkdirSync(globalGreatCtoDir, { recursive: true });
|
|
155
|
-
const guardrailsContent = `# ~/.great_cto/guardrails.yml
|
|
156
|
-
# User-defined agentshield rules. Loaded and merged with built-in rules on every scan.
|
|
157
|
-
# Rules use the same YAML format as agentshield-rules/*.yaml.
|
|
158
|
-
#
|
|
159
|
-
# action field (user rules only):
|
|
160
|
-
# block — scan fails (treated as critical finding; blocks gate:ship)
|
|
161
|
-
# audit — finding reported but scan continues (warning only)
|
|
162
|
-
# redact — same as audit; marks pattern for future content redaction
|
|
163
|
-
#
|
|
164
|
-
# Example rule — customize with your org's patterns:
|
|
165
|
-
#
|
|
166
|
-
# - id: UG-001
|
|
167
|
-
# scanner: secrets-in-prompts
|
|
168
|
-
# title: Internal API token pattern
|
|
169
|
-
# severity: critical
|
|
170
|
-
# description: |
|
|
171
|
-
# Detects hardcoded internal API tokens matching the org pattern mytoken_*.
|
|
172
|
-
# remediation: |
|
|
173
|
-
# Move to environment variable. Reference via process.env.MY_TOKEN.
|
|
174
|
-
# patterns:
|
|
175
|
-
# - 'mytoken_[a-z0-9]{32}'
|
|
176
|
-
# action: block
|
|
177
|
-
#
|
|
178
|
-
# - id: UG-002
|
|
179
|
-
# scanner: prompt-injection
|
|
180
|
-
# title: Audit use of customer data in prompts
|
|
181
|
-
# severity: high
|
|
182
|
-
# description: |
|
|
183
|
-
# Flags when customer PII fields (email, phone, address) are embedded in prompts.
|
|
184
|
-
# remediation: |
|
|
185
|
-
# Replace PII with anonymized tokens before embedding in prompts.
|
|
186
|
-
# patterns:
|
|
187
|
-
# - 'customer\.(email|phone|address|ssn)'
|
|
188
|
-
# action: audit
|
|
189
|
-
# file_globs:
|
|
190
|
-
# - "**/*.ts"
|
|
191
|
-
# - "**/*.py"
|
|
192
|
-
#
|
|
193
|
-
# Add your own rules below:
|
|
194
|
-
`;
|
|
195
|
-
writeFileSync(guardrailsPath, guardrailsContent, "utf-8");
|
|
196
|
-
success(`created ~/.great_cto/guardrails.yml ${dim("(user-level agentshield rules — edit to add org-specific patterns)")}`);
|
|
197
|
-
}
|
|
198
148
|
// Write FLOW.md — compiled delivery flow for agents and user
|
|
199
149
|
const confidence = detectionMeta?.confidence ?? "medium";
|
|
200
150
|
const size = (detection.projectSize ?? "medium");
|
package/dist/ci.js
CHANGED
|
@@ -1,95 +1,22 @@
|
|
|
1
1
|
// great-cto ci — single-command CI gate.
|
|
2
2
|
//
|
|
3
|
-
// Runs
|
|
4
|
-
//
|
|
3
|
+
// Runs archetype-validate + budget-check. Designed to be the only great_cto
|
|
4
|
+
// invocation in a CI step.
|
|
5
5
|
//
|
|
6
6
|
// Outputs:
|
|
7
7
|
// - human-readable to stderr (always)
|
|
8
|
-
// - GitHub Actions annotations (auto when $GITHUB_ACTIONS is set, or --annotations)
|
|
9
|
-
// - SARIF 2.1.0 JSON (--sarif <path>) — uploadable to GitHub Security tab
|
|
10
|
-
// - JUnit XML (--junit <path>) — for test reporters / pipeline UIs
|
|
11
8
|
//
|
|
12
9
|
// Exit codes:
|
|
13
10
|
// 0 = clean, all gates pass
|
|
14
|
-
// 1 =
|
|
15
|
-
// 2 =
|
|
11
|
+
// 1 = archetype drift (CI should fail)
|
|
12
|
+
// 2 = setup error (not a finding — infrastructure problem)
|
|
16
13
|
//
|
|
17
14
|
// Example workflow:
|
|
18
15
|
// - run: npx great-cto@latest ci ./
|
|
19
16
|
// env:
|
|
20
17
|
// GREAT_CTO_NO_TELEMETRY: "1"
|
|
21
|
-
import { existsSync, readFileSync
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
19
|
import { resolve } from "node:path";
|
|
23
|
-
const SEVERITY_ORDER = {
|
|
24
|
-
info: 0,
|
|
25
|
-
low: 1,
|
|
26
|
-
medium: 2,
|
|
27
|
-
high: 3,
|
|
28
|
-
critical: 4,
|
|
29
|
-
};
|
|
30
|
-
function severityAtLeast(sev, threshold) {
|
|
31
|
-
return (SEVERITY_ORDER[sev] ?? 0) >= (SEVERITY_ORDER[threshold] ?? 0);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Emit GitHub Actions annotation lines. These appear inline on PR diffs.
|
|
35
|
-
* Format: ::error file=path,line=N,col=M,title=T::message
|
|
36
|
-
* ::warning file=...
|
|
37
|
-
* ::notice file=...
|
|
38
|
-
*/
|
|
39
|
-
function emitGitHubAnnotation(finding) {
|
|
40
|
-
const sev = finding.rule.severity;
|
|
41
|
-
const level = sev === "critical" || sev === "high" ? "error"
|
|
42
|
-
: sev === "medium" ? "warning" : "notice";
|
|
43
|
-
const file = finding.location.file;
|
|
44
|
-
const line = finding.location.line;
|
|
45
|
-
const title = `${finding.rule.id}: ${finding.rule.title}`;
|
|
46
|
-
const message = finding.rule.owasp
|
|
47
|
-
? `${finding.rule.title} (${finding.rule.owasp})`
|
|
48
|
-
: finding.rule.title;
|
|
49
|
-
// Escape GHA-special characters
|
|
50
|
-
const escape = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
51
|
-
console.log(`::${level} file=${escape(file)},line=${line},title=${escape(title)}::${escape(message)}`);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Emit JUnit XML report. One <testcase> per scanned file, fails recorded as
|
|
55
|
-
* <failure> elements. Format compatible with most CI test reporters.
|
|
56
|
-
*/
|
|
57
|
-
function buildJunitXml(report) {
|
|
58
|
-
const findingsByFile = new Map();
|
|
59
|
-
for (const f of report.findings) {
|
|
60
|
-
const arr = findingsByFile.get(f.location.file) || [];
|
|
61
|
-
arr.push(f);
|
|
62
|
-
findingsByFile.set(f.location.file, arr);
|
|
63
|
-
}
|
|
64
|
-
const totalTests = Math.max(report.filesScanned, findingsByFile.size);
|
|
65
|
-
const failures = report.findings.length;
|
|
66
|
-
const escape = (s) => String(s)
|
|
67
|
-
.replace(/&/g, "&")
|
|
68
|
-
.replace(/</g, "<")
|
|
69
|
-
.replace(/>/g, ">")
|
|
70
|
-
.replace(/"/g, """)
|
|
71
|
-
.replace(/'/g, "'");
|
|
72
|
-
const cases = Array.from(findingsByFile.entries())
|
|
73
|
-
.map(([file, findings]) => {
|
|
74
|
-
const failureBody = findings
|
|
75
|
-
.map(f => ` <failure type="${escape(f.rule.severity)}" message="${escape(f.rule.id + ': ' + f.rule.title)}">
|
|
76
|
-
${escape(f.location.snippet || '')}
|
|
77
|
-
${escape(f.rule.owasp || '')}
|
|
78
|
-
</failure>`)
|
|
79
|
-
.join("\n");
|
|
80
|
-
return ` <testcase classname="agentshield" name="${escape(file)}" time="0">
|
|
81
|
-
${failureBody}
|
|
82
|
-
</testcase>`;
|
|
83
|
-
})
|
|
84
|
-
.join("\n");
|
|
85
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
86
|
-
<testsuites>
|
|
87
|
-
<testsuite name="great-cto ci" tests="${totalTests}" failures="${failures}" errors="0" time="${(report.durationMs / 1000).toFixed(3)}">
|
|
88
|
-
${cases}
|
|
89
|
-
</testsuite>
|
|
90
|
-
</testsuites>
|
|
91
|
-
`;
|
|
92
|
-
}
|
|
93
20
|
/**
|
|
94
21
|
* Quick archetype-detection sanity check. Fails CI if the archetype changed
|
|
95
22
|
* from what's pinned in .great_cto/PROJECT.md (signals undeclared
|
|
@@ -147,66 +74,26 @@ function budgetCheck(cwd, quiet) {
|
|
|
147
74
|
}
|
|
148
75
|
export async function runCi(args) {
|
|
149
76
|
const startTs = Date.now();
|
|
150
|
-
const inGitHubActions = process.env.GITHUB_ACTIONS === "true";
|
|
151
|
-
const wantAnnotations = args.annotations || inGitHubActions;
|
|
152
77
|
if (!args.quiet) {
|
|
153
|
-
console.error(`\ngreat-cto ci —
|
|
78
|
+
console.error(`\ngreat-cto ci — archetype + budget gate`);
|
|
154
79
|
console.error(` path: ${resolve(args.path)}`);
|
|
155
|
-
if (inGitHubActions)
|
|
156
|
-
console.error(` env: GitHub Actions detected — emitting annotations`);
|
|
157
80
|
console.error("");
|
|
158
81
|
}
|
|
159
|
-
// 1.
|
|
160
|
-
let scan;
|
|
161
|
-
let toSarif;
|
|
162
|
-
try {
|
|
163
|
-
({ scan } = await import("./agentshield/scanner.js"));
|
|
164
|
-
({ toSarif } = await import("./agentshield/sarif.js"));
|
|
165
|
-
}
|
|
166
|
-
catch (e) {
|
|
167
|
-
console.error(`ci: failed to load scanner: ${e.message}`);
|
|
168
|
-
return 2;
|
|
169
|
-
}
|
|
170
|
-
const report = scan(resolve(args.path), {
|
|
171
|
-
minSeverity: args.severity,
|
|
172
|
-
});
|
|
173
|
-
// 2. Archetype check
|
|
82
|
+
// 1. Archetype check
|
|
174
83
|
let archResult = { ok: true, msg: "skipped" };
|
|
175
84
|
if (!args.noArchetype) {
|
|
176
85
|
archResult = await archetypeCheck(args.path, args.quiet);
|
|
177
86
|
}
|
|
178
|
-
//
|
|
87
|
+
// 2. Budget check (warn-only)
|
|
179
88
|
let budgetResult = { ok: true, msg: "skipped" };
|
|
180
89
|
if (!args.noBudget) {
|
|
181
90
|
budgetResult = budgetCheck(args.path, args.quiet);
|
|
182
91
|
}
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
for (const f of report.findings) {
|
|
186
|
-
if (severityAtLeast(f.rule.severity, args.failOn)) {
|
|
187
|
-
emitGitHubAnnotation(f);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
// 5. Emit SARIF
|
|
192
|
-
if (args.sarifPath) {
|
|
193
|
-
writeFileSync(args.sarifPath, JSON.stringify(toSarif(report), null, 2));
|
|
194
|
-
if (!args.quiet)
|
|
195
|
-
console.error(` ✓ SARIF → ${args.sarifPath}`);
|
|
196
|
-
}
|
|
197
|
-
// 6. Emit JUnit XML
|
|
198
|
-
if (args.junitPath) {
|
|
199
|
-
writeFileSync(args.junitPath, buildJunitXml(report));
|
|
200
|
-
if (!args.quiet)
|
|
201
|
-
console.error(` ✓ JUnit XML → ${args.junitPath}`);
|
|
202
|
-
}
|
|
203
|
-
// 7. Summary
|
|
204
|
-
const blockingFindings = report.findings.filter((f) => severityAtLeast(f.rule.severity, args.failOn));
|
|
205
|
-
const passed = blockingFindings.length === 0 && archResult.ok;
|
|
92
|
+
// 3. Summary
|
|
93
|
+
const passed = archResult.ok;
|
|
206
94
|
if (!args.quiet) {
|
|
207
95
|
const dur = ((Date.now() - startTs) / 1000).toFixed(1);
|
|
208
96
|
console.error("");
|
|
209
|
-
console.error(` scan: ${report.findings.length} finding(s) (${blockingFindings.length} at/above ${args.failOn})`);
|
|
210
97
|
console.error(` archetype: ${archResult.ok ? "✓" : "✗"} ${archResult.msg}`);
|
|
211
98
|
console.error(` budget: ${budgetResult.msg}`);
|
|
212
99
|
console.error(` duration: ${dur}s`);
|
|
@@ -216,15 +103,6 @@ export async function runCi(args) {
|
|
|
216
103
|
}
|
|
217
104
|
else {
|
|
218
105
|
console.error("\x1b[31m✗ great-cto ci: failed\x1b[0m");
|
|
219
|
-
if (blockingFindings.length) {
|
|
220
|
-
console.error(` ${blockingFindings.length} finding(s) at/above ${args.failOn}:`);
|
|
221
|
-
for (const f of blockingFindings.slice(0, 10)) {
|
|
222
|
-
console.error(` [${f.rule.severity}] ${f.rule.id} — ${f.location.file}:${f.location.line}`);
|
|
223
|
-
}
|
|
224
|
-
if (blockingFindings.length > 10) {
|
|
225
|
-
console.error(` ... +${blockingFindings.length - 10} more`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
106
|
if (!archResult.ok)
|
|
229
107
|
console.error(` ${archResult.msg}`);
|
|
230
108
|
}
|
|
@@ -236,10 +114,6 @@ export async function runCi(args) {
|
|
|
236
114
|
*/
|
|
237
115
|
export function parseCiArgs(rawArgv) {
|
|
238
116
|
const flag = (n) => rawArgv.includes(`--${n}`);
|
|
239
|
-
const value = (n, def) => {
|
|
240
|
-
const i = rawArgv.indexOf(`--${n}`);
|
|
241
|
-
return i >= 0 && i < rawArgv.length - 1 ? rawArgv[i + 1] : def;
|
|
242
|
-
};
|
|
243
117
|
const ciIdx = rawArgv.indexOf("ci");
|
|
244
118
|
let path = ".";
|
|
245
119
|
for (let i = ciIdx + 1; i < rawArgv.length; i++) {
|
|
@@ -250,11 +124,6 @@ export function parseCiArgs(rawArgv) {
|
|
|
250
124
|
}
|
|
251
125
|
return {
|
|
252
126
|
path,
|
|
253
|
-
severity: value("severity", "high"),
|
|
254
|
-
failOn: value("fail-on", "critical"),
|
|
255
|
-
sarifPath: value("sarif") ?? null,
|
|
256
|
-
junitPath: value("junit") ?? null,
|
|
257
|
-
annotations: flag("annotations"),
|
|
258
127
|
noBudget: flag("no-budget"),
|
|
259
128
|
noArchetype: flag("no-archetype"),
|
|
260
129
|
quiet: flag("quiet"),
|
package/dist/companion.js
CHANGED
|
@@ -42,7 +42,11 @@ function isAlreadyInstalled(name) {
|
|
|
42
42
|
return null;
|
|
43
43
|
try {
|
|
44
44
|
const versions = readdirSync(base).filter((v) => /\S/.test(v));
|
|
45
|
-
|
|
45
|
+
if (versions.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
// Return the highest version, not filesystem order — matches
|
|
48
|
+
// upgrade.ts/installer.ts which both sort before picking.
|
|
49
|
+
return versions.sort(semverDescending)[0];
|
|
46
50
|
}
|
|
47
51
|
catch {
|
|
48
52
|
return null;
|
package/dist/detect.js
CHANGED
|
@@ -1118,7 +1118,7 @@ function mineReadmeKeywords(dir) {
|
|
|
1118
1118
|
if (terms.some((t) => text.includes(t)))
|
|
1119
1119
|
kws.add(bucket);
|
|
1120
1120
|
}
|
|
1121
|
-
// Wave 1-
|
|
1121
|
+
// Wave 1-4 pack-trigger raw terms — emitted verbatim so packs.ts
|
|
1122
1122
|
// can substring-match them. Keep in sync with packs.ts SIGNALS.keywords.
|
|
1123
1123
|
// Single tokens + multi-word phrases supported.
|
|
1124
1124
|
const packTerms = [
|
|
@@ -1163,6 +1163,19 @@ function mineReadmeKeywords(dir) {
|
|
|
1163
1163
|
"glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
|
|
1164
1164
|
"lab automation", "robotic biology", "liquid handler", "hamilton", "tecan",
|
|
1165
1165
|
"beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila",
|
|
1166
|
+
// digital-health-pack (Wave 4) — keep in sync with packs.ts SIGNALS.keywords
|
|
1167
|
+
"wearable", "apple watch", "apple health", "healthkit", "health connect",
|
|
1168
|
+
"garmin", "samsung health", "google fit", "fitbit", "heart rate", "hrv",
|
|
1169
|
+
"heart rate variability", "spo2", "sleep tracking", "sleep stages",
|
|
1170
|
+
"biometric sensor", "stress score", "activity tracking", "ecg wearable",
|
|
1171
|
+
"mental health", "mental wellness", "wellbeing", "mindfulness ai",
|
|
1172
|
+
"stress detection", "burnout detection", "mood tracking", "anxiety ai",
|
|
1173
|
+
"depression ai", "phq-9", "gad-7", "digital therapeutics", "dtx",
|
|
1174
|
+
"cbt app", "dbt app", "therapy ai", "personalised training",
|
|
1175
|
+
"personalized training", "fitness ai", "nutrition ai",
|
|
1176
|
+
"supplement recommendation", "supplement ai", "diet ai", "meal plan ai",
|
|
1177
|
+
"macro ai", "physician review", "physician hitl", "doctor in the loop",
|
|
1178
|
+
"clinical review workflow", "remote patient monitoring", "rpm", "teleconsultation",
|
|
1166
1179
|
];
|
|
1167
1180
|
for (const term of packTerms) {
|
|
1168
1181
|
if (text.includes(term))
|
package/dist/main.js
CHANGED
|
@@ -82,10 +82,6 @@ function parseArgs(argv) {
|
|
|
82
82
|
args.command = "board";
|
|
83
83
|
else if (a === "register")
|
|
84
84
|
args.command = "register";
|
|
85
|
-
else if (a === "scan")
|
|
86
|
-
args.command = "scan";
|
|
87
|
-
else if (a === "list-rules")
|
|
88
|
-
args.command = "list-rules";
|
|
89
85
|
else if (a === "ci")
|
|
90
86
|
args.command = "ci";
|
|
91
87
|
else if (a === "mcp")
|
|
@@ -140,136 +136,6 @@ function parseArgs(argv) {
|
|
|
140
136
|
args.positional = rest;
|
|
141
137
|
return args;
|
|
142
138
|
}
|
|
143
|
-
/**
|
|
144
|
-
* `great-cto scan [path]` — AI-specific security scanner (formerly @great-cto/agentshield).
|
|
145
|
-
*
|
|
146
|
-
* Detects OWASP LLM Top 10 patterns: prompt injection vectors, secrets in
|
|
147
|
-
* prompts, SSRF in tool definitions, RAG poisoning, cost-runaway loops.
|
|
148
|
-
*
|
|
149
|
-
* Flags (parsed from raw argv since they're scan-specific):
|
|
150
|
-
* --severity <lvl> info|low|medium|high|critical (default: info)
|
|
151
|
-
* --scanner <name> prompt-injection | secrets-in-prompts | ssrf-in-tools |
|
|
152
|
-
* rag-poisoning | cost-runaway (repeatable)
|
|
153
|
-
* --sarif <file> emit SARIF 2.1.0 to file
|
|
154
|
-
* --json emit JSON to stdout
|
|
155
|
-
* --quiet suppress human-readable output
|
|
156
|
-
* --max <n> stop after N findings
|
|
157
|
-
* --exclude <regex> add path exclude (repeatable)
|
|
158
|
-
*
|
|
159
|
-
* Exit codes:
|
|
160
|
-
* 0 = no findings (or all below severity threshold)
|
|
161
|
-
* 1 = findings at/above threshold (CI-friendly)
|
|
162
|
-
* 2 = scan failed
|
|
163
|
-
*/
|
|
164
|
-
async function runScan(args, rawArgv) {
|
|
165
|
-
const { writeFileSync } = await import("node:fs");
|
|
166
|
-
const { resolve: resolvePath } = await import("node:path");
|
|
167
|
-
// Lazy import compiled scanner — keeps cold start fast for `init` flow.
|
|
168
|
-
let scan;
|
|
169
|
-
let toSarif;
|
|
170
|
-
try {
|
|
171
|
-
({ scan } = await import("./agentshield/scanner.js"));
|
|
172
|
-
({ toSarif } = await import("./agentshield/sarif.js"));
|
|
173
|
-
}
|
|
174
|
-
catch (e) {
|
|
175
|
-
error(`scan: failed to load scanner: ${e.message}`);
|
|
176
|
-
return 2;
|
|
177
|
-
}
|
|
178
|
-
// Parse scan-specific flags from raw argv
|
|
179
|
-
const flag = (n) => rawArgv.includes(`--${n}`);
|
|
180
|
-
const value = (n, def) => {
|
|
181
|
-
const i = rawArgv.indexOf(`--${n}`);
|
|
182
|
-
return i >= 0 && i < rawArgv.length - 1 ? rawArgv[i + 1] : def;
|
|
183
|
-
};
|
|
184
|
-
const scanners = rawArgv
|
|
185
|
-
.map((a, i) => (a === "--scanner" ? rawArgv[i + 1] : null))
|
|
186
|
-
.filter(Boolean);
|
|
187
|
-
const exclude = rawArgv
|
|
188
|
-
.map((a, i) => (a === "--exclude" ? rawArgv[i + 1] : null))
|
|
189
|
-
.filter(Boolean);
|
|
190
|
-
// Path: first non-flag arg after `scan`, default cwd
|
|
191
|
-
const scanIdx = rawArgv.indexOf("scan");
|
|
192
|
-
let root = ".";
|
|
193
|
-
for (let i = scanIdx + 1; i < rawArgv.length; i++) {
|
|
194
|
-
if (rawArgv[i] && !rawArgv[i].startsWith("--")) {
|
|
195
|
-
root = rawArgv[i];
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const opts = {
|
|
200
|
-
scanners: scanners.length > 0 ? scanners : undefined,
|
|
201
|
-
minSeverity: value("severity", "info"),
|
|
202
|
-
exclude: exclude.length > 0 ? exclude : undefined,
|
|
203
|
-
maxFindings: value("max") ? parseInt(value("max"), 10) : undefined,
|
|
204
|
-
};
|
|
205
|
-
const sarifPath = value("sarif");
|
|
206
|
-
const wantsJson = flag("json");
|
|
207
|
-
const quiet = flag("quiet");
|
|
208
|
-
const report = scan(resolvePath(root), opts);
|
|
209
|
-
if (sarifPath) {
|
|
210
|
-
writeFileSync(sarifPath, JSON.stringify(toSarif(report), null, 2));
|
|
211
|
-
if (!quiet)
|
|
212
|
-
console.error(`✓ SARIF written → ${sarifPath}`);
|
|
213
|
-
}
|
|
214
|
-
if (wantsJson) {
|
|
215
|
-
console.log(JSON.stringify(report, null, 2));
|
|
216
|
-
}
|
|
217
|
-
else if (!quiet) {
|
|
218
|
-
const COLORS = {
|
|
219
|
-
critical: "\x1b[1;31m", high: "\x1b[31m", medium: "\x1b[33m",
|
|
220
|
-
low: "\x1b[36m", info: "\x1b[2m", reset: "\x1b[0m",
|
|
221
|
-
};
|
|
222
|
-
const useColor = process.stdout.isTTY;
|
|
223
|
-
const c = (sev, s) => (useColor ? `${COLORS[sev] || ""}${s}${COLORS.reset}` : s);
|
|
224
|
-
console.error(`\ngreat-cto scan ${getCliVersion()} — scanned ${report.filesScanned} file(s) in ${report.durationMs}ms\n`);
|
|
225
|
-
if (report.errors.length > 0) {
|
|
226
|
-
console.error(`\x1b[33m⚠ ${report.errors.length} error(s):\x1b[0m`);
|
|
227
|
-
for (const e of report.errors)
|
|
228
|
-
console.error(` ${e}`);
|
|
229
|
-
console.error("");
|
|
230
|
-
}
|
|
231
|
-
if (report.findings.length === 0) {
|
|
232
|
-
console.error("\x1b[32m✓ No findings.\x1b[0m\n");
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
for (const f of report.findings) {
|
|
236
|
-
const tag = c(f.rule.severity, `[${f.rule.severity.toUpperCase()}]`);
|
|
237
|
-
console.error(`${tag} ${f.rule.id} ${f.location.file}:${f.location.line}`);
|
|
238
|
-
console.error(` ${f.rule.title}`);
|
|
239
|
-
console.error(` ${c("info", f.location.snippet)}`);
|
|
240
|
-
if (f.rule.owasp)
|
|
241
|
-
console.error(` ${c("info", f.rule.owasp)}`);
|
|
242
|
-
console.error("");
|
|
243
|
-
}
|
|
244
|
-
const counts = {};
|
|
245
|
-
for (const f of report.findings)
|
|
246
|
-
counts[f.rule.severity] = (counts[f.rule.severity] || 0) + 1;
|
|
247
|
-
const order = ["critical", "high", "medium", "low", "info"];
|
|
248
|
-
const parts = order.filter((s) => counts[s]).map((s) => c(s, `${counts[s]} ${s}`));
|
|
249
|
-
console.error(`\x1b[1m${report.findings.length} finding(s)\x1b[0m — ${parts.join(", ")}\n`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return report.findings.length > 0 ? 1 : 0;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* `great-cto list-rules` — print the rule catalog.
|
|
256
|
-
*/
|
|
257
|
-
async function runListRules() {
|
|
258
|
-
let loadRules;
|
|
259
|
-
try {
|
|
260
|
-
({ loadRules } = await import("./agentshield/rules-loader.js"));
|
|
261
|
-
}
|
|
262
|
-
catch (e) {
|
|
263
|
-
error(`list-rules: failed: ${e.message}`);
|
|
264
|
-
return 2;
|
|
265
|
-
}
|
|
266
|
-
const rules = loadRules();
|
|
267
|
-
for (const r of rules) {
|
|
268
|
-
console.log(`${r.id.padEnd(8)} ${r.severity.padEnd(8)} ${r.scanner.padEnd(20)} ${r.title}`);
|
|
269
|
-
}
|
|
270
|
-
console.log(`\n${rules.length} rule(s) loaded.`);
|
|
271
|
-
return 0;
|
|
272
|
-
}
|
|
273
139
|
async function runRegister(args) {
|
|
274
140
|
const { join } = await import("node:path");
|
|
275
141
|
const { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } = await import("node:fs");
|
|
@@ -464,9 +330,7 @@ ${bold("Usage:")}
|
|
|
464
330
|
npx great-cto [init] [options] Detect + bootstrap
|
|
465
331
|
npx great-cto board [--port 3141] [--no-open]
|
|
466
332
|
npx great-cto register [--dir PATH]
|
|
467
|
-
npx great-cto
|
|
468
|
-
npx great-cto list-rules
|
|
469
|
-
npx great-cto ci [path] [--fail-on LVL] [--sarif F] [--junit F]
|
|
333
|
+
npx great-cto ci [path] [--no-archetype] [--no-budget]
|
|
470
334
|
npx great-cto mcp [--sse --port N]
|
|
471
335
|
npx great-cto adapt [--dry-run]
|
|
472
336
|
npx great-cto serve [--port 3142]
|
|
@@ -490,27 +354,18 @@ ${bold("Upgrade:")}
|
|
|
490
354
|
great-cto upgrade beads Upgrade beads only
|
|
491
355
|
${dim("(Safe to run any time — idempotent if already on latest)")}
|
|
492
356
|
|
|
493
|
-
${bold("Scan (AI-security):")}
|
|
494
|
-
great-cto scan AI-specific scan of cwd (OWASP LLM Top 10)
|
|
495
|
-
great-cto scan ./src --severity high Filter by minimum severity
|
|
496
|
-
great-cto scan --scanner ssrf-in-tools Run only one scanner
|
|
497
|
-
great-cto scan --sarif out.sarif Emit SARIF for GitHub Code Scanning
|
|
498
|
-
great-cto scan --json JSON output for CI pipelines
|
|
499
|
-
great-cto list-rules Print rule catalog
|
|
500
|
-
${dim("(exits 1 if findings ≥ severity threshold; CI-friendly)")}
|
|
501
|
-
|
|
502
357
|
${bold("CI gate:")}
|
|
503
|
-
great-cto ci Single-command CI gate (
|
|
504
|
-
great-cto ci --
|
|
505
|
-
great-cto ci --
|
|
506
|
-
|
|
507
|
-
${dim("(auto-detects \$GITHUB_ACTIONS → emits ::error:: annotations)")}
|
|
358
|
+
great-cto ci Single-command CI gate (archetype + budget check)
|
|
359
|
+
great-cto ci --no-archetype Skip the archetype-drift check
|
|
360
|
+
great-cto ci --no-budget Skip the monthly-budget sanity check
|
|
361
|
+
${dim("(exits 1 on archetype drift; budget is warn-only)")}
|
|
508
362
|
|
|
509
363
|
${bold("MCP server (cross-platform):")}
|
|
510
364
|
great-cto mcp Stdio MCP server — works in Claude Code /
|
|
511
365
|
Claude Desktop / any MCP host
|
|
512
366
|
great-cto mcp --sse --port 8765 SSE mode for remote / multi-client (TODO v2.5)
|
|
513
|
-
${dim("Tools exposed:
|
|
367
|
+
${dim("Tools exposed: detect_archetype, estimate_cost, query_decisions,")}
|
|
368
|
+
${dim(" project_status, cost_summary, pipeline_stages, recent_verdicts")}
|
|
514
369
|
|
|
515
370
|
${bold("Claude Code adapter:")}
|
|
516
371
|
great-cto adapt Generate AGENTS.md + CLAUDE.md
|
|
@@ -997,26 +852,6 @@ async function main() {
|
|
|
997
852
|
log(`Run ${cyan("great-cto --help")} for usage.`);
|
|
998
853
|
process.exit(2);
|
|
999
854
|
}
|
|
1000
|
-
if (args.command === "scan") {
|
|
1001
|
-
try {
|
|
1002
|
-
const code = await runScan(args, rawArgv);
|
|
1003
|
-
process.exit(code);
|
|
1004
|
-
}
|
|
1005
|
-
catch (e) {
|
|
1006
|
-
error(e.message);
|
|
1007
|
-
process.exit(2);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
if (args.command === "list-rules") {
|
|
1011
|
-
try {
|
|
1012
|
-
const code = await runListRules();
|
|
1013
|
-
process.exit(code);
|
|
1014
|
-
}
|
|
1015
|
-
catch (e) {
|
|
1016
|
-
error(e.message);
|
|
1017
|
-
process.exit(2);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
855
|
if (args.command === "board") {
|
|
1021
856
|
try {
|
|
1022
857
|
const code = await runBoard(args);
|
|
@@ -1080,8 +915,11 @@ async function main() {
|
|
|
1080
915
|
if (args.command === "serve") {
|
|
1081
916
|
try {
|
|
1082
917
|
const { runServe } = await import("./serve.js");
|
|
918
|
+
const explicitPort = rawArgv.some((a) => a === "--port" || a.startsWith("--port="));
|
|
1083
919
|
const code = await runServe({
|
|
1084
|
-
|
|
920
|
+
// serve defaults to 3142 (board uses 3141). Honor an explicit --port
|
|
921
|
+
// of any value, including 3141 — only fall back to 3142 when unset.
|
|
922
|
+
port: explicitPort ? args.boardPort : 3142,
|
|
1085
923
|
noLog: rawArgv.includes("--no-log"),
|
|
1086
924
|
insecure: rawArgv.includes("--insecure"),
|
|
1087
925
|
});
|
|
@@ -1128,8 +966,8 @@ async function main() {
|
|
|
1128
966
|
log(` ${cyan("/" + tried)} ${dim("[args]")}`);
|
|
1129
967
|
log("");
|
|
1130
968
|
log(`The CLI surface (this command) only exposes:`);
|
|
1131
|
-
log(` ${cyan("init")} · ${cyan("
|
|
1132
|
-
log(` ${cyan("
|
|
969
|
+
log(` ${cyan("init")} · ${cyan("ci")} · ${cyan("mcp")} · ${cyan("adapt")} ·`);
|
|
970
|
+
log(` ${cyan("serve")} · ${cyan("webhook")} · ${cyan("report")} · ${cyan("board")} · ${cyan("register")} · ${cyan("upgrade")}`);
|
|
1133
971
|
log("");
|
|
1134
972
|
log(`Run ${cyan("npx great-cto --help")} for the full CLI reference.`);
|
|
1135
973
|
process.exit(2);
|
package/dist/mcp.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
// sse — long-running HTTP/SSE for remote / multi-client access
|
|
11
11
|
//
|
|
12
12
|
// Tools exposed:
|
|
13
|
-
// scan OWASP LLM Top 10 + 24 rules → findings
|
|
14
|
-
// list_rules full rule catalogue
|
|
15
13
|
// detect_archetype archetype + compliance for a path
|
|
16
14
|
// estimate_cost LLM/human time for a task
|
|
17
15
|
// query_decisions search ~/.great_cto/decisions.md
|
|
16
|
+
// project_status board project state
|
|
17
|
+
// cost_summary board cost rollup
|
|
18
|
+
// pipeline_stages board pipeline stage breakdown
|
|
19
|
+
// recent_verdicts board recent agent verdicts
|
|
18
20
|
//
|
|
19
21
|
// Protocol: minimal MCP 2024-11-05 implementation. We hand-roll because
|
|
20
22
|
// adding @modelcontextprotocol/sdk would balloon install size for what is
|
|
@@ -28,41 +30,6 @@ const SERVER_INFO = {
|
|
|
28
30
|
version: "", // populated at startup
|
|
29
31
|
};
|
|
30
32
|
// ── Tool implementations ────────────────────────────────────────────────────
|
|
31
|
-
async function toolScan(args) {
|
|
32
|
-
const { scan } = await import("./agentshield/scanner.js");
|
|
33
|
-
const path = args.path ?? ".";
|
|
34
|
-
const report = scan(resolve(path), {
|
|
35
|
-
minSeverity: (args.severity ?? "info"),
|
|
36
|
-
scanners: args.scanner,
|
|
37
|
-
});
|
|
38
|
-
return {
|
|
39
|
-
files_scanned: report.filesScanned,
|
|
40
|
-
duration_ms: report.durationMs,
|
|
41
|
-
findings: report.findings.map((f) => ({
|
|
42
|
-
rule_id: f.rule.id,
|
|
43
|
-
severity: f.rule.severity,
|
|
44
|
-
title: f.rule.title,
|
|
45
|
-
owasp: f.rule.owasp,
|
|
46
|
-
file: f.location.file,
|
|
47
|
-
line: f.location.line,
|
|
48
|
-
snippet: f.location.snippet,
|
|
49
|
-
})),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
async function toolListRules() {
|
|
53
|
-
const { loadRules } = await import("./agentshield/rules-loader.js");
|
|
54
|
-
const rules = loadRules();
|
|
55
|
-
return {
|
|
56
|
-
count: rules.length,
|
|
57
|
-
rules: rules.map((r) => ({
|
|
58
|
-
id: r.id,
|
|
59
|
-
severity: r.severity,
|
|
60
|
-
scanner: r.scanner,
|
|
61
|
-
title: r.title,
|
|
62
|
-
owasp: r.owasp ?? null,
|
|
63
|
-
})),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
33
|
async function toolDetectArchetype(args) {
|
|
67
34
|
const { detect } = await import("./detect.js");
|
|
68
35
|
const { pickArchetype, suggestCompliance } = await import("./archetypes.js");
|
|
@@ -234,36 +201,6 @@ async function toolRecentVerdicts(args) {
|
|
|
234
201
|
}
|
|
235
202
|
// ── Tool dispatch table ────────────────────────────────────────────────────
|
|
236
203
|
const TOOLS = [
|
|
237
|
-
{
|
|
238
|
-
name: "scan",
|
|
239
|
-
description: "Scan code for AI/LLM-specific security issues (OWASP LLM Top 10, 24 rules). Returns findings with severity, file, line, OWASP mapping.",
|
|
240
|
-
inputSchema: {
|
|
241
|
-
type: "object",
|
|
242
|
-
properties: {
|
|
243
|
-
path: { type: "string", description: "File or directory to scan (default: cwd)" },
|
|
244
|
-
severity: {
|
|
245
|
-
type: "string",
|
|
246
|
-
enum: ["info", "low", "medium", "high", "critical"],
|
|
247
|
-
description: "Minimum severity to report",
|
|
248
|
-
},
|
|
249
|
-
scanner: {
|
|
250
|
-
type: "array",
|
|
251
|
-
items: {
|
|
252
|
-
type: "string",
|
|
253
|
-
enum: ["prompt-injection", "secrets-in-prompts", "ssrf-in-tools", "rag-poisoning", "cost-runaway"],
|
|
254
|
-
},
|
|
255
|
-
description: "Limit to specific scanner categories",
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
handler: toolScan,
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
name: "list_rules",
|
|
263
|
-
description: "List all 24 AgentShield security rules with severity and OWASP LLM Top 10 mapping.",
|
|
264
|
-
inputSchema: { type: "object", properties: {} },
|
|
265
|
-
handler: toolListRules,
|
|
266
|
-
},
|
|
267
204
|
{
|
|
268
205
|
name: "detect_archetype",
|
|
269
206
|
description: "Detect the project archetype (one of 25: fintech, healthcare, commerce, agent-product, mlops, edtech, gov-public, insurance, ...) and the compliance gates that apply.",
|
package/dist/report.js
CHANGED
|
@@ -45,10 +45,14 @@ function readAllVerdicts() {
|
|
|
45
45
|
return out.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
46
46
|
}
|
|
47
47
|
function periodToCutoff(period) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// "all" intentionally returns "" → every verdict passes the `ts >= cutoff`
|
|
49
|
+
// filter (no lower bound).
|
|
50
|
+
if (period === "all")
|
|
50
51
|
return "";
|
|
51
|
-
|
|
52
|
+
// Malformed input (e.g. "30" without the "d", "1w") must NOT silently fall
|
|
53
|
+
// through to all-time — fall back to the documented 30d default instead.
|
|
54
|
+
const m = period.match(/^(\d+)d$/);
|
|
55
|
+
const days = m ? parseInt(m[1], 10) : 30;
|
|
52
56
|
return new Date(Date.now() - days * 86_400_000).toISOString();
|
|
53
57
|
}
|
|
54
58
|
// ── Cost report ────────────────────────────────────────────────────────────
|