praxis-sec 1.2.0 → 1.2.2
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/assets/praxis-architecture.svg +304 -0
- package/assets/praxis-logo.svg +38 -0
- package/cli/agents/abom-generator.js +1 -1
- package/cli/agents/agent-attestation-agent.js +10 -1
- package/cli/agents/agent-config-scanner.js +1 -1
- package/cli/agents/ai-infra-inventory-agent.js +482 -482
- package/cli/agents/base-agent.js +1 -1
- package/cli/agents/endpoint-agent-abuse-agent.js +1 -1
- package/cli/agents/html-reporter.js +10 -10
- package/cli/agents/index.js +2 -2
- package/cli/agents/injection-tester.js +8 -1
- package/cli/agents/mcp-security-agent.js +600 -594
- package/cli/agents/memory-poisoning-agent.js +1 -1
- package/cli/agents/model-file-scanner.js +1 -1
- package/cli/agents/orchestrator.js +375 -355
- package/cli/agents/prompt-injection-prober.js +228 -228
- package/cli/bin/praxis.js +7 -3
- package/cli/commands/agent-fix.js +1091 -1245
- package/cli/commands/audit.js +1232 -1216
- package/cli/commands/baseline.js +3 -2
- package/cli/commands/benchmark.js +1 -1
- package/cli/commands/ci.js +50 -25
- package/cli/commands/deps.js +11 -5
- package/cli/commands/diff.js +2 -1
- package/cli/commands/env-audit.js +1 -1
- package/cli/commands/fix.js +1 -1
- package/cli/commands/legal.js +2 -1
- package/cli/commands/mcp.js +3 -2
- package/cli/commands/openclaw.js +3 -6
- package/cli/commands/red-team.js +351 -350
- package/cli/commands/remediate.js +1 -1
- package/cli/commands/rotate.js +1 -1
- package/cli/commands/rules.js +1 -1
- package/cli/commands/scan-standard.js +3 -6
- package/cli/commands/scan.js +554 -554
- package/cli/commands/score.js +1 -1
- package/cli/commands/undo.js +22 -77
- package/cli/commands/vibe-check.js +3 -2
- package/cli/commands/watch.js +6 -5
- package/cli/core/fix-plan.js +274 -0
- package/cli/core/fs.js +27 -0
- package/cli/core/git-clone.js +8 -6
- package/cli/core/glob.js +56 -0
- package/cli/core/output/html-theme.js +158 -158
- package/cli/core/output/json.js +56 -48
- package/cli/core/output/sarif.js +2 -2
- package/cli/core/paths.js +91 -0
- package/cli/core/web/jobs.js +2 -2
- package/cli/core/web/server.js +19 -8
- package/cli/data/threatpacks/latest.json +41 -41
- package/cli/integrations/github-action.js +136 -0
- package/cli/utils/cache-manager.js +2 -1
- package/cli/utils/plugin-loader.js +15 -95
- package/cli/utils/rule-import.js +227 -227
- package/cli/utils/rule-registry.js +425 -425
- package/cli/utils/scan-fingerprint.js +1 -1
- package/cli/utils/score-history.js +118 -118
- package/docs/USAGE.md +16 -9
- package/docs/design/WEB-UI.md +4 -5
- package/package.json +81 -71
package/cli/commands/red-team.js
CHANGED
|
@@ -1,350 +1,351 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Red Team Command
|
|
3
|
-
* =================
|
|
4
|
-
*
|
|
5
|
-
* Run all security agents against the codebase.
|
|
6
|
-
* This is the main entry point for the multi-agent security audit.
|
|
7
|
-
*
|
|
8
|
-
* USAGE:
|
|
9
|
-
* npx praxis-sec red-team [path] Full multi-agent audit
|
|
10
|
-
* npx praxis-sec red-team . --agents injection,auth Run specific agents
|
|
11
|
-
* npx praxis-sec red-team . --json JSON output
|
|
12
|
-
* npx praxis-sec red-team . --html report.html Generate HTML report
|
|
13
|
-
* npx praxis-sec red-team . --sarif SARIF output
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import fs from 'fs';
|
|
17
|
-
import path from 'path';
|
|
18
|
-
import { renderFindingsSARIF } from '../core/output/sarif.js';
|
|
19
|
-
import chalk from 'chalk';
|
|
20
|
-
import ora from 'ora';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import
|
|
31
|
-
import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (options.
|
|
105
|
-
if (options.
|
|
106
|
-
if (options.
|
|
107
|
-
if (options.
|
|
108
|
-
if (options.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
scoringEngine.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
finding.
|
|
157
|
-
finding.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
console.log(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
console.log(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
console.log();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.log(
|
|
267
|
-
console.log(chalk.yellow(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
console.log(` ${
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
console.log(
|
|
288
|
-
|
|
289
|
-
if (recon.
|
|
290
|
-
if (recon.
|
|
291
|
-
if (recon.
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
console.log(
|
|
298
|
-
console.log(chalk.
|
|
299
|
-
console.log(chalk.gray('
|
|
300
|
-
console.log(chalk.gray('
|
|
301
|
-
console.log(chalk.gray('
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* `
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Red Team Command
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* Run all security agents against the codebase.
|
|
6
|
+
* This is the main entry point for the multi-agent security audit.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx praxis-sec red-team [path] Full multi-agent audit
|
|
10
|
+
* npx praxis-sec red-team . --agents injection,auth Run specific agents
|
|
11
|
+
* npx praxis-sec red-team . --json JSON output
|
|
12
|
+
* npx praxis-sec red-team . --html report.html Generate HTML report
|
|
13
|
+
* npx praxis-sec red-team . --sarif SARIF output
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { renderFindingsSARIF } from '../core/output/sarif.js';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
import ora from 'ora';
|
|
21
|
+
import { displayPath } from '../core/paths.js';
|
|
22
|
+
import { buildOrchestratorAsync } from '../agents/index.js';
|
|
23
|
+
import { SwarmOrchestrator } from '../agents/swarm-orchestrator.js';
|
|
24
|
+
import { ReconAgent } from '../agents/recon-agent.js';
|
|
25
|
+
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
26
|
+
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
27
|
+
import { HTMLReporter } from '../agents/html-reporter.js';
|
|
28
|
+
import { SBOMGenerator } from '../agents/sbom-generator.js';
|
|
29
|
+
import { autoDetectProvider } from '../providers/llm-provider.js';
|
|
30
|
+
import { runDepsAudit } from './deps.js';
|
|
31
|
+
import * as output from '../utils/output.js';
|
|
32
|
+
import { printBanner } from '../utils/output.js';
|
|
33
|
+
|
|
34
|
+
export async function redTeamCommand(targetPath = '.', options = {}) {
|
|
35
|
+
const absolutePath = path.resolve(targetPath);
|
|
36
|
+
|
|
37
|
+
// Any of these formats owns stdout; decorative output must not follow it.
|
|
38
|
+
const machineOutput = !!(options.json || options.sarif || options.html || options.sbom);
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(absolutePath)) {
|
|
41
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
let findings = [];
|
|
48
|
+
let recon = {};
|
|
49
|
+
let agentResults = [];
|
|
50
|
+
|
|
51
|
+
// ── 1a. Swarm mode (parallel execution via best available provider) ────────
|
|
52
|
+
if (options.swarm) {
|
|
53
|
+
printBanner();
|
|
54
|
+
output.header('AI Swarm Mode');
|
|
55
|
+
console.log();
|
|
56
|
+
|
|
57
|
+
const swarm = SwarmOrchestrator.create(absolutePath, {
|
|
58
|
+
provider: options.provider,
|
|
59
|
+
model: options.model,
|
|
60
|
+
verbose: options.verbose,
|
|
61
|
+
budgetCents: options.budget ?? 200,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!swarm) {
|
|
65
|
+
output.error('Swarm mode requires DEEPSEEK_API_KEY or MOONSHOT_API_KEY. Set one and retry.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const reconSpinner = ora({ text: 'Mapping attack surface...', color: 'cyan' }).start();
|
|
70
|
+
const reconAgent = new ReconAgent();
|
|
71
|
+
const reconResult = await reconAgent.analyze({ rootPath: absolutePath });
|
|
72
|
+
recon = Array.isArray(reconResult) ? {} : reconResult;
|
|
73
|
+
const files = await reconAgent.discoverFiles(absolutePath);
|
|
74
|
+
reconSpinner.succeed(chalk.green('Attack surface mapped'));
|
|
75
|
+
|
|
76
|
+
const providerLabel = swarm.provider?.name || 'AI';
|
|
77
|
+
const swarmSpinner = ora({ text: `Deploying ${chalk.cyan('23 swarm agents')} via ${providerLabel}...`, color: 'cyan' }).start();
|
|
78
|
+
try {
|
|
79
|
+
findings = await swarm.run(absolutePath, recon, files);
|
|
80
|
+
swarmSpinner.succeed(chalk.green(`Swarm complete — ${findings.length} finding(s)`));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
swarmSpinner.fail(chalk.red(`Swarm failed: ${err.message}`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
agentResults = [{ agent: 'KimiSwarm', category: 'swarm', findingCount: findings.length, success: true }];
|
|
87
|
+
|
|
88
|
+
} else {
|
|
89
|
+
// ── 1b. Standard local orchestration ───────────────────────────────────
|
|
90
|
+
printBanner();
|
|
91
|
+
output.header('Multi-Agent Security Audit');
|
|
92
|
+
console.log();
|
|
93
|
+
|
|
94
|
+
const orchestrator = await buildOrchestratorAsync(absolutePath, { quiet: true });
|
|
95
|
+
|
|
96
|
+
const agentFilter = options.agents
|
|
97
|
+
? options.agents.split(',').map(a => a.trim())
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
const orchestratorOpts = {
|
|
101
|
+
verbose: options.verbose,
|
|
102
|
+
agents: agentFilter,
|
|
103
|
+
};
|
|
104
|
+
if (options.deep) orchestratorOpts.deep = true;
|
|
105
|
+
if (options.local) orchestratorOpts.local = true;
|
|
106
|
+
if (options.model) orchestratorOpts.model = options.model;
|
|
107
|
+
if (options.provider) orchestratorOpts.provider = options.provider;
|
|
108
|
+
if (options.baseUrl) orchestratorOpts.baseUrl = options.baseUrl;
|
|
109
|
+
if (options.budget) orchestratorOpts.budget = options.budget;
|
|
110
|
+
|
|
111
|
+
const results = await orchestrator.runAll(absolutePath, orchestratorOpts); // praxis-ignore — orchestrator result, not LLM output triggering actions
|
|
112
|
+
({ recon, findings, agentResults } = results);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 2. Dependency audit ─────────────────────────────────────────────────────
|
|
116
|
+
let depVulns = [];
|
|
117
|
+
if (!options.noDeps) {
|
|
118
|
+
const depSpinner = ora({ text: 'Auditing dependencies...', color: 'cyan' }).start();
|
|
119
|
+
try {
|
|
120
|
+
const depResult = await runDepsAudit(absolutePath);
|
|
121
|
+
depVulns = depResult.vulns || [];
|
|
122
|
+
depSpinner.succeed(
|
|
123
|
+
depVulns.length === 0
|
|
124
|
+
? chalk.green('Dependencies: clean')
|
|
125
|
+
: chalk.yellow(`Dependencies: ${depVulns.length} CVE(s)`)
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
depSpinner.succeed(chalk.gray('Dependencies: skipped'));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── 3. Apply policy ─────────────────────────────────────────────────────────
|
|
133
|
+
const policy = PolicyEngine.load(absolutePath);
|
|
134
|
+
const filteredFindings = policy.applyPolicy(findings);
|
|
135
|
+
|
|
136
|
+
// ── 4. Score ────────────────────────────────────────────────────────────────
|
|
137
|
+
const scoringEngine = new ScoringEngine();
|
|
138
|
+
const scoreResult = scoringEngine.compute(filteredFindings, depVulns);
|
|
139
|
+
scoringEngine.saveToHistory(absolutePath, scoreResult);
|
|
140
|
+
|
|
141
|
+
// ── 5. AI classification (if provider available) ────────────────────────────
|
|
142
|
+
if (options.ai !== false) {
|
|
143
|
+
const provider = autoDetectProvider(absolutePath, {
|
|
144
|
+
provider: options.provider,
|
|
145
|
+
baseUrl: options.baseUrl,
|
|
146
|
+
model: options.model,
|
|
147
|
+
});
|
|
148
|
+
if (provider && filteredFindings.length > 0 && filteredFindings.length <= 50) {
|
|
149
|
+
const aiSpinner = ora({ text: `Classifying ${filteredFindings.length} finding(s) with ${provider.name}...`, color: 'cyan' }).start();
|
|
150
|
+
try {
|
|
151
|
+
const classifications = await provider.classify(filteredFindings);
|
|
152
|
+
// Merge classifications back into findings
|
|
153
|
+
for (const cl of classifications) {
|
|
154
|
+
const finding = filteredFindings.find(f => `${f.file}:${f.line}` === cl.id);
|
|
155
|
+
if (finding) {
|
|
156
|
+
finding.aiClassification = cl.classification;
|
|
157
|
+
finding.aiReason = cl.reason;
|
|
158
|
+
finding.aiFix = cl.fix;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
aiSpinner.succeed(chalk.green(`AI classification complete (${provider.name})`));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
aiSpinner.fail(chalk.yellow(`AI classification failed: ${err.message}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── 6. Output ───────────────────────────────────────────────────────────────
|
|
169
|
+
if (options.json) {
|
|
170
|
+
outputJSON(scoreResult, filteredFindings, recon, agentResults);
|
|
171
|
+
} else if (options.sarif) {
|
|
172
|
+
outputSARIF(filteredFindings, absolutePath);
|
|
173
|
+
} else if (options.html) {
|
|
174
|
+
const reporter = new HTMLReporter();
|
|
175
|
+
const htmlPath = typeof options.html === 'string' ? options.html : 'praxis-report.html';
|
|
176
|
+
reporter.generateToFile(scoreResult, filteredFindings, recon, absolutePath, htmlPath, agentResults);
|
|
177
|
+
output.success(`HTML report saved to ${htmlPath}`);
|
|
178
|
+
} else {
|
|
179
|
+
printResults(scoreResult, filteredFindings, recon, agentResults, depVulns, absolutePath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── 7. SBOM (if requested) ──────────────────────────────────────────────────
|
|
183
|
+
if (options.sbom) {
|
|
184
|
+
const sbomGen = new SBOMGenerator();
|
|
185
|
+
const sbomPath = typeof options.sbom === 'string' ? options.sbom : 'sbom.json';
|
|
186
|
+
sbomGen.generateToFile(absolutePath, sbomPath);
|
|
187
|
+
output.success(`SBOM saved to ${sbomPath}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── 8. Policy evaluation ────────────────────────────────────────────────────
|
|
191
|
+
const violations = policy.evaluate(scoreResult, filteredFindings, {
|
|
192
|
+
agentResults,
|
|
193
|
+
depsRun: options.deps !== false,
|
|
194
|
+
secretsRun: true,
|
|
195
|
+
depVulns,
|
|
196
|
+
});
|
|
197
|
+
if (violations.length > 0) {
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(chalk.red.bold(' Policy Violations:'));
|
|
200
|
+
for (const v of violations.slice(0, 10)) {
|
|
201
|
+
console.log(chalk.red(` ✗ ${v.message}`));
|
|
202
|
+
}
|
|
203
|
+
if (violations.length > 10) {
|
|
204
|
+
console.log(chalk.gray(` ... and ${violations.length - 10} more`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── 9. Trend ────────────────────────────────────────────────────────────────
|
|
209
|
+
// Suppressed for machine output. `scan redteam . --sarif > results.sarif` must
|
|
210
|
+
// produce a parseable file; printing a trend line after the SARIF document
|
|
211
|
+
// corrupts it, and `audit.js` already guards the same way via `machineOutput`.
|
|
212
|
+
if (!machineOutput) {
|
|
213
|
+
const trend = scoringEngine.getTrend(absolutePath, scoreResult.score);
|
|
214
|
+
if (trend) {
|
|
215
|
+
const arrow = trend.diff > 0 ? chalk.green('↑') : trend.diff < 0 ? chalk.red('↓') : chalk.gray('→');
|
|
216
|
+
console.log();
|
|
217
|
+
console.log(chalk.gray(` Trend: ${trend.previousScore} → ${trend.currentScore} ${arrow} (${trend.diff > 0 ? '+' : ''}${trend.diff})`));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Exit code
|
|
224
|
+
process.exit(scoreResult.score >= 75 ? 0 : 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// =============================================================================
|
|
228
|
+
// OUTPUT FORMATTERS
|
|
229
|
+
// =============================================================================
|
|
230
|
+
|
|
231
|
+
function printResults(scoreResult, findings, recon, agentResults, depVulns, rootPath) { // praxis-ignore
|
|
232
|
+
const GRADE_COLOR = { A: chalk.green.bold, B: chalk.cyan.bold, C: chalk.yellow.bold, D: chalk.red, F: chalk.red.bold };
|
|
233
|
+
const SEV_COLOR = { critical: chalk.red.bold, high: chalk.yellow, medium: chalk.blue, low: chalk.gray };
|
|
234
|
+
|
|
235
|
+
// ── Score ───────────────────────────────────────────────────────────────────
|
|
236
|
+
console.log();
|
|
237
|
+
const gradeColor = GRADE_COLOR[scoreResult.grade.letter] || chalk.white;
|
|
238
|
+
const scoreColor = scoreResult.score >= 75 ? chalk.green.bold : scoreResult.score >= 60 ? chalk.yellow.bold : chalk.red.bold;
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
chalk.white.bold(' Security Score: ') +
|
|
242
|
+
scoreColor(`${scoreResult.score}/100 `) +
|
|
243
|
+
gradeColor(scoreResult.grade.letter) +
|
|
244
|
+
chalk.gray(` — ${scoreResult.grade.label}`)
|
|
245
|
+
);
|
|
246
|
+
console.log(chalk.cyan(' ' + '─'.repeat(58)));
|
|
247
|
+
console.log();
|
|
248
|
+
|
|
249
|
+
// ── Category breakdown ──────────────────────────────────────────────────────
|
|
250
|
+
for (const [key, cat] of Object.entries(scoreResult.categories)) {
|
|
251
|
+
const count = Object.values(cat.counts).reduce((a, b) => a + b, 0);
|
|
252
|
+
const icon = count === 0 ? chalk.green('✔') : chalk.red('✘');
|
|
253
|
+
const status = count === 0 ? chalk.green('clean') : chalk.red(`${count} issue(s)`);
|
|
254
|
+
const deduction = cat.deduction > 0 ? chalk.red(`-${cat.deduction} pts`) : chalk.gray('+0');
|
|
255
|
+
console.log(` ${icon} ${chalk.white.bold(cat.label.padEnd(22))} ${status.padEnd(25)} ${deduction}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Dependencies row
|
|
259
|
+
const depCount = depVulns.length;
|
|
260
|
+
const depIcon = depCount === 0 ? chalk.green('✔') : chalk.red('✘');
|
|
261
|
+
const depDeduction = scoreResult.categories.deps?.deduction || 0;
|
|
262
|
+
console.log(` ${depIcon} ${chalk.white.bold('Dependencies'.padEnd(22))} ${depCount === 0 ? chalk.green('clean') : chalk.red(`${depCount} CVE(s)`)}`);
|
|
263
|
+
|
|
264
|
+
// ── Top findings ────────────────────────────────────────────────────────────
|
|
265
|
+
if (findings.length > 0) {
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(chalk.yellow.bold(` Top Findings (${Math.min(findings.length, 20)} of ${findings.length})`));
|
|
268
|
+
console.log(chalk.yellow(' ' + '─'.repeat(58)));
|
|
269
|
+
|
|
270
|
+
for (const f of findings.slice(0, 20)) {
|
|
271
|
+
const relFile = displayPath(f.file, rootPath);
|
|
272
|
+
const sevColor = SEV_COLOR[f.severity] || chalk.white;
|
|
273
|
+
const aiTag = f.aiClassification === 'FALSE_POSITIVE'
|
|
274
|
+
? chalk.gray(' [FP]')
|
|
275
|
+
: f.aiClassification === 'REAL' ? chalk.red(' [REAL]') : '';
|
|
276
|
+
|
|
277
|
+
console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`.padEnd(12))} ${chalk.white(`${relFile}:${f.line}`)}${aiTag}`);
|
|
278
|
+
console.log(` ${chalk.gray(' ')} ${f.title || f.rule}`);
|
|
279
|
+
if (f.aiFix || f.fix) {
|
|
280
|
+
console.log(` ${chalk.gray(' Fix:')} ${chalk.green((f.aiFix || f.fix).slice(0, 80))}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Attack surface summary ──────────────────────────────────────────────────
|
|
286
|
+
if (recon) {
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(chalk.cyan(' Attack Surface:'));
|
|
289
|
+
if (recon.frameworks?.length) console.log(chalk.gray(` Frameworks: ${recon.frameworks.join(', ')}`));
|
|
290
|
+
if (recon.databases?.length) console.log(chalk.gray(` Databases: ${recon.databases.join(', ')}`));
|
|
291
|
+
if (recon.authPatterns?.length) console.log(chalk.gray(` Auth: ${recon.authPatterns.join(', ')}`));
|
|
292
|
+
if (recon.apiRoutes?.length) console.log(chalk.gray(` API Routes: ${recon.apiRoutes.length} discovered`));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Next steps ──────────────────────────────────────────────────────────────
|
|
296
|
+
if (findings.length > 0) {
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(chalk.yellow.bold(' Next steps:'));
|
|
299
|
+
console.log(chalk.gray(' 1. Review and fix findings above'));
|
|
300
|
+
console.log(chalk.gray(' 2. Run again: ') + chalk.cyan('npx praxis-sec red-team .'));
|
|
301
|
+
console.log(chalk.gray(' 3. Generate report: ') + chalk.cyan('npx praxis-sec red-team . --html report.html'));
|
|
302
|
+
console.log(chalk.gray(' 4. Set policy: ') + chalk.cyan('npx praxis-sec policy init'));
|
|
303
|
+
} else {
|
|
304
|
+
console.log();
|
|
305
|
+
output.success('All agents report clean — safe to ship!');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(chalk.cyan(' ' + '═'.repeat(58)));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function outputJSON(scoreResult, findings, recon, agentResults) {
|
|
312
|
+
console.log(JSON.stringify({
|
|
313
|
+
score: scoreResult.score,
|
|
314
|
+
grade: scoreResult.grade.letter,
|
|
315
|
+
gradeLabel: scoreResult.grade.label,
|
|
316
|
+
totalFindings: findings.length,
|
|
317
|
+
categories: Object.fromEntries(
|
|
318
|
+
Object.entries(scoreResult.categories).map(([k, v]) => [k, {
|
|
319
|
+
label: v.label,
|
|
320
|
+
findingCount: Object.values(v.counts).reduce((a, b) => a + b, 0),
|
|
321
|
+
deduction: v.deduction,
|
|
322
|
+
counts: v.counts,
|
|
323
|
+
}])
|
|
324
|
+
),
|
|
325
|
+
findings: findings.map(f => ({
|
|
326
|
+
file: f.file,
|
|
327
|
+
line: f.line,
|
|
328
|
+
severity: f.severity,
|
|
329
|
+
category: f.category,
|
|
330
|
+
rule: f.rule,
|
|
331
|
+
title: f.title,
|
|
332
|
+
description: f.description,
|
|
333
|
+
fix: f.fix,
|
|
334
|
+
aiClassification: f.aiClassification,
|
|
335
|
+
aiFix: f.aiFix,
|
|
336
|
+
})),
|
|
337
|
+
recon,
|
|
338
|
+
agents: agentResults,
|
|
339
|
+
}, null, 2));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* SARIF for GitHub Code Scanning.
|
|
344
|
+
*
|
|
345
|
+
* Delegates to the shared serializer in `cli/core/output/sarif.js`; this was
|
|
346
|
+
* a private copy hardcoding a driver version of `4.0.0` and carrying no
|
|
347
|
+
* `security-severity`.
|
|
348
|
+
*/
|
|
349
|
+
function outputSARIF(findings, rootPath) {
|
|
350
|
+
console.log(renderFindingsSARIF(findings, { rootPath }));
|
|
351
|
+
}
|