praxis-sec 1.2.0 → 1.2.1
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/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 +3 -3
- package/cli/agents/index.js +2 -2
- package/cli/agents/injection-tester.js +8 -1
- package/cli/agents/memory-poisoning-agent.js +1 -1
- package/cli/agents/model-file-scanner.js +1 -1
- package/cli/agents/orchestrator.js +11 -6
- 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 +1228 -1216
- package/cli/commands/baseline.js +1 -1
- package/cli/commands/benchmark.js +1 -1
- package/cli/commands/ci.js +45 -21
- package/cli/commands/deps.js +11 -5
- package/cli/commands/env-audit.js +1 -1
- package/cli/commands/fix.js +1 -1
- package/cli/commands/mcp.js +1 -1
- package/cli/commands/red-team.js +350 -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.js +554 -554
- package/cli/commands/score.js +1 -1
- package/cli/commands/undo.js +22 -77
- package/cli/commands/vibe-check.js +1 -1
- 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/sarif.js +2 -2
- 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/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 +13 -4
package/cli/commands/baseline.js
CHANGED
|
@@ -19,7 +19,7 @@ import ora from 'ora';
|
|
|
19
19
|
import { buildOrchestrator } from '../agents/index.js';
|
|
20
20
|
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
21
21
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
22
|
-
import fg from '
|
|
22
|
+
import fg from '../core/glob.js';
|
|
23
23
|
|
|
24
24
|
const BASELINE_FILE = '.praxis/baseline.json';
|
|
25
25
|
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from '../utils/patterns.js';
|
|
35
35
|
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
36
36
|
import * as output from '../utils/output.js';
|
|
37
|
-
import fg from '
|
|
37
|
+
import fg from '../core/glob.js';
|
|
38
38
|
|
|
39
39
|
// =============================================================================
|
|
40
40
|
// INDUSTRY BENCHMARKS (aggregated from public research)
|
package/cli/commands/ci.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
38
38
|
import { ThreatIntel } from '../utils/threat-intel.js';
|
|
39
39
|
import * as intelOrchestrator from '../utils/intel/index.js';
|
|
40
|
-
import fg from '
|
|
40
|
+
import fg from '../core/glob.js';
|
|
41
41
|
|
|
42
42
|
// =============================================================================
|
|
43
43
|
// MAIN COMMAND
|
|
@@ -50,6 +50,12 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
50
50
|
const alwaysFailOn = options.alwaysFailOn || null;
|
|
51
51
|
const sarifPath = options.sarif || null;
|
|
52
52
|
|
|
53
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 100 ||
|
|
54
|
+
[failOn, alwaysFailOn].some(value => value && !['critical', 'high', 'medium', 'low'].includes(value))) {
|
|
55
|
+
console.error('[praxis] Invalid CI gate: threshold must be 0-100 and severity must be critical, high, medium, or low.');
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
if (!fs.existsSync(absolutePath)) {
|
|
54
60
|
console.error(`[praxis] Path does not exist: ${absolutePath}`);
|
|
55
61
|
process.exit(1);
|
|
@@ -99,16 +105,26 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
99
105
|
|
|
100
106
|
// ── Agent Scan ───────────────────────────────────────────────────────────
|
|
101
107
|
const orchestrator = buildOrchestrator();
|
|
102
|
-
const results = await orchestrator.runAll(absolutePath, { quiet: true }); // praxis-ignore — orchestrator result, not LLM output triggering actions
|
|
108
|
+
const results = await orchestrator.runAll(absolutePath, { quiet: true, deep: options.deep }); // praxis-ignore — orchestrator result, not LLM output triggering actions
|
|
103
109
|
const agentFindings = results.findings;
|
|
110
|
+
let scanComplete = results.agentResults.every(agent => agent.success);
|
|
111
|
+
if (!scanComplete) console.error('[praxis] Scan incomplete: one or more agents failed.');
|
|
104
112
|
|
|
105
113
|
// ── Dependency Audit ─────────────────────────────────────────────────────
|
|
106
114
|
let depVulns = [];
|
|
115
|
+
let dependencyAudit = options.deps === false ? 'skipped' : 'not-applicable';
|
|
107
116
|
if (options.deps !== false) {
|
|
108
117
|
try {
|
|
109
118
|
const depResult = await runDepsAudit(absolutePath);
|
|
110
119
|
depVulns = depResult.vulns || [];
|
|
111
|
-
|
|
120
|
+
dependencyAudit = depResult.error ? 'failed' : (depResult.pm ? 'complete' : 'not-applicable');
|
|
121
|
+
} catch {
|
|
122
|
+
dependencyAudit = 'failed';
|
|
123
|
+
}
|
|
124
|
+
if (dependencyAudit === 'failed') {
|
|
125
|
+
scanComplete = false;
|
|
126
|
+
console.error('[praxis] Dependency audit incomplete. Install the package manager audit tool and check registry access, or explicitly use --no-deps.');
|
|
127
|
+
}
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
// ── Merge & Deduplicate ──────────────────────────────────────────────────
|
|
@@ -140,6 +156,8 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
140
156
|
scoringEngine.saveToHistory(absolutePath, scoreResult);
|
|
141
157
|
|
|
142
158
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
159
|
+
const gateFindings = [...allFindings, ...depVulns];
|
|
160
|
+
const floorFindings = [...preBaselineFindings, ...depVulns];
|
|
143
161
|
|
|
144
162
|
// ── SARIF Output ─────────────────────────────────────────────────────────
|
|
145
163
|
if (sarifPath) {
|
|
@@ -168,7 +186,10 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
168
186
|
})),
|
|
169
187
|
} : {}),
|
|
170
188
|
threshold,
|
|
171
|
-
|
|
189
|
+
scanComplete,
|
|
190
|
+
dependencyAudit,
|
|
191
|
+
floorPass: determinePass(scoreResult, [], 0, null, alwaysFailOn, floorFindings),
|
|
192
|
+
pass: scanComplete && determinePass(scoreResult, gateFindings, threshold, failOn, alwaysFailOn, floorFindings),
|
|
172
193
|
duration: `${duration}s`,
|
|
173
194
|
}, null, 2));
|
|
174
195
|
} else {
|
|
@@ -192,7 +213,7 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
|
|
195
|
-
// ── GitHub PR Inline Annotations
|
|
216
|
+
// ── GitHub PR Inline Annotations ───────────────────────────
|
|
196
217
|
emitGitHubAnnotations(allFindings, absolutePath);
|
|
197
218
|
|
|
198
219
|
// ── GitHub PR Comment ──────────────────────────────────────────────────
|
|
@@ -200,12 +221,12 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
200
221
|
try {
|
|
201
222
|
postPRComment(scoreResult, allFindings, depVulns, absolutePath, duration);
|
|
202
223
|
} catch (err) {
|
|
203
|
-
console.
|
|
224
|
+
console.error(`[praxis] Warning: Could not post PR comment: ${err.message}`);
|
|
204
225
|
}
|
|
205
226
|
}
|
|
206
227
|
|
|
207
228
|
// ── Exit Code ────────────────────────────────────────────────────────────
|
|
208
|
-
const pass = determinePass(scoreResult,
|
|
229
|
+
const pass = scanComplete && determinePass(scoreResult, gateFindings, threshold, failOn, alwaysFailOn, floorFindings);
|
|
209
230
|
if (!pass) {
|
|
210
231
|
if (!options.json) {
|
|
211
232
|
if (failOn) {
|
|
@@ -229,14 +250,7 @@ export async function ciCommand(targetPath = '.', options = {}) {
|
|
|
229
250
|
// HELPERS
|
|
230
251
|
// =============================================================================
|
|
231
252
|
|
|
232
|
-
function determinePass(scoreResult, findings, threshold, failOn, alwaysFailOn, preBaselineFindings = null) {
|
|
233
|
-
if (failOn) {
|
|
234
|
-
const sevOrder = ['critical', 'high', 'medium', 'low'];
|
|
235
|
-
const failIndex = sevOrder.indexOf(failOn);
|
|
236
|
-
if (failIndex === -1) return scoreResult.score >= threshold;
|
|
237
|
-
const blockingSevs = sevOrder.slice(0, failIndex + 1);
|
|
238
|
-
return !findings.some(f => blockingSevs.includes(f.severity));
|
|
239
|
-
}
|
|
253
|
+
export function determinePass(scoreResult, findings, threshold, failOn, alwaysFailOn, preBaselineFindings = null) {
|
|
240
254
|
if (alwaysFailOn) {
|
|
241
255
|
const sevOrder = ['critical', 'high', 'medium', 'low'];
|
|
242
256
|
const floorIndex = sevOrder.indexOf(alwaysFailOn);
|
|
@@ -246,6 +260,13 @@ function determinePass(scoreResult, findings, threshold, failOn, alwaysFailOn, p
|
|
|
246
260
|
const floorSet = preBaselineFindings || findings;
|
|
247
261
|
if (floorSet.some(f => floorSevs.includes(f.severity))) return false;
|
|
248
262
|
}
|
|
263
|
+
if (failOn) {
|
|
264
|
+
const sevOrder = ['critical', 'high', 'medium', 'low'];
|
|
265
|
+
const failIndex = sevOrder.indexOf(failOn);
|
|
266
|
+
if (failIndex === -1) return scoreResult.score >= threshold;
|
|
267
|
+
const blockingSevs = sevOrder.slice(0, failIndex + 1);
|
|
268
|
+
return !findings.some(f => blockingSevs.includes(f.severity));
|
|
269
|
+
}
|
|
249
270
|
return scoreResult.score >= threshold;
|
|
250
271
|
}
|
|
251
272
|
|
|
@@ -256,16 +277,19 @@ function emitGitHubAnnotations(findings, rootPath) {
|
|
|
256
277
|
const rel = path.relative(rootPath, f.file).replace(/\\/g, '/');
|
|
257
278
|
const level = ['critical', 'high'].includes(f.severity) ? 'error' : 'warning';
|
|
258
279
|
const col = f.column || 1;
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
280
|
+
const escapeData = value => String(value).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
281
|
+
const escapeProperty = value => escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
282
|
+
const title = escapeProperty(f.title || f.rule || 'Security Finding');
|
|
283
|
+
// Matched text may be a live credential. Annotations only need the description.
|
|
284
|
+
const msg = escapeData(String(f.description || 'Security finding detected').slice(0, 300));
|
|
285
|
+
console.error(`::${level} file=${escapeProperty(rel)},line=${f.line},col=${col},title=${title}::${msg}`);
|
|
262
286
|
}
|
|
263
287
|
}
|
|
264
288
|
|
|
265
289
|
/**
|
|
266
290
|
* SARIF for GitHub Code Scanning.
|
|
267
291
|
*
|
|
268
|
-
* Delegates to the shared serializer in `cli/core/output/sarif.js
|
|
292
|
+
* Delegates to the shared serializer in `cli/core/output/sarif.js`. This
|
|
269
293
|
* used to be a private copy, which is why the GitHub Action's SARIF carried no
|
|
270
294
|
* `security-severity` at all — the fix landed in the registry and never reached here.
|
|
271
295
|
*
|
|
@@ -301,7 +325,7 @@ function postPRComment(scoreResult, findings, depVulns, rootPath, duration) {
|
|
|
301
325
|
const parsed = JSON.parse(prJson);
|
|
302
326
|
prNumber = String(parsed.number);
|
|
303
327
|
} catch {
|
|
304
|
-
console.
|
|
328
|
+
console.error('[praxis] No PR detected — skipping PR comment');
|
|
305
329
|
return;
|
|
306
330
|
}
|
|
307
331
|
}
|
|
@@ -344,7 +368,7 @@ function postPRComment(scoreResult, findings, depVulns, rootPath, duration) {
|
|
|
344
368
|
stdio: ['pipe', 'pipe', 'pipe'], // praxis-ignore
|
|
345
369
|
});
|
|
346
370
|
|
|
347
|
-
console.
|
|
371
|
+
console.error(`[praxis] PR comment posted on #${prNumber}`);
|
|
348
372
|
}
|
|
349
373
|
|
|
350
374
|
function checkIntelFreshness(maxAge) {
|
package/cli/commands/deps.js
CHANGED
|
@@ -229,7 +229,10 @@ function parseNpmAudit(jsonStr) {
|
|
|
229
229
|
try {
|
|
230
230
|
data = JSON.parse(jsonStr);
|
|
231
231
|
} catch {
|
|
232
|
-
|
|
232
|
+
throw new Error('Dependency audit did not produce valid JSON');
|
|
233
|
+
}
|
|
234
|
+
if (!data || data.error || (!data.vulnerabilities && !data.advisories)) {
|
|
235
|
+
throw new Error('Dependency audit did not produce an audit report');
|
|
233
236
|
}
|
|
234
237
|
|
|
235
238
|
const vulns = [];
|
|
@@ -331,11 +334,14 @@ function parsePipAudit(jsonStr) {
|
|
|
331
334
|
try {
|
|
332
335
|
data = JSON.parse(jsonStr);
|
|
333
336
|
} catch {
|
|
334
|
-
|
|
337
|
+
throw new Error('Dependency audit did not produce valid JSON');
|
|
335
338
|
}
|
|
336
339
|
|
|
340
|
+
const dependencies = Array.isArray(data) ? data : data?.dependencies;
|
|
341
|
+
if (!Array.isArray(dependencies)) throw new Error('Dependency audit did not produce an audit report');
|
|
342
|
+
|
|
337
343
|
const vulns = [];
|
|
338
|
-
for (const pkg of
|
|
344
|
+
for (const pkg of dependencies) {
|
|
339
345
|
for (const vuln of (pkg.vulns || [])) {
|
|
340
346
|
const fixVersion = vuln.fix_versions?.[0];
|
|
341
347
|
vulns.push({
|
|
@@ -500,7 +506,7 @@ function printDepFindings(vulns, pm) {
|
|
|
500
506
|
|
|
501
507
|
/**
|
|
502
508
|
* Run the dependency audit for a given path and return normalized vulnerabilities.
|
|
503
|
-
* Returns { pm, vulns }
|
|
509
|
+
* Returns { pm, vulns, error? }. An unavailable audit is never a clean audit.
|
|
504
510
|
* Does not print anything — used programmatically by other commands.
|
|
505
511
|
*/
|
|
506
512
|
export async function runDepsAudit(rootPath) {
|
|
@@ -511,6 +517,6 @@ export async function runDepsAudit(rootPath) {
|
|
|
511
517
|
const vulns = runAudit(pm, rootPath);
|
|
512
518
|
return { pm, vulns };
|
|
513
519
|
} catch {
|
|
514
|
-
return { pm, vulns: [] };
|
|
520
|
+
return { pm, vulns: [], error: `${pm.name} dependency audit could not be completed` };
|
|
515
521
|
}
|
|
516
522
|
}
|
package/cli/commands/fix.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
MAX_FILE_SIZE
|
|
24
24
|
} from '../utils/patterns.js';
|
|
25
25
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
26
|
-
import fg from '
|
|
26
|
+
import fg from '../core/glob.js';
|
|
27
27
|
import * as output from '../utils/output.js';
|
|
28
28
|
|
|
29
29
|
// =============================================================================
|
package/cli/commands/mcp.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import fs from 'fs';
|
|
36
36
|
import path from 'path';
|
|
37
|
-
import fg from '
|
|
37
|
+
import fg from '../core/glob.js';
|
|
38
38
|
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, TEST_FILE_PATTERNS, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
39
39
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
40
40
|
import { buildOrchestrator } from '../agents/index.js';
|