great-cto 2.0.0 → 2.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/agentshield-rules/cost-runaway.yaml +149 -0
- package/agentshield-rules/prompt-injection.yaml +117 -0
- package/agentshield-rules/rag-poisoning.yaml +113 -0
- package/agentshield-rules/secrets-in-prompts.yaml +90 -0
- package/agentshield-rules/ssrf-in-tools.yaml +99 -0
- package/dist/agentshield/index.js +15 -0
- package/dist/agentshield/rules-loader.js +175 -0
- package/dist/agentshield/sarif.js +80 -0
- package/dist/agentshield/scanner.js +219 -0
- package/dist/agentshield/types.js +10 -0
- package/dist/archetypes.js +213 -5
- package/dist/main.js +167 -1
- package/package.json +3 -2
package/dist/archetypes.js
CHANGED
|
@@ -141,17 +141,20 @@ const RULES = [
|
|
|
141
141
|
archetype: "agent-product",
|
|
142
142
|
score: (d) => {
|
|
143
143
|
const llms = ["anthropic-sdk", "openai-sdk", "google-ai", "aws-bedrock", "cohere"];
|
|
144
|
+
const llmFrameworks = ["langchain", "llamaindex"]; // RAG-capable LLM frameworks
|
|
144
145
|
const vdbs = ["pinecone", "weaviate", "chroma", "qdrant"];
|
|
145
146
|
const agents = ["langgraph", "crewai", "autogen", "mastra", "mcp"];
|
|
146
147
|
const hasLlm = llms.some((s) => d.stack.includes(s));
|
|
148
|
+
const hasLlmFw = llmFrameworks.some((s) => d.stack.includes(s));
|
|
147
149
|
const hasVdb = vdbs.some((s) => d.stack.includes(s));
|
|
148
150
|
const hasAgentFw = agents.some((s) => d.stack.includes(s));
|
|
149
151
|
let s = 0;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
// RAG-style agent: any LLM (raw SDK or framework) + vector DB
|
|
153
|
+
if ((hasLlm || hasLlmFw) && hasVdb)
|
|
154
|
+
s += 7;
|
|
152
155
|
if (hasAgentFw)
|
|
153
156
|
s += 6; // explicit agent framework
|
|
154
|
-
if (hasLlm && hasAgentFw)
|
|
157
|
+
if ((hasLlm || hasLlmFw) && hasAgentFw)
|
|
155
158
|
s += 2; // bonus
|
|
156
159
|
// README mining hint
|
|
157
160
|
if (d.readmeKeywords.includes("agent") || d.readmeKeywords.includes("ai"))
|
|
@@ -212,8 +215,17 @@ const RULES = [
|
|
|
212
215
|
if (agents.some((a) => d.stack.includes(a)))
|
|
213
216
|
s = Math.max(0, s - 2);
|
|
214
217
|
const vdbs = ["pinecone", "weaviate", "chroma", "qdrant"];
|
|
215
|
-
|
|
218
|
+
const hasVdb = vdbs.some((v) => d.stack.includes(v));
|
|
219
|
+
if (hasVdb)
|
|
216
220
|
s = Math.max(0, s - 2);
|
|
221
|
+
// Strong RAG signal — LangChain/LlamaIndex + VDB is almost certainly
|
|
222
|
+
// an agent-product (RAG-style), not a generic ai-system. The +5 score
|
|
223
|
+
// these frameworks contribute (above) is appropriate when there's no
|
|
224
|
+
// VDB; with a VDB it overcounts. Apply an additional deduction so
|
|
225
|
+
// agent-product wins the tie. See test "LangChain + Pinecone → agent-product".
|
|
226
|
+
const hasLlmFw = ["langchain", "llamaindex"].some((s) => d.stack.includes(s));
|
|
227
|
+
if (hasVdb && hasLlmFw)
|
|
228
|
+
s = Math.max(0, s - 5);
|
|
217
229
|
return s;
|
|
218
230
|
},
|
|
219
231
|
reason: (d) => {
|
|
@@ -794,12 +806,173 @@ const RULES = [
|
|
|
794
806
|
return "no web/mobile/infra framework detected — looks like a library/SDK";
|
|
795
807
|
},
|
|
796
808
|
},
|
|
809
|
+
// ── edtech (education technology — COPPA / FERPA / WCAG-AA child safety) ──
|
|
810
|
+
// Distinct from cms (general content) and healthcare (PHI). Drives age-gate,
|
|
811
|
+
// parental-consent, and accessibility patterns.
|
|
812
|
+
{
|
|
813
|
+
archetype: "edtech",
|
|
814
|
+
score: (d) => {
|
|
815
|
+
let s = 0;
|
|
816
|
+
const lmsLibs = ["canvas-lms", "moodle-api", "schoology-sdk", "blackboard-rest",
|
|
817
|
+
"google-classroom", "khan-academy-cli", "learnosity",
|
|
818
|
+
"kahoot-api", "h5p", "scorm", "lti", "lti-1.3"];
|
|
819
|
+
lmsLibs.forEach((l) => { if (d.stack.includes(l))
|
|
820
|
+
s += 6; });
|
|
821
|
+
// Auth/identity providers commonly used in edtech
|
|
822
|
+
if (d.stack.includes("clever-sdk"))
|
|
823
|
+
s += 6;
|
|
824
|
+
if (d.stack.includes("classlink-sso"))
|
|
825
|
+
s += 4;
|
|
826
|
+
const kws = d.readmeKeywords;
|
|
827
|
+
const eduKeywords = ["student", "classroom", "teacher", "k-12", "k12",
|
|
828
|
+
"lms", "learning management", "grade book", "gradebook",
|
|
829
|
+
"enrollment", "transcript", "pupil", "tutoring", "edtech"];
|
|
830
|
+
const matchedKws = eduKeywords.filter((k) => kws.includes(k));
|
|
831
|
+
if (matchedKws.length >= 2)
|
|
832
|
+
s += 5;
|
|
833
|
+
else if (matchedKws.length === 1)
|
|
834
|
+
s += 2;
|
|
835
|
+
// Strong signal: COPPA / FERPA explicitly mentioned
|
|
836
|
+
if (kws.includes("coppa") || kws.includes("ferpa"))
|
|
837
|
+
s += 6;
|
|
838
|
+
if (kws.includes("parental consent") || kws.includes("age gate"))
|
|
839
|
+
s += 4;
|
|
840
|
+
return s;
|
|
841
|
+
},
|
|
842
|
+
reason: (d) => {
|
|
843
|
+
const kws = d.readmeKeywords;
|
|
844
|
+
const bits = [];
|
|
845
|
+
const lmsLibs = ["canvas-lms", "moodle-api", "schoology-sdk", "google-classroom", "lti", "scorm"];
|
|
846
|
+
lmsLibs.forEach((l) => { if (d.stack.includes(l))
|
|
847
|
+
bits.push(l); });
|
|
848
|
+
if (kws.includes("coppa"))
|
|
849
|
+
bits.push("COPPA mention");
|
|
850
|
+
if (kws.includes("ferpa"))
|
|
851
|
+
bits.push("FERPA mention");
|
|
852
|
+
if (kws.includes("k-12") || kws.includes("k12"))
|
|
853
|
+
bits.push("K-12 keyword");
|
|
854
|
+
if (kws.includes("student"))
|
|
855
|
+
bits.push("student-data keyword");
|
|
856
|
+
return `edtech detected (${bits.join(", ") || "education domain signals"}) — COPPA/FERPA/WCAG-AA child-safety gates required`;
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
// ── gov-public (government / civic-tech — FedRAMP / NIST 800-53 / Section 508) ──
|
|
860
|
+
// Severe regulatory burden. Distinct from regulated (which is more EU-focused
|
|
861
|
+
// DORA/NIS2). gov-public targets US federal/state + UK gov.uk patterns.
|
|
862
|
+
{
|
|
863
|
+
archetype: "gov-public",
|
|
864
|
+
score: (d) => {
|
|
865
|
+
let s = 0;
|
|
866
|
+
const govLibs = ["login-gov-sdk", "id-me-sdk", "idme-sdk",
|
|
867
|
+
"usds-design-system", "uswds", "uk-gov-design-system",
|
|
868
|
+
"gov-uk-frontend", "verify-gov-uk",
|
|
869
|
+
"usajobs-sdk", "data-gov", "irs-modernized-efile"];
|
|
870
|
+
govLibs.forEach((l) => { if (d.stack.includes(l))
|
|
871
|
+
s += 6; });
|
|
872
|
+
const kws = d.readmeKeywords;
|
|
873
|
+
const govKeywords = ["fedramp", "fisma", "nist 800-53", "nist-800-53",
|
|
874
|
+
"section 508", "section-508", "ato", "civic tech",
|
|
875
|
+
"government", "municipal", "federal", "agency",
|
|
876
|
+
"gov.uk", "usds", "data.gov", "usa.gov", "irs", "ssa",
|
|
877
|
+
"department of", "stateramp", "cjis"];
|
|
878
|
+
const matchedKws = govKeywords.filter((k) => kws.includes(k));
|
|
879
|
+
if (matchedKws.length >= 2)
|
|
880
|
+
s += 6;
|
|
881
|
+
else if (matchedKws.length === 1)
|
|
882
|
+
s += 3;
|
|
883
|
+
// Very strong signals
|
|
884
|
+
if (kws.includes("fedramp") || kws.includes("fisma"))
|
|
885
|
+
s += 4;
|
|
886
|
+
if (kws.includes("section 508") || kws.includes("section-508"))
|
|
887
|
+
s += 3;
|
|
888
|
+
if (kws.includes("ato"))
|
|
889
|
+
s += 3;
|
|
890
|
+
return s;
|
|
891
|
+
},
|
|
892
|
+
reason: (d) => {
|
|
893
|
+
const kws = d.readmeKeywords;
|
|
894
|
+
const bits = [];
|
|
895
|
+
if (d.stack.includes("login-gov-sdk"))
|
|
896
|
+
bits.push("login.gov");
|
|
897
|
+
if (d.stack.includes("usds-design-system") || d.stack.includes("uswds"))
|
|
898
|
+
bits.push("USWDS");
|
|
899
|
+
if (d.stack.includes("uk-gov-design-system") || d.stack.includes("gov-uk-frontend"))
|
|
900
|
+
bits.push("gov.uk Design System");
|
|
901
|
+
if (kws.includes("fedramp"))
|
|
902
|
+
bits.push("FedRAMP mention");
|
|
903
|
+
if (kws.includes("nist-800-53") || kws.includes("nist 800-53"))
|
|
904
|
+
bits.push("NIST 800-53 mention");
|
|
905
|
+
if (kws.includes("section 508") || kws.includes("section-508"))
|
|
906
|
+
bits.push("Section 508 mention");
|
|
907
|
+
return `gov-public detected (${bits.join(", ") || "government domain signals"}) — FedRAMP/NIST 800-53/Section 508 gates required`;
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
// ── insurance (insurtech — NAIC / Solvency II / actuarial / claims fraud) ──
|
|
911
|
+
// Fintech-adjacent but distinct: multi-state filings, anti-discrimination
|
|
912
|
+
// pricing, actuarial model auditability, claims fraud detection.
|
|
913
|
+
{
|
|
914
|
+
archetype: "insurance",
|
|
915
|
+
score: (d) => {
|
|
916
|
+
let s = 0;
|
|
917
|
+
const insuranceLibs = ["acord-standards", "naic-schemas", "drools-rules",
|
|
918
|
+
"solvency2-calc", "openexposure", "ms-actuarial",
|
|
919
|
+
"lloyds-vendor-api", "verisk-sdk", "ccc-one-sdk",
|
|
920
|
+
"guidewire-cloud", "duck-creek", "majesco-sdk",
|
|
921
|
+
"ebix", "aplus-pas"];
|
|
922
|
+
insuranceLibs.forEach((l) => { if (d.stack.includes(l))
|
|
923
|
+
s += 6; });
|
|
924
|
+
const kws = d.readmeKeywords;
|
|
925
|
+
const insuranceKeywords = ["policy", "underwriting", "premium", "claim",
|
|
926
|
+
"actuarial", "reinsurance", "naic", "solvency",
|
|
927
|
+
"broker", "carrier", "mga", "mgu", "tpa",
|
|
928
|
+
"insurance", "insurtech", "insurer", "insured",
|
|
929
|
+
"deductible", "coverage", "bordereau"];
|
|
930
|
+
const matchedKws = insuranceKeywords.filter((k) => kws.includes(k));
|
|
931
|
+
if (matchedKws.length >= 3)
|
|
932
|
+
s += 6;
|
|
933
|
+
else if (matchedKws.length === 2)
|
|
934
|
+
s += 3;
|
|
935
|
+
else if (matchedKws.length === 1)
|
|
936
|
+
s += 1;
|
|
937
|
+
// Very strong signals — NAIC/Solvency/IFRS 17 explicit
|
|
938
|
+
if (kws.includes("naic") || kws.includes("solvency ii") || kws.includes("solvency-ii"))
|
|
939
|
+
s += 5;
|
|
940
|
+
if (kws.includes("ifrs 17") || kws.includes("ifrs-17"))
|
|
941
|
+
s += 4;
|
|
942
|
+
if (kws.includes("insurtech"))
|
|
943
|
+
s += 4;
|
|
944
|
+
// Don't double-score: if also matches commerce/fintech, subtract
|
|
945
|
+
// since insurance is distinct domain (not generic fintech)
|
|
946
|
+
if (d.stack.includes("stripe") && !insuranceLibs.some((l) => d.stack.includes(l))) {
|
|
947
|
+
s = Math.max(0, s - 2);
|
|
948
|
+
}
|
|
949
|
+
return s;
|
|
950
|
+
},
|
|
951
|
+
reason: (d) => {
|
|
952
|
+
const kws = d.readmeKeywords;
|
|
953
|
+
const bits = [];
|
|
954
|
+
const insuranceLibs = ["acord-standards", "naic-schemas", "guidewire-cloud", "duck-creek", "majesco-sdk"];
|
|
955
|
+
insuranceLibs.forEach((l) => { if (d.stack.includes(l))
|
|
956
|
+
bits.push(l); });
|
|
957
|
+
if (kws.includes("naic"))
|
|
958
|
+
bits.push("NAIC mention");
|
|
959
|
+
if (kws.includes("solvency ii") || kws.includes("solvency-ii"))
|
|
960
|
+
bits.push("Solvency II mention");
|
|
961
|
+
if (kws.includes("actuarial"))
|
|
962
|
+
bits.push("actuarial keyword");
|
|
963
|
+
if (kws.includes("underwriting"))
|
|
964
|
+
bits.push("underwriting keyword");
|
|
965
|
+
if (kws.includes("insurtech"))
|
|
966
|
+
bits.push("insurtech keyword");
|
|
967
|
+
return `insurance detected (${bits.join(", ") || "insurance domain signals"}) — NAIC/Solvency II/actuarial-audit gates required`;
|
|
968
|
+
},
|
|
969
|
+
},
|
|
797
970
|
];
|
|
798
971
|
// Tie-break priority — when two rules score equally, prefer the one
|
|
799
972
|
// higher in this list (more specific / domain-bound first).
|
|
800
973
|
const TIE_BREAK_PRIORITY = [
|
|
801
974
|
"browser-extension", "iot-embedded", "web3", "game",
|
|
802
|
-
"agent-product", "fintech", "healthcare", "marketplace",
|
|
975
|
+
"agent-product", "fintech", "insurance", "healthcare", "edtech", "gov-public", "marketplace",
|
|
803
976
|
"mlops", "streaming",
|
|
804
977
|
"commerce", "enterprise-saas", "ai-system", "devtools",
|
|
805
978
|
"data-platform", "cms", "infra", "mobile-app",
|
|
@@ -944,6 +1117,41 @@ export function suggestCompliance(d, archetype) {
|
|
|
944
1117
|
c.add("gdpr");
|
|
945
1118
|
c.add("dsa-eu");
|
|
946
1119
|
}
|
|
1120
|
+
if (archetype === "edtech") {
|
|
1121
|
+
c.add("coppa");
|
|
1122
|
+
c.add("ferpa");
|
|
1123
|
+
c.add("gdpr-k");
|
|
1124
|
+
c.add("wcag-2.2-aa");
|
|
1125
|
+
c.add("section-508");
|
|
1126
|
+
// State student-privacy laws
|
|
1127
|
+
c.add("sopipa-ca");
|
|
1128
|
+
}
|
|
1129
|
+
if (archetype === "gov-public") {
|
|
1130
|
+
c.add("fedramp");
|
|
1131
|
+
c.add("nist-800-53");
|
|
1132
|
+
c.add("fisma");
|
|
1133
|
+
c.add("section-508");
|
|
1134
|
+
c.add("pia");
|
|
1135
|
+
// CJIS only if law-enforcement keywords present
|
|
1136
|
+
const kws = d.readmeKeywords;
|
|
1137
|
+
if (kws.includes("cjis") || kws.includes("law enforcement") || kws.includes("criminal justice")) {
|
|
1138
|
+
c.add("cjis");
|
|
1139
|
+
}
|
|
1140
|
+
// StateRAMP if state-level
|
|
1141
|
+
if (kws.includes("stateramp") || kws.includes("state government"))
|
|
1142
|
+
c.add("stateramp");
|
|
1143
|
+
c.add("ato"); // Authority to Operate
|
|
1144
|
+
}
|
|
1145
|
+
if (archetype === "insurance") {
|
|
1146
|
+
c.add("naic");
|
|
1147
|
+
c.add("solvency-ii");
|
|
1148
|
+
c.add("ifrs-17");
|
|
1149
|
+
c.add("gdpr");
|
|
1150
|
+
c.add("ccpa");
|
|
1151
|
+
c.add("anti-discrimination-pricing");
|
|
1152
|
+
c.add("actuarial-asops");
|
|
1153
|
+
c.add("state-doi"); // Department of Insurance per US state
|
|
1154
|
+
}
|
|
947
1155
|
// ── stack-derived (cross-archetype) ──────────────
|
|
948
1156
|
if (d.stack.includes("stripe") || d.stack.includes("braintree") ||
|
|
949
1157
|
d.stack.includes("adyen") || d.stack.includes("paddle")) {
|
package/dist/main.js
CHANGED
|
@@ -79,6 +79,10 @@ function parseArgs(argv) {
|
|
|
79
79
|
args.command = "board";
|
|
80
80
|
else if (a === "register")
|
|
81
81
|
args.command = "register";
|
|
82
|
+
else if (a === "scan")
|
|
83
|
+
args.command = "scan";
|
|
84
|
+
else if (a === "list-rules")
|
|
85
|
+
args.command = "list-rules";
|
|
82
86
|
else if (a.startsWith("--dir="))
|
|
83
87
|
args.dir = a.slice("--dir=".length);
|
|
84
88
|
else if (a === "--dir")
|
|
@@ -92,6 +96,136 @@ function parseArgs(argv) {
|
|
|
92
96
|
args.dir = resolve(args.dir);
|
|
93
97
|
return args;
|
|
94
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* `great-cto scan [path]` — AI-specific security scanner (formerly @great-cto/agentshield).
|
|
101
|
+
*
|
|
102
|
+
* Detects OWASP LLM Top 10 patterns: prompt injection vectors, secrets in
|
|
103
|
+
* prompts, SSRF in tool definitions, RAG poisoning, cost-runaway loops.
|
|
104
|
+
*
|
|
105
|
+
* Flags (parsed from raw argv since they're scan-specific):
|
|
106
|
+
* --severity <lvl> info|low|medium|high|critical (default: info)
|
|
107
|
+
* --scanner <name> prompt-injection | secrets-in-prompts | ssrf-in-tools |
|
|
108
|
+
* rag-poisoning | cost-runaway (repeatable)
|
|
109
|
+
* --sarif <file> emit SARIF 2.1.0 to file
|
|
110
|
+
* --json emit JSON to stdout
|
|
111
|
+
* --quiet suppress human-readable output
|
|
112
|
+
* --max <n> stop after N findings
|
|
113
|
+
* --exclude <regex> add path exclude (repeatable)
|
|
114
|
+
*
|
|
115
|
+
* Exit codes:
|
|
116
|
+
* 0 = no findings (or all below severity threshold)
|
|
117
|
+
* 1 = findings at/above threshold (CI-friendly)
|
|
118
|
+
* 2 = scan failed
|
|
119
|
+
*/
|
|
120
|
+
async function runScan(args, rawArgv) {
|
|
121
|
+
const { writeFileSync } = await import("node:fs");
|
|
122
|
+
const { resolve: resolvePath } = await import("node:path");
|
|
123
|
+
// Lazy import compiled scanner — keeps cold start fast for `init` flow.
|
|
124
|
+
let scan;
|
|
125
|
+
let toSarif;
|
|
126
|
+
try {
|
|
127
|
+
({ scan } = await import("./agentshield/scanner.js"));
|
|
128
|
+
({ toSarif } = await import("./agentshield/sarif.js"));
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
error(`scan: failed to load scanner: ${e.message}`);
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
// Parse scan-specific flags from raw argv
|
|
135
|
+
const flag = (n) => rawArgv.includes(`--${n}`);
|
|
136
|
+
const value = (n, def) => {
|
|
137
|
+
const i = rawArgv.indexOf(`--${n}`);
|
|
138
|
+
return i >= 0 && i < rawArgv.length - 1 ? rawArgv[i + 1] : def;
|
|
139
|
+
};
|
|
140
|
+
const scanners = rawArgv
|
|
141
|
+
.map((a, i) => (a === "--scanner" ? rawArgv[i + 1] : null))
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
const exclude = rawArgv
|
|
144
|
+
.map((a, i) => (a === "--exclude" ? rawArgv[i + 1] : null))
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
// Path: first non-flag arg after `scan`, default cwd
|
|
147
|
+
const scanIdx = rawArgv.indexOf("scan");
|
|
148
|
+
let root = ".";
|
|
149
|
+
for (let i = scanIdx + 1; i < rawArgv.length; i++) {
|
|
150
|
+
if (rawArgv[i] && !rawArgv[i].startsWith("--")) {
|
|
151
|
+
root = rawArgv[i];
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const opts = {
|
|
156
|
+
scanners: scanners.length > 0 ? scanners : undefined,
|
|
157
|
+
minSeverity: value("severity", "info"),
|
|
158
|
+
exclude: exclude.length > 0 ? exclude : undefined,
|
|
159
|
+
maxFindings: value("max") ? parseInt(value("max"), 10) : undefined,
|
|
160
|
+
};
|
|
161
|
+
const sarifPath = value("sarif");
|
|
162
|
+
const wantsJson = flag("json");
|
|
163
|
+
const quiet = flag("quiet");
|
|
164
|
+
const report = scan(resolvePath(root), opts);
|
|
165
|
+
if (sarifPath) {
|
|
166
|
+
writeFileSync(sarifPath, JSON.stringify(toSarif(report), null, 2));
|
|
167
|
+
if (!quiet)
|
|
168
|
+
console.error(`✓ SARIF written → ${sarifPath}`);
|
|
169
|
+
}
|
|
170
|
+
if (wantsJson) {
|
|
171
|
+
console.log(JSON.stringify(report, null, 2));
|
|
172
|
+
}
|
|
173
|
+
else if (!quiet) {
|
|
174
|
+
const COLORS = {
|
|
175
|
+
critical: "\x1b[1;31m", high: "\x1b[31m", medium: "\x1b[33m",
|
|
176
|
+
low: "\x1b[36m", info: "\x1b[2m", reset: "\x1b[0m",
|
|
177
|
+
};
|
|
178
|
+
const useColor = process.stdout.isTTY;
|
|
179
|
+
const c = (sev, s) => (useColor ? `${COLORS[sev] || ""}${s}${COLORS.reset}` : s);
|
|
180
|
+
console.error(`\ngreat-cto scan ${getCliVersion()} — scanned ${report.filesScanned} file(s) in ${report.durationMs}ms\n`);
|
|
181
|
+
if (report.errors.length > 0) {
|
|
182
|
+
console.error(`\x1b[33m⚠ ${report.errors.length} error(s):\x1b[0m`);
|
|
183
|
+
for (const e of report.errors)
|
|
184
|
+
console.error(` ${e}`);
|
|
185
|
+
console.error("");
|
|
186
|
+
}
|
|
187
|
+
if (report.findings.length === 0) {
|
|
188
|
+
console.error("\x1b[32m✓ No findings.\x1b[0m\n");
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const f of report.findings) {
|
|
192
|
+
const tag = c(f.rule.severity, `[${f.rule.severity.toUpperCase()}]`);
|
|
193
|
+
console.error(`${tag} ${f.rule.id} ${f.location.file}:${f.location.line}`);
|
|
194
|
+
console.error(` ${f.rule.title}`);
|
|
195
|
+
console.error(` ${c("info", f.location.snippet)}`);
|
|
196
|
+
if (f.rule.owasp)
|
|
197
|
+
console.error(` ${c("info", f.rule.owasp)}`);
|
|
198
|
+
console.error("");
|
|
199
|
+
}
|
|
200
|
+
const counts = {};
|
|
201
|
+
for (const f of report.findings)
|
|
202
|
+
counts[f.rule.severity] = (counts[f.rule.severity] || 0) + 1;
|
|
203
|
+
const order = ["critical", "high", "medium", "low", "info"];
|
|
204
|
+
const parts = order.filter((s) => counts[s]).map((s) => c(s, `${counts[s]} ${s}`));
|
|
205
|
+
console.error(`\x1b[1m${report.findings.length} finding(s)\x1b[0m — ${parts.join(", ")}\n`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return report.findings.length > 0 ? 1 : 0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* `great-cto list-rules` — print the rule catalog.
|
|
212
|
+
*/
|
|
213
|
+
async function runListRules() {
|
|
214
|
+
let loadRules;
|
|
215
|
+
try {
|
|
216
|
+
({ loadRules } = await import("./agentshield/rules-loader.js"));
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
error(`list-rules: failed: ${e.message}`);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
const rules = loadRules();
|
|
223
|
+
for (const r of rules) {
|
|
224
|
+
console.log(`${r.id.padEnd(8)} ${r.severity.padEnd(8)} ${r.scanner.padEnd(20)} ${r.title}`);
|
|
225
|
+
}
|
|
226
|
+
console.log(`\n${rules.length} rule(s) loaded.`);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
95
229
|
async function runRegister(args) {
|
|
96
230
|
const { join } = await import("node:path");
|
|
97
231
|
const { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } = await import("node:fs");
|
|
@@ -180,6 +314,8 @@ ${bold("Usage:")}
|
|
|
180
314
|
npx great-cto [init] [options]
|
|
181
315
|
npx great-cto board [--port 3141] [--no-open]
|
|
182
316
|
npx great-cto register [--dir PATH]
|
|
317
|
+
npx great-cto scan [path] [--severity LVL] [--scanner NAME] [--sarif FILE]
|
|
318
|
+
npx great-cto list-rules
|
|
183
319
|
npx great-cto help
|
|
184
320
|
npx great-cto version
|
|
185
321
|
|
|
@@ -193,6 +329,15 @@ ${bold("Register:")}
|
|
|
193
329
|
(auto-discovered after /audit or /start, but
|
|
194
330
|
run this if the project doesn't appear in board)
|
|
195
331
|
|
|
332
|
+
${bold("Scan (AI-security):")}
|
|
333
|
+
great-cto scan AI-specific scan of cwd (OWASP LLM Top 10)
|
|
334
|
+
great-cto scan ./src --severity high Filter by minimum severity
|
|
335
|
+
great-cto scan --scanner ssrf-in-tools Run only one scanner
|
|
336
|
+
great-cto scan --sarif out.sarif Emit SARIF for GitHub Code Scanning
|
|
337
|
+
great-cto scan --json JSON output for CI pipelines
|
|
338
|
+
great-cto list-rules Print rule catalog
|
|
339
|
+
${dim("(exits 1 if findings ≥ severity threshold; CI-friendly)")}
|
|
340
|
+
|
|
196
341
|
${bold("Options:")}
|
|
197
342
|
-y, --yes Skip confirmation prompts (non-interactive)
|
|
198
343
|
--dry-run Show what would be done without doing it
|
|
@@ -541,11 +686,32 @@ async function runInit(args) {
|
|
|
541
686
|
return 0;
|
|
542
687
|
}
|
|
543
688
|
async function main() {
|
|
544
|
-
const
|
|
689
|
+
const rawArgv = process.argv.slice(2);
|
|
690
|
+
const args = parseArgs(rawArgv);
|
|
545
691
|
if (args.command === "help") {
|
|
546
692
|
printHelp();
|
|
547
693
|
process.exit(0);
|
|
548
694
|
}
|
|
695
|
+
if (args.command === "scan") {
|
|
696
|
+
try {
|
|
697
|
+
const code = await runScan(args, rawArgv);
|
|
698
|
+
process.exit(code);
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
error(e.message);
|
|
702
|
+
process.exit(2);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (args.command === "list-rules") {
|
|
706
|
+
try {
|
|
707
|
+
const code = await runListRules();
|
|
708
|
+
process.exit(code);
|
|
709
|
+
}
|
|
710
|
+
catch (e) {
|
|
711
|
+
error(e.message);
|
|
712
|
+
process.exit(2);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
549
715
|
if (args.command === "board") {
|
|
550
716
|
try {
|
|
551
717
|
const code = await runBoard(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -69,12 +69,13 @@
|
|
|
69
69
|
"files": [
|
|
70
70
|
"index.mjs",
|
|
71
71
|
"dist/",
|
|
72
|
+
"agentshield-rules/",
|
|
72
73
|
"README.md"
|
|
73
74
|
],
|
|
74
75
|
"type": "module",
|
|
75
76
|
"scripts": {
|
|
76
77
|
"build": "tsc",
|
|
77
|
-
"test": "npm run build && node --test tests/*.test.mjs",
|
|
78
|
+
"test": "npm run build && node --test tests/*.test.mjs tests/**/*.test.mjs",
|
|
78
79
|
"test:e2e": "npm run build && node ../../tests/run-archetype-e2e.mjs",
|
|
79
80
|
"prepublishOnly": "npm run build"
|
|
80
81
|
},
|