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
@@ -1,158 +1,158 @@
1
- /**
2
- * Shared HTML report theme — single source of truth for every Praxis HTML surface.
3
- * ============================================================================
4
- *
5
- * Extracted (P-IMP-051c) because `cli/agents/html-reporter.js` and
6
- * `cli/commands/team-report.js` each carried their own ~100-line inline stylesheet
7
- * and their own severity palette. Those palettes had already drifted
8
- * (`critical` was `#ef4444` in one and `#dc2626` in the other), so every brand or
9
- * styling fix had to be applied twice — and the two files disagreed on the same
10
- * concept.
11
- *
12
- * Also centralises the primitives that must never be re-implemented per report:
13
- * - `esc()` — HTML escaping. `team-report.js` previously had none and
14
- * interpolated finding text straight into markup, which is
15
- * an injection sink in a file users open in a browser.
16
- * - `severityBadgeClass()` — maps a severity to a *known* CSS class, so an arbitrary
17
- * severity string can't be injected into a class attribute.
18
- * - `countBySeverity()` — one counting implementation, not one per report.
19
- *
20
- * Design constraints (per AGENTS.md): no build step, no bundler, no framework. These
21
- * are plain template strings and functions, so every report stays a single
22
- * self-contained offline HTML file.
23
- */
24
-
25
- /** Severity → accent colour. Used for bars, legends, and inline accents. */
26
- export const SEVERITY_COLORS = {
27
- critical: '#ef4444',
28
- high: '#f97316',
29
- medium: '#eab308',
30
- low: '#38bdf8',
31
- info: '#64748b',
32
- };
33
-
34
- /** Security-score letter grade → colour. */
35
- export const GRADE_COLORS = {
36
- A: '#22c55e',
37
- B: '#06b6d4',
38
- C: '#eab308',
39
- D: '#f97316',
40
- F: '#ef4444',
41
- };
42
-
43
- /** Every severity we recognise, in report order. */
44
- export const SEVERITIES = ['critical', 'high', 'medium', 'low'];
45
-
46
- /**
47
- * Severities rendered in distribution bars/legends. Includes `info` so the segments
48
- * always sum to 100% of the findings shown — omitting it left the bar visibly
49
- * under-filled whenever a scan produced informational findings.
50
- */
51
- export const DISPLAY_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
52
-
53
- /**
54
- * Escapes text for interpolation into HTML markup or a quoted attribute.
55
- * Null/undefined become an empty string so callers can interpolate directly.
56
- */
57
- export function esc(str) {
58
- if (str === null || str === undefined) return '';
59
- return String(str)
60
- .replace(/&/g, '&')
61
- .replace(/</g, '&lt;')
62
- .replace(/>/g, '&gt;')
63
- .replace(/"/g, '&quot;')
64
- .replace(/'/g, '&#039;');
65
- }
66
-
67
- /**
68
- * Maps a severity to a known-safe CSS class suffix.
69
- *
70
- * Returns `''` for anything unrecognised so the result is always a safe subset of
71
- * the class attribute — a finding whose `severity` came from a parsed report file
72
- * must never be able to break out of `class="sev-badge …"`.
73
- */
74
- export function severityBadgeClass(severity) {
75
- const key = String(severity || '').toLowerCase();
76
- return SEVERITIES.includes(key) || key === 'info' ? key : '';
77
- }
78
-
79
- /** Renders a severity badge span. Escapes the label and sanitises the class. */
80
- export function severityBadge(severity, label) {
81
- const cls = severityBadgeClass(severity);
82
- const text = label ?? String(severity || 'unknown');
83
- const classes = cls ? `sev-badge sev-${cls}` : 'sev-badge';
84
- return `<span class="${classes}">${esc(text)}</span>`;
85
- }
86
-
87
- /** Counts findings per severity, always returning a fully-populated record. */
88
- export function countBySeverity(findings = []) {
89
- const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
90
- for (const f of Array.isArray(findings) ? findings : []) {
91
- const key = String(f?.severity || '').toLowerCase();
92
- if (key in counts) counts[key]++;
93
- }
94
- return counts;
95
- }
96
-
97
- /**
98
- * The shared base stylesheet: reset, typography, layout container, tables, code,
99
- * severity badges, and the muted/empty/footer primitives. Every report includes
100
- * this, then appends its own component styles.
101
- */
102
- export function baseStyles() {
103
- return `
104
- *{margin:0;padding:0;box-sizing:border-box}
105
- html{scroll-behavior:smooth}
106
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:#090d16;color:#cbd5e1;line-height:1.55;font-size:14px}
107
- a{color:#38bdf8;text-decoration:none}
108
- a:hover{text-decoration:underline}
109
-
110
- .container{max-width:1440px;margin:1.8rem auto;padding:0 2rem}
111
- h1{font-size:1.8rem;font-weight:700;color:#f8fafc;margin-bottom:0.25rem}
112
- h2{font-size:1.1rem;font-weight:600;margin:2rem 0 1rem;color:#94a3b8;border-bottom:1px solid #1e293b;padding-bottom:0.5rem;text-transform:uppercase;letter-spacing:0.05em}
113
-
114
- .table-responsive{overflow-x:auto}
115
- table{width:100%;border-collapse:collapse;font-size:0.86rem;text-align:left}
116
- th{background:#131d33;color:#94a3b8;padding:0.75rem 1rem;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.6px;border-bottom:1px solid #1e293b}
117
- td{padding:0.75rem 1rem;border-bottom:1px solid #172239;vertical-align:top}
118
- tr:hover td{background:#111b30}
119
- code{background:#050811;border:1px solid #1e293b;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.8rem;word-break:break-word}
120
- small{color:#64748b}
121
-
122
- .sev-badge{display:inline-block;padding:3px 9px;border-radius:6px;font-size:0.72rem;font-weight:800;text-transform:uppercase;letter-spacing:0.5px}
123
- .sev-critical{background:#450a0a;color:#fca5a5;border:1px solid #991b1b}
124
- .sev-high{background:#431407;color:#fdba74;border:1px solid #9a3412}
125
- .sev-medium{background:#422006;color:#fde047;border:1px solid #854d0e}
126
- .sev-low{background:#082f49;color:#7dd3fc;border:1px solid #075985}
127
- .sev-info{background:#1e293b;color:#cbd5e1;border:1px solid #334155}
128
-
129
- .muted{color:#64748b}
130
- .ok{color:#6ee7b7}
131
- .fail{color:#fca5a5}
132
- .empty-state{text-align:center;color:#64748b;padding:2.5rem 1rem;font-size:0.9rem}
133
- .footer{text-align:center;padding:2.5rem 0 1.5rem;color:#64748b;font-size:0.8rem;border-top:1px solid #1e293b;margin-top:3rem}
134
- @media(max-width:1024px){.container{padding:0 1rem}}
135
- `;
136
- }
137
-
138
- /**
139
- * Wraps body markup in a complete, self-contained HTML document.
140
- *
141
- * `styles` is injected verbatim and must be trusted (it is always generated by
142
- * this module, never derived from scan findings). `body` and `title` are escaped.
143
- */
144
- export function documentShell({ title, styles, body, bodyClass = '' }) {
145
- const cls = bodyClass ? ` class="${esc(bodyClass)}"` : '';
146
- return `<!DOCTYPE html>
147
- <html lang="en">
148
- <head>
149
- <meta charset="utf-8">
150
- <meta name="viewport" content="width=device-width,initial-scale=1">
151
- <title>${esc(title)}</title>
152
- <style>${styles}</style>
153
- </head>
154
- <body${cls}>
155
- ${body}
156
- </body>
157
- </html>`;
158
- }
1
+ /**
2
+ * Shared HTML report theme — single source of truth for every Praxis HTML surface.
3
+ * ============================================================================
4
+ *
5
+ * Extracted because `cli/agents/html-reporter.js` and
6
+ * `cli/commands/team-report.js` each carried their own ~100-line inline stylesheet
7
+ * and their own severity palette. Those palettes had already drifted
8
+ * (`critical` was `#ef4444` in one and `#dc2626` in the other), so every brand or
9
+ * styling fix had to be applied twice — and the two files disagreed on the same
10
+ * concept.
11
+ *
12
+ * Also centralises the primitives that must never be re-implemented per report:
13
+ * - `esc()` — HTML escaping. `team-report.js` previously had none and
14
+ * interpolated finding text straight into markup, which is
15
+ * an injection sink in a file users open in a browser.
16
+ * - `severityBadgeClass()` — maps a severity to a *known* CSS class, so an arbitrary
17
+ * severity string can't be injected into a class attribute.
18
+ * - `countBySeverity()` — one counting implementation, not one per report.
19
+ *
20
+ * Design constraints (per AGENTS.md): no build step, no bundler, no framework. These
21
+ * are plain template strings and functions, so every report stays a single
22
+ * self-contained offline HTML file.
23
+ */
24
+
25
+ /** Severity → accent colour. Used for bars, legends, and inline accents. */
26
+ export const SEVERITY_COLORS = {
27
+ critical: '#ef4444',
28
+ high: '#f97316',
29
+ medium: '#eab308',
30
+ low: '#38bdf8',
31
+ info: '#64748b',
32
+ };
33
+
34
+ /** Security-score letter grade → colour. */
35
+ export const GRADE_COLORS = {
36
+ A: '#22c55e',
37
+ B: '#06b6d4',
38
+ C: '#eab308',
39
+ D: '#f97316',
40
+ F: '#ef4444',
41
+ };
42
+
43
+ /** Every severity we recognise, in report order. */
44
+ export const SEVERITIES = ['critical', 'high', 'medium', 'low'];
45
+
46
+ /**
47
+ * Severities rendered in distribution bars/legends. Includes `info` so the segments
48
+ * always sum to 100% of the findings shown — omitting it left the bar visibly
49
+ * under-filled whenever a scan produced informational findings.
50
+ */
51
+ export const DISPLAY_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
52
+
53
+ /**
54
+ * Escapes text for interpolation into HTML markup or a quoted attribute.
55
+ * Null/undefined become an empty string so callers can interpolate directly.
56
+ */
57
+ export function esc(str) {
58
+ if (str === null || str === undefined) return '';
59
+ return String(str)
60
+ .replace(/&/g, '&amp;')
61
+ .replace(/</g, '&lt;')
62
+ .replace(/>/g, '&gt;')
63
+ .replace(/"/g, '&quot;')
64
+ .replace(/'/g, '&#039;');
65
+ }
66
+
67
+ /**
68
+ * Maps a severity to a known-safe CSS class suffix.
69
+ *
70
+ * Returns `''` for anything unrecognised so the result is always a safe subset of
71
+ * the class attribute — a finding whose `severity` came from a parsed report file
72
+ * must never be able to break out of `class="sev-badge …"`.
73
+ */
74
+ export function severityBadgeClass(severity) {
75
+ const key = String(severity || '').toLowerCase();
76
+ return SEVERITIES.includes(key) || key === 'info' ? key : '';
77
+ }
78
+
79
+ /** Renders a severity badge span. Escapes the label and sanitises the class. */
80
+ export function severityBadge(severity, label) {
81
+ const cls = severityBadgeClass(severity);
82
+ const text = label ?? String(severity || 'unknown');
83
+ const classes = cls ? `sev-badge sev-${cls}` : 'sev-badge';
84
+ return `<span class="${classes}">${esc(text)}</span>`;
85
+ }
86
+
87
+ /** Counts findings per severity, always returning a fully-populated record. */
88
+ export function countBySeverity(findings = []) {
89
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
90
+ for (const f of Array.isArray(findings) ? findings : []) {
91
+ const key = String(f?.severity || '').toLowerCase();
92
+ if (key in counts) counts[key]++;
93
+ }
94
+ return counts;
95
+ }
96
+
97
+ /**
98
+ * The shared base stylesheet: reset, typography, layout container, tables, code,
99
+ * severity badges, and the muted/empty/footer primitives. Every report includes
100
+ * this, then appends its own component styles.
101
+ */
102
+ export function baseStyles() {
103
+ return `
104
+ *{margin:0;padding:0;box-sizing:border-box}
105
+ html{scroll-behavior:smooth}
106
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:#090d16;color:#cbd5e1;line-height:1.55;font-size:14px}
107
+ a{color:#38bdf8;text-decoration:none}
108
+ a:hover{text-decoration:underline}
109
+
110
+ .container{max-width:1440px;margin:1.8rem auto;padding:0 2rem}
111
+ h1{font-size:1.8rem;font-weight:700;color:#f8fafc;margin-bottom:0.25rem}
112
+ h2{font-size:1.1rem;font-weight:600;margin:2rem 0 1rem;color:#94a3b8;border-bottom:1px solid #1e293b;padding-bottom:0.5rem;text-transform:uppercase;letter-spacing:0.05em}
113
+
114
+ .table-responsive{overflow-x:auto}
115
+ table{width:100%;border-collapse:collapse;font-size:0.86rem;text-align:left}
116
+ th{background:#131d33;color:#94a3b8;padding:0.75rem 1rem;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.6px;border-bottom:1px solid #1e293b}
117
+ td{padding:0.75rem 1rem;border-bottom:1px solid #172239;vertical-align:top}
118
+ tr:hover td{background:#111b30}
119
+ code{background:#050811;border:1px solid #1e293b;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.8rem;word-break:break-word}
120
+ small{color:#64748b}
121
+
122
+ .sev-badge{display:inline-block;padding:3px 9px;border-radius:6px;font-size:0.72rem;font-weight:800;text-transform:uppercase;letter-spacing:0.5px}
123
+ .sev-critical{background:#450a0a;color:#fca5a5;border:1px solid #991b1b}
124
+ .sev-high{background:#431407;color:#fdba74;border:1px solid #9a3412}
125
+ .sev-medium{background:#422006;color:#fde047;border:1px solid #854d0e}
126
+ .sev-low{background:#082f49;color:#7dd3fc;border:1px solid #075985}
127
+ .sev-info{background:#1e293b;color:#cbd5e1;border:1px solid #334155}
128
+
129
+ .muted{color:#64748b}
130
+ .ok{color:#6ee7b7}
131
+ .fail{color:#fca5a5}
132
+ .empty-state{text-align:center;color:#64748b;padding:2.5rem 1rem;font-size:0.9rem}
133
+ .footer{text-align:center;padding:2.5rem 0 1.5rem;color:#64748b;font-size:0.8rem;border-top:1px solid #1e293b;margin-top:3rem}
134
+ @media(max-width:1024px){.container{padding:0 1rem}}
135
+ `;
136
+ }
137
+
138
+ /**
139
+ * Wraps body markup in a complete, self-contained HTML document.
140
+ *
141
+ * `styles` is injected verbatim and must be trusted (it is always generated by
142
+ * this module, never derived from scan findings). `body` and `title` are escaped.
143
+ */
144
+ export function documentShell({ title, styles, body, bodyClass = '' }) {
145
+ const cls = bodyClass ? ` class="${esc(bodyClass)}"` : '';
146
+ return `<!DOCTYPE html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="utf-8">
150
+ <meta name="viewport" content="width=device-width,initial-scale=1">
151
+ <title>${esc(title)}</title>
152
+ <style>${styles}</style>
153
+ </head>
154
+ <body${cls}>
155
+ ${body}
156
+ </body>
157
+ </html>`;
158
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SARIF v2.1.0 output formatter.
3
3
  *
4
- * The single SARIF serializer for Praxis (P-IMP-062). Every emitting path must go
4
+ * The single SARIF serializer for Praxis. Every emitting path must go
5
5
  * through here: `scan --sarif`, `scan ci --sarif`, `audit --sarif`, `redteam --sarif`,
6
6
  * `scan standard --sarif` and `--format sarif`. Four commands used to carry private
7
7
  * copies of this logic, which meant fixes applied here reached almost nobody — most
@@ -84,7 +84,7 @@ function renderSARIFDocument(report, options = {}) {
84
84
  toolName = 'praxis',
85
85
  // Defaults to the real package version. This used to fall back to a hardcoded
86
86
  // '1.0.0', so any caller that forgot to pass `toolVersion` reported a confidently
87
- // wrong driver version in Code Scanning (P-IMP-065).
87
+ // wrong driver version in Code Scanning.
88
88
  toolVersion = report.version || toolVersion_(),
89
89
  informationUri = 'https://github.com/Ganron007/Praxis',
90
90
  rootPath = null,
@@ -36,7 +36,7 @@ export class JobQueue extends EventEmitter {
36
36
  * @returns {{ok:true, job:object} | {ok:false, error:string}}
37
37
  */
38
38
  enqueue(project) {
39
- const queued = this.jobs.length + this.pending.length;
39
+ const queued = this.running + this.pending.length;
40
40
  if (queued >= this.maxQueue) {
41
41
  return { ok: false, error: `queue is full (max ${this.maxQueue} scans)` };
42
42
  }
@@ -203,4 +203,4 @@ export async function runScanWithOrchestrator(rootPath, onProgress) {
203
203
  standardsSummary,
204
204
  scannedAt: new Date().toISOString(),
205
205
  };
206
- }
206
+ }
@@ -18,9 +18,11 @@
18
18
 
19
19
  import http from 'http';
20
20
  import { URL } from 'url';
21
+ import { isIP } from 'net';
21
22
  import { listProjects, addProject, getProject, removeProject } from './projects.js';
22
23
  import { JobQueue, runScanWithOrchestrator } from './jobs.js';
23
24
  import { renderFrontend, AGENT_ROSTER } from './ui.js';
25
+ import jsonReport from '../output/json.js';
24
26
 
25
27
  const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); // praxis-ignore SSRF_INTERNAL_IP — loopback allow-list: binding only to loopback IS the mitigation
26
28
  const CLIENT_HEADER = 'x-praxis-client';
@@ -28,7 +30,8 @@ const CLIENT_VALUE = 'praxis-web';
28
30
  const MAX_BODY_BYTES = 64 * 1024;
29
31
 
30
32
  export function isLoopback(host) {
31
- return LOOPBACK.has(host) || String(host).startsWith('127.');
33
+ const normalized = String(host).replace(/^\[|\]$/g, '');
34
+ return LOOPBACK.has(normalized) || (isIP(normalized) === 4 && normalized.startsWith('127.'));
32
35
  }
33
36
 
34
37
  /** Decides the effective bind, refusing an unsafe remote bind (threat T4). */
@@ -136,14 +139,14 @@ function guardMutation(req) {
136
139
  }
137
140
  const origin = req.headers.origin;
138
141
  if (origin) {
139
- let host;
142
+ let originURL;
140
143
  try {
141
- host = new URL(origin).hostname;
144
+ originURL = new URL(origin);
142
145
  } catch {
143
146
  return { ok: false, error: 'malformed Origin' };
144
147
  }
145
- // Same-origin or loopback origin only: blocks DNS rebinding.
146
- if (!isLoopback(host) && host !== 'localhost') {
148
+ // A different loopback port is still a different browser origin.
149
+ if (!['http:', 'https:'].includes(originURL.protocol) || originURL.host !== req.headers.host) {
147
150
  return { ok: false, error: 'cross-origin request refused' };
148
151
  }
149
152
  }
@@ -211,6 +214,13 @@ export function createServer({ host: _host = '127.0.0.1', port: _port = 7317, to
211
214
  const route = url.pathname.replace(/\/+$/, '') || '/';
212
215
  const method = req.method || 'GET';
213
216
 
217
+ // DNS rebinding can read reports through GET as well as mutate through POST.
218
+ // Without token auth, every request must address the actual local server.
219
+ if (!token && ((!isLoopback(url.hostname) && url.hostname !== 'localhost') ||
220
+ Number(url.port || 80) !== server.address()?.port)) {
221
+ return sendJSON(res, 403, { error: 'untrusted Host header' });
222
+ }
223
+
214
224
  // Auth for remote binds.
215
225
  if (token) {
216
226
  const auth = req.headers.authorization || '';
@@ -266,7 +276,7 @@ export function createServer({ host: _host = '127.0.0.1', port: _port = 7317, to
266
276
  if (!job) return sendJSON(res, 404, { error: 'no completed scan for this project' });
267
277
  const full = jobs.get(job.id);
268
278
  if (!full?.result) return sendJSON(res, 404, { error: 'scan has no result' });
269
- return sendJSON(res, 200, { job: jobs._public(full), result: full.result });
279
+ return sendJSON(res, 200, { job: jobs._public(full), result: JSON.parse(jsonReport(full.result)) });
270
280
  }
271
281
  }
272
282
 
@@ -279,7 +289,7 @@ export function createServer({ host: _host = '127.0.0.1', port: _port = 7317, to
279
289
  const full = jobs.get(jobReportMatch[1]);
280
290
  if (!full) return sendJSON(res, 404, { error: 'unknown job' });
281
291
  if (!full.result) return sendJSON(res, 404, { error: 'scan has no result' });
282
- return sendJSON(res, 200, { job: jobs._public(full), result: full.result });
292
+ return sendJSON(res, 200, { job: jobs._public(full), result: JSON.parse(jsonReport(full.result)) });
283
293
  }
284
294
 
285
295
  const jobMatch = route.match(/^\/api\/jobs\/([\w-]+)$/);
@@ -313,6 +323,7 @@ export function createServer({ host: _host = '127.0.0.1', port: _port = 7317, to
313
323
  });
314
324
  const send = j => res.write(`data: ${JSON.stringify(j)}\n\n`);
315
325
  send(jobs._public(job));
326
+ if (['done', 'failed', 'cancelled'].includes(job.status)) return res.end();
316
327
  const onUpdate = j => {
317
328
  if (j.id !== job.id) return;
318
329
  send(j);
@@ -350,4 +361,4 @@ export function createServer({ host: _host = '127.0.0.1', port: _port = 7317, to
350
361
  });
351
362
 
352
363
  return { server, jobs };
353
- }
364
+ }
@@ -1,41 +1,41 @@
1
- {
2
- "version": "1.1",
3
- "_refresh_policy": {
4
- "cadence": "Incident-driven + weekly review",
5
- "process": "1. Track OWASP LLM/Agentic releases and reported AI attack incidents. 2. Add signatures in own phrasing (never copy AGPL/copyrighted text verbatim). 3. Bump version. 4. Run npm test (prober tests enforce compilation + ReDoS guard).",
6
- "owner": "Praxis contributors",
7
- "lastReviewed": "2026-10-02"
8
- },
9
- "probes": [
10
- {
11
- "id": "TP-001",
12
- "category": "obfuscated-injection",
13
- "title": "Encoded-payload decode instruction (base64/hex/ROT)",
14
- "regex": "(?i)(?:decode|base64|rot1[0-9]|deobfuscate)[\\s\\S]{0,80}(?:then\\s+(?:respond|answer|execute|run)|before\\s+you\\s+(?:respond|answer))",
15
- "severity": "high",
16
- "description": "Source contains a decode-then-execute instruction that tells a model to decode an encoded payload before acting — the obfuscated-injection family used to smuggle instructions past content filters.",
17
- "fix": "Reject decode-first instructions from untrusted content. Never execute content decoded from user-controlled strings."
18
- },
19
- {
20
- "id": "TP-002",
21
- "category": "jailbreak-frame",
22
- "title": "System-prompt redefinition instruction",
23
- "regex": "(?i)(?:redefine|replace|rewrite)\\s+(?:your|the)\\s+(?:system|base)\\s+(?:prompt|instructions|rules)[\\s\\S]{0,120}(?:with|to)\\s+",
24
- "severity": "high",
25
- "description": "Source contains an instruction that asks the model to replace its own system prompt — a jailbreak frame that erases the developer's guardrails.",
26
- "fix": "Strip prompt-redefinition frames. Treat any instruction that modifies the system prompt as untrusted input."
27
- },
28
- {
29
- "id": "TP-003",
30
- "category": "indirect-injection",
31
- "title": "Document-payload split injection (multi-file chaining)",
32
- "regex": "(?i)(?:part\\s*\\d+\\s*of\\s*\\d+|continue\\s+(?:reading|with|in)\\s+(?:the\\s+)?(?:next|following|part|message)|when\\s+combined\\s+with\\s+(?:the\\s+)?(?:above|previous|preceding|earlier|these|prior|other)\\s+(?:instruction|instructions|rule|rules|prompt|prompts|text|content|contents|message|messages|directive|directives|command|commands|guideline|guidelines)\\b)",
33
- "severity": "medium",
34
- "description": "Source contains split-payload markers that chain instructions across documents/messages — the payload-splitting technique that defeats per-input content filters.",
35
- "fix": "Evaluate documents independently; do not concatenate instruction-like text across untrusted sources.",
36
- "_tightened": "P-IMP-057: the bare phrase \"when combined with\" matched ordinary English (e.g. the comment \"when combined with machine output\") and produced a medium-severity finding. It now requires a chaining referent (above/previous/preceding/earlier/these/prior/other) AND an instruction-like noun, so benign prose like \"merged when combined with the previous version\" no longer matches."
37
- }
38
- ],
39
- "eaa": [],
40
- "gateways": []
41
- }
1
+ {
2
+ "version": "1.1",
3
+ "_refresh_policy": {
4
+ "cadence": "Incident-driven + weekly review",
5
+ "process": "1. Track OWASP LLM/Agentic releases and reported AI attack incidents. 2. Add signatures in own phrasing (never copy AGPL/copyrighted text verbatim). 3. Bump version. 4. Run npm test (prober tests enforce compilation + ReDoS guard).",
6
+ "owner": "Praxis contributors",
7
+ "lastReviewed": "2026-10-02"
8
+ },
9
+ "probes": [
10
+ {
11
+ "id": "TP-001",
12
+ "category": "obfuscated-injection",
13
+ "title": "Encoded-payload decode instruction (base64/hex/ROT)",
14
+ "regex": "(?i)(?:decode|base64|rot1[0-9]|deobfuscate)[\\s\\S]{0,80}(?:then\\s+(?:respond|answer|execute|run)|before\\s+you\\s+(?:respond|answer))",
15
+ "severity": "high",
16
+ "description": "Source contains a decode-then-execute instruction that tells a model to decode an encoded payload before acting — the obfuscated-injection family used to smuggle instructions past content filters.",
17
+ "fix": "Reject decode-first instructions from untrusted content. Never execute content decoded from user-controlled strings."
18
+ },
19
+ {
20
+ "id": "TP-002",
21
+ "category": "jailbreak-frame",
22
+ "title": "System-prompt redefinition instruction",
23
+ "regex": "(?i)(?:redefine|replace|rewrite)\\s+(?:your|the)\\s+(?:system|base)\\s+(?:prompt|instructions|rules)[\\s\\S]{0,120}(?:with|to)\\s+",
24
+ "severity": "high",
25
+ "description": "Source contains an instruction that asks the model to replace its own system prompt — a jailbreak frame that erases the developer's guardrails.",
26
+ "fix": "Strip prompt-redefinition frames. Treat any instruction that modifies the system prompt as untrusted input."
27
+ },
28
+ {
29
+ "id": "TP-003",
30
+ "category": "indirect-injection",
31
+ "title": "Document-payload split injection (multi-file chaining)",
32
+ "regex": "(?i)(?:part\\s*\\d+\\s*of\\s*\\d+|continue\\s+(?:reading|with|in)\\s+(?:the\\s+)?(?:next|following|part|message)|when\\s+combined\\s+with\\s+(?:the\\s+)?(?:above|previous|preceding|earlier|these|prior|other)\\s+(?:instruction|instructions|rule|rules|prompt|prompts|text|content|contents|message|messages|directive|directives|command|commands|guideline|guidelines)\\b)",
33
+ "severity": "medium",
34
+ "description": "Source contains split-payload markers that chain instructions across documents/messages — the payload-splitting technique that defeats per-input content filters.",
35
+ "fix": "Evaluate documents independently; do not concatenate instruction-like text across untrusted sources.",
36
+ "_tightened": "The bare phrase \"when combined with\" matched ordinary English (e.g. the comment \"when combined with machine output\") and produced a medium-severity finding. It now requires a chaining referent (above/previous/preceding/earlier/these/prior/other) AND an instruction-like noun, so benign prose like \"merged when combined with the previous version\" no longer matches."
37
+ }
38
+ ],
39
+ "eaa": [],
40
+ "gateways": []
41
+ }