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
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared HTML report theme — single source of truth for every Praxis HTML surface.
|
|
3
|
-
* ============================================================================
|
|
4
|
-
*
|
|
5
|
-
* Extracted
|
|
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, '<')
|
|
62
|
-
.replace(/>/g, '>')
|
|
63
|
-
.replace(/"/g, '"')
|
|
64
|
-
.replace(/'/g, ''');
|
|
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, '&')
|
|
61
|
+
.replace(/</g, '<')
|
|
62
|
+
.replace(/>/g, '>')
|
|
63
|
+
.replace(/"/g, '"')
|
|
64
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|
package/cli/core/output/sarif.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SARIF v2.1.0 output formatter.
|
|
3
3
|
*
|
|
4
|
-
* The single SARIF serializer for Praxis
|
|
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
|
|
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,
|
package/cli/core/web/jobs.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|
package/cli/core/web/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
142
|
+
let originURL;
|
|
140
143
|
try {
|
|
141
|
-
|
|
144
|
+
originURL = new URL(origin);
|
|
142
145
|
} catch {
|
|
143
146
|
return { ok: false, error: 'malformed Origin' };
|
|
144
147
|
}
|
|
145
|
-
//
|
|
146
|
-
if (!
|
|
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": "
|
|
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
|
+
}
|