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.
Files changed (49) hide show
  1. package/cli/agents/abom-generator.js +1 -1
  2. package/cli/agents/agent-attestation-agent.js +10 -1
  3. package/cli/agents/agent-config-scanner.js +1 -1
  4. package/cli/agents/ai-infra-inventory-agent.js +482 -482
  5. package/cli/agents/base-agent.js +1 -1
  6. package/cli/agents/endpoint-agent-abuse-agent.js +1 -1
  7. package/cli/agents/html-reporter.js +3 -3
  8. package/cli/agents/index.js +2 -2
  9. package/cli/agents/injection-tester.js +8 -1
  10. package/cli/agents/memory-poisoning-agent.js +1 -1
  11. package/cli/agents/model-file-scanner.js +1 -1
  12. package/cli/agents/orchestrator.js +11 -6
  13. package/cli/agents/prompt-injection-prober.js +228 -228
  14. package/cli/bin/praxis.js +7 -3
  15. package/cli/commands/agent-fix.js +1091 -1245
  16. package/cli/commands/audit.js +1228 -1216
  17. package/cli/commands/baseline.js +1 -1
  18. package/cli/commands/benchmark.js +1 -1
  19. package/cli/commands/ci.js +45 -21
  20. package/cli/commands/deps.js +11 -5
  21. package/cli/commands/env-audit.js +1 -1
  22. package/cli/commands/fix.js +1 -1
  23. package/cli/commands/mcp.js +1 -1
  24. package/cli/commands/red-team.js +350 -350
  25. package/cli/commands/remediate.js +1 -1
  26. package/cli/commands/rotate.js +1 -1
  27. package/cli/commands/rules.js +1 -1
  28. package/cli/commands/scan.js +554 -554
  29. package/cli/commands/score.js +1 -1
  30. package/cli/commands/undo.js +22 -77
  31. package/cli/commands/vibe-check.js +1 -1
  32. package/cli/core/fix-plan.js +274 -0
  33. package/cli/core/fs.js +27 -0
  34. package/cli/core/git-clone.js +8 -6
  35. package/cli/core/glob.js +56 -0
  36. package/cli/core/output/html-theme.js +158 -158
  37. package/cli/core/output/sarif.js +2 -2
  38. package/cli/core/web/jobs.js +2 -2
  39. package/cli/core/web/server.js +19 -8
  40. package/cli/data/threatpacks/latest.json +41 -41
  41. package/cli/integrations/github-action.js +136 -0
  42. package/cli/utils/plugin-loader.js +15 -95
  43. package/cli/utils/rule-import.js +227 -227
  44. package/cli/utils/rule-registry.js +425 -425
  45. package/cli/utils/scan-fingerprint.js +1 -1
  46. package/cli/utils/score-history.js +118 -118
  47. package/docs/USAGE.md +16 -9
  48. package/docs/design/WEB-UI.md +4 -5
  49. package/package.json +13 -4
@@ -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 'fast-glob';
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 'fast-glob';
37
+ import fg from '../core/glob.js';
38
38
 
39
39
  // =============================================================================
40
40
  // INDUSTRY BENCHMARKS (aggregated from public research)
@@ -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 'fast-glob';
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
- } catch { /* skip */ }
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
- pass: determinePass(scoreResult, allFindings, threshold, failOn, alwaysFailOn, preBaselineFindings),
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 (P-IMP-044) ───────────────────────────
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.log(`[praxis] Warning: Could not post PR comment: ${err.message}`);
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, allFindings, threshold, failOn, alwaysFailOn, preBaselineFindings);
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 title = (f.title || f.rule || 'Security Finding').replace(/\r?\n/g, ' ');
260
- const msg = (f.description || f.matched || '').replace(/\r?\n/g, ' ').slice(0, 300);
261
- console.log(`::${level} file=${rel},line=${f.line},col=${col},title=${title}::${msg}`);
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` (P-IMP-062). This
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.log('[praxis] No PR detected — skipping PR comment');
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.log(`[praxis] PR comment posted on #${prNumber}`);
371
+ console.error(`[praxis] PR comment posted on #${prNumber}`);
348
372
  }
349
373
 
350
374
  function checkIntelFreshness(maxAge) {
@@ -229,7 +229,10 @@ function parseNpmAudit(jsonStr) {
229
229
  try {
230
230
  data = JSON.parse(jsonStr);
231
231
  } catch {
232
- return [];
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
- return [];
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 (Array.isArray(data) ? data : [])) {
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 } or { pm: null, vulns: [] } if no manifest found.
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
  }
@@ -22,7 +22,7 @@
22
22
 
23
23
  import fs from 'fs';
24
24
  import path from 'path';
25
- import fg from 'fast-glob';
25
+ import fg from '../core/glob.js';
26
26
  import chalk from 'chalk';
27
27
  import ora from 'ora';
28
28
  import { execFileSync } from 'child_process';
@@ -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 'fast-glob';
26
+ import fg from '../core/glob.js';
27
27
  import * as output from '../utils/output.js';
28
28
 
29
29
  // =============================================================================
@@ -34,7 +34,7 @@
34
34
 
35
35
  import fs from 'fs';
36
36
  import path from 'path';
37
- import fg from 'fast-glob';
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';