praxis-sec 1.0.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/ai-defense/system-prompt-armor.md +327 -0
- package/checklists/launch-day.md +168 -0
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agent-config-scanner.js +787 -0
- package/cli/agents/agent-telemetry-agent.js +415 -0
- package/cli/agents/agentic-security-agent.js +296 -0
- package/cli/agents/agentic-supply-chain-agent.js +463 -0
- package/cli/agents/ai-infra-inventory-agent.js +449 -0
- package/cli/agents/api-fuzzer.js +345 -0
- package/cli/agents/auth-bypass-agent.js +348 -0
- package/cli/agents/base-agent.js +280 -0
- package/cli/agents/cicd-scanner.js +300 -0
- package/cli/agents/config-auditor.js +757 -0
- package/cli/agents/deep-analyzer.js +776 -0
- package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/git-history-scanner.js +169 -0
- package/cli/agents/governance-audits.js +138 -0
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/html-reporter.js +1125 -0
- package/cli/agents/index.js +147 -0
- package/cli/agents/injection-tester.js +502 -0
- package/cli/agents/legal-risk-agent.js +328 -0
- package/cli/agents/llm-redteam.js +199 -0
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/mcp-security-agent.js +588 -0
- package/cli/agents/memory-poisoning-agent.js +305 -0
- package/cli/agents/mobile-scanner.js +231 -0
- package/cli/agents/model-file-scanner.js +259 -0
- package/cli/agents/orchestrator.js +355 -0
- package/cli/agents/pii-compliance-agent.js +301 -0
- package/cli/agents/policy-engine.js +229 -0
- package/cli/agents/prompt-injection-prober.js +224 -0
- package/cli/agents/rag-security-agent.js +204 -0
- package/cli/agents/recon-agent.js +207 -0
- package/cli/agents/sbom-generator.js +265 -0
- package/cli/agents/scoring-engine.js +273 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/stateful-watcher.js +238 -0
- package/cli/agents/supabase-rls-agent.js +154 -0
- package/cli/agents/supply-chain-agent.js +857 -0
- package/cli/agents/swarm-orchestrator.js +200 -0
- package/cli/agents/verifier-agent.js +303 -0
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/praxis.js +866 -0
- package/cli/commands/abom.js +73 -0
- package/cli/commands/agent-fix.js +1245 -0
- package/cli/commands/audit.js +1180 -0
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/baseline.js +193 -0
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/checklist.js +223 -0
- package/cli/commands/ci.js +403 -0
- package/cli/commands/deps.js +516 -0
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +195 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/fix.js +218 -0
- package/cli/commands/guard.js +396 -0
- package/cli/commands/hooks.js +278 -0
- package/cli/commands/init.js +514 -0
- package/cli/commands/legal.js +158 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/mcp.js +660 -0
- package/cli/commands/openclaw.js +386 -0
- package/cli/commands/red-team.js +350 -0
- package/cli/commands/redteam.js +78 -0
- package/cli/commands/remediate.js +797 -0
- package/cli/commands/rotate.js +768 -0
- package/cli/commands/rules.js +196 -0
- package/cli/commands/scan-mcp.js +534 -0
- package/cli/commands/scan-skill.js +588 -0
- package/cli/commands/scan-standard.js +251 -0
- package/cli/commands/scan.js +524 -0
- package/cli/commands/score.js +449 -0
- package/cli/commands/shell.js +514 -0
- package/cli/commands/team-report.js +398 -0
- package/cli/commands/undo.js +161 -0
- package/cli/commands/update-intel.js +126 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +757 -0
- package/cli/commands/web.js +63 -0
- package/cli/core/ast/guardrail-detector.js +141 -0
- package/cli/core/ast/index.js +22 -0
- package/cli/core/ast/parser.js +676 -0
- package/cli/core/ast/scope-tree.js +287 -0
- package/cli/core/ast/taint-tracker.js +158 -0
- package/cli/core/branding.js +37 -0
- package/cli/core/env.js +38 -0
- package/cli/core/errors.js +61 -0
- package/cli/core/fs.js +62 -0
- package/cli/core/output/compliance.js +90 -0
- package/cli/core/output/html-theme.js +158 -0
- package/cli/core/output/index.js +57 -0
- package/cli/core/output/json.js +48 -0
- package/cli/core/output/sarif.js +240 -0
- package/cli/core/version.js +67 -0
- package/cli/core/web/jobs.js +183 -0
- package/cli/core/web/projects.js +146 -0
- package/cli/core/web/server.js +439 -0
- package/cli/data/atlas-knowledge.json +5640 -0
- package/cli/data/eaa-catalog.json +39 -0
- package/cli/data/known-mcps.json +26 -0
- package/cli/data/probes/prompt-injection-corpus.json +271 -0
- package/cli/data/threat-intel.json +85 -0
- package/cli/data/threatpacks/latest.json +41 -0
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +90 -0
- package/cli/providers/llm-provider.js +766 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/cache-manager.js +310 -0
- package/cli/utils/compliance-map.js +191 -0
- package/cli/utils/entropy.js +132 -0
- package/cli/utils/fix-ledger.js +127 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/intel/cache.js +61 -0
- package/cli/utils/intel/http.js +88 -0
- package/cli/utils/intel/index.js +235 -0
- package/cli/utils/intel/merge.js +229 -0
- package/cli/utils/intel/sources/epss.js +54 -0
- package/cli/utils/intel/sources/ghsa.js +81 -0
- package/cli/utils/intel/sources/gitguardian.js +40 -0
- package/cli/utils/intel/sources/gitleaks.js +101 -0
- package/cli/utils/intel/sources/kev.js +38 -0
- package/cli/utils/intel/sources/nvd.js +84 -0
- package/cli/utils/intel/sources/osv.js +132 -0
- package/cli/utils/intel/sources/phylum.js +44 -0
- package/cli/utils/intel/sources/snyk.js +46 -0
- package/cli/utils/intel/sources/socket.js +69 -0
- package/cli/utils/intel/sources/sonatype.js +84 -0
- package/cli/utils/intel/sources/threatpack.js +69 -0
- package/cli/utils/mcp-trust.js +60 -0
- package/cli/utils/output.js +251 -0
- package/cli/utils/patterns.js +1130 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/cli/utils/plugin-loader.js +364 -0
- package/cli/utils/rule-import.js +228 -0
- package/cli/utils/rule-registry.js +426 -0
- package/cli/utils/scan-fingerprint.js +109 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/score-history.js +119 -0
- package/cli/utils/secrets-verifier.js +247 -0
- package/cli/utils/security-memory.js +296 -0
- package/cli/utils/standards/atlas-knowledge.js +87 -0
- package/cli/utils/standards/index.js +127 -0
- package/cli/utils/standards/sources/avid.js +45 -0
- package/cli/utils/standards/sources/eu-ai-act.js +89 -0
- package/cli/utils/standards/sources/google-saif.js +39 -0
- package/cli/utils/standards/sources/iso-42001.js +94 -0
- package/cli/utils/standards/sources/mitre-atlas.js +54 -0
- package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
- package/cli/utils/standards/sources/owasp-llm.js +45 -0
- package/cli/utils/standards/sources/owasp-ml.js +45 -0
- package/cli/utils/threat-intel.js +265 -0
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/gitignore-template +258 -0
- package/configs/nextjs-security-headers.js +220 -0
- package/configs/praxisignore-template +50 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/docs/THIRD_PARTY_NOTICES.md +26 -0
- package/docs/THREAT_INTEL.md +292 -0
- package/docs/USAGE.md +1205 -0
- package/docs/design/WEB-UI.md +82 -0
- package/package.json +71 -0
- package/scripts/check-determinism.mjs +119 -0
- package/snippets/README.md +122 -0
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis Scope Tree & Symbol Resolver
|
|
3
|
+
* ===================================
|
|
4
|
+
*
|
|
5
|
+
* Constructs lexical scopes and resolves symbols across JavaScript,
|
|
6
|
+
* TypeScript, and Python codebases.
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* 1. Scope hierarchy: Global, Function, Class, Block, TryCatch.
|
|
10
|
+
* 2. Symbol resolution: declarations, parameter origin, constant folding,
|
|
11
|
+
* and variable mutation tracking.
|
|
12
|
+
* 3. Enclosing scope queries: `getScopeAt(line)` for accurate block boundaries.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { NodeType } from './parser.js';
|
|
16
|
+
|
|
17
|
+
export const ScopeType = {
|
|
18
|
+
GLOBAL: 'GLOBAL',
|
|
19
|
+
FUNCTION: 'FUNCTION',
|
|
20
|
+
CLASS: 'CLASS',
|
|
21
|
+
BLOCK: 'BLOCK',
|
|
22
|
+
TRY_CATCH: 'TRY_CATCH',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class Scope {
|
|
26
|
+
constructor(type, range, parent = null, name = '') {
|
|
27
|
+
this.type = type;
|
|
28
|
+
this.name = name;
|
|
29
|
+
this.range = range || { startLine: 1, endLine: 100000 };
|
|
30
|
+
this.parent = parent;
|
|
31
|
+
this.children = [];
|
|
32
|
+
this.variables = new Map(); // name -> { kind, init, isStatic, line, isParam, sanitized }
|
|
33
|
+
this.imports = new Map(); // name -> { source, importedName, line }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
containsLine(line) {
|
|
37
|
+
return line >= this.range.startLine && line <= this.range.endLine;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
addVariable(name, info) {
|
|
41
|
+
this.variables.set(name, {
|
|
42
|
+
name,
|
|
43
|
+
kind: info.kind || 'var',
|
|
44
|
+
init: info.init || null,
|
|
45
|
+
isStatic: info.isStatic ?? false,
|
|
46
|
+
line: info.line || 1,
|
|
47
|
+
isParam: info.isParam ?? false,
|
|
48
|
+
sanitized: info.sanitized ?? false,
|
|
49
|
+
assignments: info.assignments || [],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addImport(localName, info) {
|
|
54
|
+
this.imports.set(localName, info);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lookupVariable(name) {
|
|
58
|
+
if (this.variables.has(name)) {
|
|
59
|
+
return this.variables.get(name);
|
|
60
|
+
}
|
|
61
|
+
if (this.parent) {
|
|
62
|
+
return this.parent.lookupVariable(name);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lookupImport(name) {
|
|
68
|
+
if (this.imports.has(name)) {
|
|
69
|
+
return this.imports.get(name);
|
|
70
|
+
}
|
|
71
|
+
if (this.parent) {
|
|
72
|
+
return this.parent.lookupImport(name);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ScopeTree {
|
|
79
|
+
constructor(ast, code = '') {
|
|
80
|
+
this.ast = ast;
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.globalScope = new Scope(ScopeType.GLOBAL, ast?.range || { startLine: 1, endLine: 10000 });
|
|
83
|
+
this._buildTree();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static build(ast, code = '') {
|
|
87
|
+
return new ScopeTree(ast, code);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_buildTree() {
|
|
91
|
+
if (!this.ast || !this.ast.body) return;
|
|
92
|
+
|
|
93
|
+
for (const node of this.ast.body) {
|
|
94
|
+
this._processNode(node, this.globalScope);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_processNode(node, currentScope) {
|
|
99
|
+
if (!node) return;
|
|
100
|
+
|
|
101
|
+
switch (node.type) {
|
|
102
|
+
case NodeType.FUNCTION_DECLARATION: {
|
|
103
|
+
const fnScope = new Scope(
|
|
104
|
+
ScopeType.FUNCTION,
|
|
105
|
+
node.range,
|
|
106
|
+
currentScope,
|
|
107
|
+
node.name
|
|
108
|
+
);
|
|
109
|
+
currentScope.children.push(fnScope);
|
|
110
|
+
currentScope.addVariable(node.name, {
|
|
111
|
+
kind: 'function',
|
|
112
|
+
init: node.name,
|
|
113
|
+
isStatic: true,
|
|
114
|
+
line: node.line,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Register parameters
|
|
118
|
+
if (Array.isArray(node.params)) {
|
|
119
|
+
for (const param of node.params) {
|
|
120
|
+
fnScope.addVariable(param, {
|
|
121
|
+
kind: 'param',
|
|
122
|
+
init: null,
|
|
123
|
+
isStatic: false,
|
|
124
|
+
isParam: true,
|
|
125
|
+
line: node.line,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case NodeType.CLASS_DECLARATION: {
|
|
133
|
+
const classScope = new Scope(
|
|
134
|
+
ScopeType.CLASS,
|
|
135
|
+
node.range,
|
|
136
|
+
currentScope,
|
|
137
|
+
node.name
|
|
138
|
+
);
|
|
139
|
+
currentScope.children.push(classScope);
|
|
140
|
+
currentScope.addVariable(node.name, {
|
|
141
|
+
kind: 'class',
|
|
142
|
+
init: node.name,
|
|
143
|
+
isStatic: true,
|
|
144
|
+
line: node.line,
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case NodeType.VARIABLE_DECLARATION: {
|
|
150
|
+
if (Array.isArray(node.declarations)) {
|
|
151
|
+
for (const decl of node.declarations) {
|
|
152
|
+
currentScope.addVariable(decl.name, {
|
|
153
|
+
kind: node.kind,
|
|
154
|
+
init: decl.init,
|
|
155
|
+
isStatic: decl.isStatic,
|
|
156
|
+
line: decl.line,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case NodeType.ASSIGNMENT: {
|
|
164
|
+
const existing = currentScope.lookupVariable(node.target);
|
|
165
|
+
if (existing) {
|
|
166
|
+
existing.assignments = existing.assignments || [];
|
|
167
|
+
existing.assignments.push({
|
|
168
|
+
value: node.value,
|
|
169
|
+
isStatic: node.isStatic,
|
|
170
|
+
line: node.line,
|
|
171
|
+
});
|
|
172
|
+
if (!node.isStatic) {
|
|
173
|
+
existing.isStatic = false;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
currentScope.addVariable(node.target, {
|
|
177
|
+
kind: 'assignment',
|
|
178
|
+
init: node.value,
|
|
179
|
+
isStatic: node.isStatic,
|
|
180
|
+
line: node.line,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case NodeType.IMPORT_DECLARATION: {
|
|
187
|
+
if (Array.isArray(node.importedNames)) {
|
|
188
|
+
for (const name of node.importedNames) {
|
|
189
|
+
currentScope.addImport(name, {
|
|
190
|
+
source: node.source,
|
|
191
|
+
importedName: name,
|
|
192
|
+
line: node.line,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case NodeType.TRY_STATEMENT: {
|
|
200
|
+
const tryScope = new Scope(
|
|
201
|
+
ScopeType.TRY_CATCH,
|
|
202
|
+
node.range,
|
|
203
|
+
currentScope,
|
|
204
|
+
'try'
|
|
205
|
+
);
|
|
206
|
+
currentScope.children.push(tryScope);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
default:
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Find the deepest enclosing scope for a given line number.
|
|
217
|
+
*
|
|
218
|
+
* @param {number} line — 1-indexed line number
|
|
219
|
+
* @returns {Scope} — Deepest matching scope (defaults to globalScope)
|
|
220
|
+
*/
|
|
221
|
+
getScopeAt(line) {
|
|
222
|
+
let current = this.globalScope;
|
|
223
|
+
|
|
224
|
+
const findDeepest = (scope) => {
|
|
225
|
+
for (const child of scope.children) {
|
|
226
|
+
if (child.containsLine(line)) {
|
|
227
|
+
current = child;
|
|
228
|
+
findDeepest(child);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
findDeepest(this.globalScope);
|
|
235
|
+
return current;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a line is within a function scope.
|
|
240
|
+
*/
|
|
241
|
+
getEnclosingFunction(line) {
|
|
242
|
+
let scope = this.getScopeAt(line);
|
|
243
|
+
while (scope) {
|
|
244
|
+
if (scope.type === ScopeType.FUNCTION) {
|
|
245
|
+
return scope;
|
|
246
|
+
}
|
|
247
|
+
scope = scope.parent;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if a line is inside a try/catch block.
|
|
254
|
+
*/
|
|
255
|
+
isInTryCatch(line) {
|
|
256
|
+
let scope = this.getScopeAt(line);
|
|
257
|
+
while (scope) {
|
|
258
|
+
if (scope.type === ScopeType.TRY_CATCH) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
scope = scope.parent;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if a variable at a given line is static (never received user input).
|
|
268
|
+
*/
|
|
269
|
+
isVariableStatic(name, line) {
|
|
270
|
+
const scope = this.getScopeAt(line);
|
|
271
|
+
const variable = scope.lookupVariable(name);
|
|
272
|
+
if (!variable) return null;
|
|
273
|
+
return variable.isStatic === true && !variable.isParam;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if a variable originated from function parameters or known input.
|
|
278
|
+
*/
|
|
279
|
+
isVariableUserInput(name, line) {
|
|
280
|
+
const scope = this.getScopeAt(line);
|
|
281
|
+
const variable = scope.lookupVariable(name);
|
|
282
|
+
if (!variable) return false;
|
|
283
|
+
return variable.isParam === true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default ScopeTree;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis Taint Tracker & Dataflow Engine
|
|
3
|
+
* =====================================
|
|
4
|
+
*
|
|
5
|
+
* Performs intra-file source-to-sink dataflow analysis to determine whether
|
|
6
|
+
* dangerous sinks (SQL, Command execution, RAG ingestion, Prompt injection)
|
|
7
|
+
* receive tainted untrusted input or safe/sanitized values.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ASTParser } from './parser.js';
|
|
11
|
+
import { ScopeTree } from './scope-tree.js';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// PATTERNS & REGEXES
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export const UNTRUSTED_SOURCES = [
|
|
18
|
+
/\breq\.(?:body|query|params|headers|file|files|cookies)\b/,
|
|
19
|
+
/\brequest\.(?:body|query|params|args|form|json|data|GET|POST|headers|files)\b/,
|
|
20
|
+
/\bctx\.(?:request|query|params|body)\b/,
|
|
21
|
+
/\bevent\.(?:body|queryStringParameters|headers)\b/,
|
|
22
|
+
/\bsearchParams\.get\s*\(/,
|
|
23
|
+
/\bformData\.(?:get|getAll)\s*\(/,
|
|
24
|
+
/\bprocess\.argv\b/,
|
|
25
|
+
/\bsys\.argv\b/,
|
|
26
|
+
/\binput\s*\(/,
|
|
27
|
+
/\b(?:userInput|user_input|userPayload|user_payload|userQuery|user_query|userPrompt|user_prompt|userMessage|user_message|rawInput|raw_input)\b/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const SANITIZERS = [
|
|
31
|
+
{ name: 'parseInt/parseFloat/Number', regex: /\b(?:parseInt|parseFloat|Number|int|float)\s*\(/ },
|
|
32
|
+
{ name: 'DOMPurify', regex: /\bDOMPurify\.sanitize\s*\(/ },
|
|
33
|
+
{ name: 'sanitizeHtml', regex: /\bsanitize(?:Html)?\s*\(/i },
|
|
34
|
+
{ name: 'validator.escape', regex: /\bvalidator\.escape\s*\(/ },
|
|
35
|
+
{ name: 'encodeURIComponent', regex: /\bencodeURIComponent\s*\(/ },
|
|
36
|
+
{ name: 'shlex.quote', regex: /\bshlex\.quote\s*\(/ },
|
|
37
|
+
{ name: 'html.escape', regex: /\bhtml\.escape\s*\(/ },
|
|
38
|
+
{ name: 'schema.safeParse/Zod', regex: /\b(?:schema\.safeParse|safeParse|schema\.parse|zod|yup|ajv)\b/i },
|
|
39
|
+
{ name: 'parameterized/prepared', regex: /(?:\$1|\$\d+|\?|Prisma\.sql|prepared|bindValue|bindParam)/i },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export class TaintTracker {
|
|
43
|
+
/**
|
|
44
|
+
* Analyze dataflow for a finding's code context.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} context — { file, line, matched, code, ast, scopeTree }
|
|
47
|
+
* @returns {object} — { isTainted, isSanitized, isStatic, reason, confidenceAdjustment }
|
|
48
|
+
*/
|
|
49
|
+
static evaluateFinding(context) {
|
|
50
|
+
const { line, matched = '', code = '' } = context;
|
|
51
|
+
const lines = code ? code.split('\n') : [];
|
|
52
|
+
const findingLine = lines[line - 1] || matched;
|
|
53
|
+
|
|
54
|
+
// ── 1. Comment check ──────────────────────────────────────────────────────
|
|
55
|
+
if (/^\s*(?:\/\/|#|\*|\/\*|<!--)/.test(findingLine)) {
|
|
56
|
+
return {
|
|
57
|
+
isTainted: false,
|
|
58
|
+
isSanitized: true,
|
|
59
|
+
isStatic: true,
|
|
60
|
+
reason: 'Finding is inside a comment line',
|
|
61
|
+
confidenceAdjustment: 'low',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 2. Direct string literal / constant check ────────────────────────────
|
|
66
|
+
if (/^['"][^'"]*['"]$/.test(matched.trim())) {
|
|
67
|
+
return {
|
|
68
|
+
isTainted: false,
|
|
69
|
+
isSanitized: false,
|
|
70
|
+
isStatic: true,
|
|
71
|
+
reason: 'Value is a static string literal without interpolation',
|
|
72
|
+
confidenceAdjustment: 'low',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── 3. Check for immediate user input on the finding line ─────────────────
|
|
77
|
+
const hasDirectUserInput = UNTRUSTED_SOURCES.some(src => src.test(findingLine));
|
|
78
|
+
const hasDirectSanitization = SANITIZERS.some(s => s.regex.test(findingLine));
|
|
79
|
+
|
|
80
|
+
if (hasDirectSanitization && !hasDirectUserInput) {
|
|
81
|
+
return {
|
|
82
|
+
isTainted: false,
|
|
83
|
+
isSanitized: true,
|
|
84
|
+
isStatic: false,
|
|
85
|
+
reason: 'Finding is wrapped in active sanitization/validation logic',
|
|
86
|
+
confidenceAdjustment: 'low',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 4. AST Scope-based upstream trace ─────────────────────────────────────
|
|
91
|
+
let scopeTree = context.scopeTree;
|
|
92
|
+
if (!scopeTree && code) {
|
|
93
|
+
const parsed = ASTParser.parse(code, context.file || '');
|
|
94
|
+
scopeTree = ScopeTree.build(parsed.ast, code);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (scopeTree) {
|
|
98
|
+
const scope = scopeTree.getScopeAt(line);
|
|
99
|
+
const encFn = scopeTree.getEnclosingFunction(line);
|
|
100
|
+
const inTryCatch = scopeTree.isInTryCatch(line);
|
|
101
|
+
|
|
102
|
+
// Check the lines within the enclosing scope (not bleeding into other functions)
|
|
103
|
+
const startLine = scope.range.startLine;
|
|
104
|
+
const endLine = Math.min(line, scope.range.endLine);
|
|
105
|
+
const upstreamLines = lines.slice(startLine - 1, endLine - 1);
|
|
106
|
+
const upstreamText = upstreamLines.join('\n');
|
|
107
|
+
|
|
108
|
+
const hasScopeUserInput = UNTRUSTED_SOURCES.some(src => src.test(upstreamText));
|
|
109
|
+
const matchedSanitizer = SANITIZERS.find(s => s.regex.test(upstreamText));
|
|
110
|
+
|
|
111
|
+
if (matchedSanitizer && !hasDirectUserInput) {
|
|
112
|
+
return {
|
|
113
|
+
isTainted: false,
|
|
114
|
+
isSanitized: true,
|
|
115
|
+
isStatic: false,
|
|
116
|
+
sanitizer: matchedSanitizer.name,
|
|
117
|
+
reason: `Sanitization detected in enclosing scope via ${matchedSanitizer.name}`,
|
|
118
|
+
confidenceAdjustment: 'downgrade',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (hasScopeUserInput || hasDirectUserInput || encFn?.params?.length > 0) {
|
|
123
|
+
return {
|
|
124
|
+
isTainted: true,
|
|
125
|
+
isSanitized: false,
|
|
126
|
+
isStatic: false,
|
|
127
|
+
inTryCatch,
|
|
128
|
+
reason: 'Untrusted user input flows into this sink within function scope',
|
|
129
|
+
confidenceAdjustment: 'confirm',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if variables referenced in matched text are static in scope
|
|
134
|
+
const idMatches = matched.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [];
|
|
135
|
+
const staticVars = idMatches.filter(id => scopeTree.isVariableStatic(id, line));
|
|
136
|
+
if (staticVars.length > 0 && !hasScopeUserInput) {
|
|
137
|
+
return {
|
|
138
|
+
isTainted: false,
|
|
139
|
+
isSanitized: false,
|
|
140
|
+
isStatic: true,
|
|
141
|
+
reason: `Referenced variable(s) [${staticVars.join(', ')}] are statically initialized constants`,
|
|
142
|
+
confidenceAdjustment: 'downgrade',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Default neutral
|
|
148
|
+
return {
|
|
149
|
+
isTainted: hasDirectUserInput,
|
|
150
|
+
isSanitized: hasDirectSanitization,
|
|
151
|
+
isStatic: false,
|
|
152
|
+
reason: 'Standard pattern match',
|
|
153
|
+
confidenceAdjustment: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default TaintTracker;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis branding & identity
|
|
3
|
+
* ==========================
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for the banner, tagline, and product name used
|
|
6
|
+
* across the CLI. Importing this avoids hardcoded "praxis" / "Praxis"
|
|
7
|
+
* strings drifting out of sync after the rebrand.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
export const PRODUCT_NAME = 'praxis';
|
|
13
|
+
export const PRODUCT_DISPLAY = 'Praxis';
|
|
14
|
+
export const TAGLINE = 'From finding to fix, on autopilot.';
|
|
15
|
+
export const SUBTITLE = '28 agents. AI-native security with agentic remediation.';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Print the praxis ASCII banner. Call at the top of any command that
|
|
19
|
+
* should show branding (mostly `--help` and `praxis` with no args).
|
|
20
|
+
*/
|
|
21
|
+
export function printBanner(version) {
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.cyan('██████╗ ██████╗ █████╗ ██╗ ██╗██╗███████╗'));
|
|
24
|
+
console.log(chalk.cyan('██╔══██╗██╔══██╗██╔══██╗╚██╗██╔╝██║██╔════╝'));
|
|
25
|
+
console.log(chalk.cyan('██████╔╝██████╔╝███████║ ╚███╔╝ ██║███████╗'));
|
|
26
|
+
console.log(chalk.cyan('██╔═══╝ ██╔══██╗██╔══██║ ██╔██╗ ██║╚════██║'));
|
|
27
|
+
console.log(chalk.cyan('██║ ██║ ██║██║ ██║██╔╝ ██╗██║███████║'));
|
|
28
|
+
console.log(chalk.cyan('╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝'));
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(chalk.white.bold(` ${TAGLINE}`));
|
|
31
|
+
if (version) {
|
|
32
|
+
console.log(chalk.gray(` v${version} · ${SUBTITLE}`));
|
|
33
|
+
} else {
|
|
34
|
+
console.log(chalk.gray(` ${SUBTITLE}`));
|
|
35
|
+
}
|
|
36
|
+
console.log();
|
|
37
|
+
}
|
package/cli/core/env.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal .env loader (no dependency).
|
|
3
|
+
*
|
|
4
|
+
* Loads KEY=VALUE pairs from <rootPath>/.env into process.env without
|
|
5
|
+
* overriding variables that are already set (real environment wins).
|
|
6
|
+
* Lines starting with # and blank lines are ignored; values may be
|
|
7
|
+
* wrapped in single or double quotes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
export function loadDotEnv(rootPath) {
|
|
14
|
+
try {
|
|
15
|
+
const envPath = path.join(rootPath, '.env');
|
|
16
|
+
if (!fs.existsSync(envPath)) return false;
|
|
17
|
+
|
|
18
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
19
|
+
for (const line of content.split('\n')) {
|
|
20
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
|
|
21
|
+
if (!m) continue;
|
|
22
|
+
const key = m[1];
|
|
23
|
+
let val = m[2].trim();
|
|
24
|
+
if (
|
|
25
|
+
(val.startsWith('"') && val.endsWith('"') && val.length >= 2) ||
|
|
26
|
+
(val.startsWith("'") && val.endsWith("'") && val.length >= 2)
|
|
27
|
+
) {
|
|
28
|
+
val = val.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
if (process.env[key] === undefined || process.env[key] === '') {
|
|
31
|
+
process.env[key] = val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling helpers
|
|
3
|
+
* ======================
|
|
4
|
+
*
|
|
5
|
+
* Replacement for the ~124 silent `try { ... } catch { /* skip *\/ }`
|
|
6
|
+
* blocks scattered across the codebase. Errors are no longer invisible:
|
|
7
|
+
* they surface under `--verbose` (or when `PRAXIS_DEBUG=1`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as output from '../utils/output.js';
|
|
11
|
+
|
|
12
|
+
const DEBUG = process.env.PRAXIS_DEBUG === '1' || process.env.PRAXIS_DEBUG === 'true';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run `fn`, return its result, and on throw return `fallback` instead.
|
|
16
|
+
* Optionally logs the error under verbose/debug mode so it stops being
|
|
17
|
+
* invisible.
|
|
18
|
+
*
|
|
19
|
+
* const config = safeCatch(() => JSON.parse(raw), {}, { ctx: 'config' });
|
|
20
|
+
*/
|
|
21
|
+
export function safeCatch(fn, fallback, options = {}) {
|
|
22
|
+
const { ctx = 'unknown', verbose = false } = options;
|
|
23
|
+
try {
|
|
24
|
+
return fn();
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (DEBUG || verbose) {
|
|
27
|
+
output.warning(`[${ctx}] swallowed: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Async variant.
|
|
35
|
+
*/
|
|
36
|
+
export async function safeCatchAsync(fn, fallback, options = {}) {
|
|
37
|
+
const { ctx = 'unknown', verbose = false } = options;
|
|
38
|
+
try {
|
|
39
|
+
return await fn();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (DEBUG || verbose) {
|
|
42
|
+
output.warning(`[${ctx}] swallowed: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert any value into an Error so callers can rely on `.message` and
|
|
50
|
+
* `.stack`. Useful at boundaries (catch arms with `catch (e)` where `e`
|
|
51
|
+
* may be a string).
|
|
52
|
+
*/
|
|
53
|
+
export function toError(value) {
|
|
54
|
+
if (value instanceof Error) return value;
|
|
55
|
+
if (typeof value === 'string') return new Error(value);
|
|
56
|
+
try {
|
|
57
|
+
return new Error(JSON.stringify(value));
|
|
58
|
+
} catch {
|
|
59
|
+
return new Error(String(value));
|
|
60
|
+
}
|
|
61
|
+
}
|
package/cli/core/fs.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path & filesystem helpers
|
|
3
|
+
* =========================
|
|
4
|
+
*
|
|
5
|
+
* One implementation of the path-validation logic that was previously
|
|
6
|
+
* duplicated 16+ times across `cli/commands/`. Commands should use these
|
|
7
|
+
* instead of rolling their own `path.resolve + fs.existsSync` checks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import * as output from '../utils/output.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a user-supplied path to an absolute path and verify it exists.
|
|
16
|
+
*
|
|
17
|
+
* Exits the process with code 1 (and prints a friendly error) if the path
|
|
18
|
+
* is missing, unless `{ exitOnMissing: false }` is passed. Returns the
|
|
19
|
+
* absolute path on success.
|
|
20
|
+
*/
|
|
21
|
+
export function validatePath(targetPath, options = {}) {
|
|
22
|
+
const { exitOnMissing = true, label = 'path' } = options;
|
|
23
|
+
const absolutePath = path.resolve(targetPath || '.');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(absolutePath)) {
|
|
26
|
+
if (exitOnMissing) {
|
|
27
|
+
output.error(`${label} does not exist: ${absolutePath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return absolutePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a path and require that it points at a directory. Same exit
|
|
38
|
+
* semantics as `validatePath`.
|
|
39
|
+
*/
|
|
40
|
+
export function validateDir(targetPath, options = {}) {
|
|
41
|
+
const absolutePath = validatePath(targetPath, options);
|
|
42
|
+
if (!absolutePath) return null;
|
|
43
|
+
|
|
44
|
+
const stat = fs.statSync(absolutePath);
|
|
45
|
+
if (!stat.isDirectory()) {
|
|
46
|
+
if (options.exitOnMissing !== false) {
|
|
47
|
+
output.error(`expected a directory, got a file: ${absolutePath}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return absolutePath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Synchronously and atomically ensure a directory exists. Mirrors the
|
|
57
|
+
* `mkdir -p` idiom used throughout the codebase.
|
|
58
|
+
*/
|
|
59
|
+
export function ensureDir(dir) {
|
|
60
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
return dir;
|
|
62
|
+
}
|