great-cto 2.30.1 → 2.32.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/archetypes.js +1 -1
- package/dist/bootstrap.js +0 -50
- package/dist/ci.js +10 -141
- package/dist/main.js +9 -174
- package/dist/mcp.js +4 -67
- package/package.json +2 -3
- 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/archetypes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Archetype decision: detected stack → archetype recommendation.
|
|
2
|
-
// Mirrors great_cto's
|
|
2
|
+
// Mirrors great_cto's 25 archetypes (+ greenfield fallback) in skills/great_cto/ARCHETYPES.md.
|
|
3
3
|
// Rules are evaluated; highest score wins.
|
|
4
4
|
const RULES = [
|
|
5
5
|
// ── browser-extension ─────────────────────────────
|
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/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);
|
|
@@ -1128,8 +963,8 @@ async function main() {
|
|
|
1128
963
|
log(` ${cyan("/" + tried)} ${dim("[args]")}`);
|
|
1129
964
|
log("");
|
|
1130
965
|
log(`The CLI surface (this command) only exposes:`);
|
|
1131
|
-
log(` ${cyan("init")} · ${cyan("
|
|
1132
|
-
log(` ${cyan("
|
|
966
|
+
log(` ${cyan("init")} · ${cyan("ci")} · ${cyan("mcp")} · ${cyan("adapt")} ·`);
|
|
967
|
+
log(` ${cyan("serve")} · ${cyan("webhook")} · ${cyan("report")} · ${cyan("board")} · ${cyan("register")} · ${cyan("upgrade")}`);
|
|
1133
968
|
log("");
|
|
1134
969
|
log(`Run ${cyan("npx great-cto --help")} for the full CLI reference.`);
|
|
1135
970
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -70,7 +70,6 @@
|
|
|
70
70
|
"index.mjs",
|
|
71
71
|
"dist/",
|
|
72
72
|
"assets/",
|
|
73
|
-
"agentshield-rules/",
|
|
74
73
|
"postinstall.mjs",
|
|
75
74
|
"README.md"
|
|
76
75
|
],
|
|
@@ -89,4 +88,4 @@
|
|
|
89
88
|
"engines": {
|
|
90
89
|
"node": ">=18.17.0"
|
|
91
90
|
}
|
|
92
|
-
}
|
|
91
|
+
}
|