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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/ai-defense/cost-protection.md +292 -0
  4. package/ai-defense/llm-security-checklist.md +324 -0
  5. package/ai-defense/prompt-injection-patterns.js +283 -0
  6. package/ai-defense/system-prompt-armor.md +327 -0
  7. package/checklists/launch-day.md +168 -0
  8. package/cli/agents/abom-generator.js +225 -0
  9. package/cli/agents/agent-attestation-agent.js +318 -0
  10. package/cli/agents/agent-config-scanner.js +787 -0
  11. package/cli/agents/agent-telemetry-agent.js +415 -0
  12. package/cli/agents/agentic-security-agent.js +296 -0
  13. package/cli/agents/agentic-supply-chain-agent.js +463 -0
  14. package/cli/agents/ai-infra-inventory-agent.js +449 -0
  15. package/cli/agents/api-fuzzer.js +345 -0
  16. package/cli/agents/auth-bypass-agent.js +348 -0
  17. package/cli/agents/base-agent.js +280 -0
  18. package/cli/agents/cicd-scanner.js +300 -0
  19. package/cli/agents/config-auditor.js +757 -0
  20. package/cli/agents/deep-analyzer.js +776 -0
  21. package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
  22. package/cli/agents/exception-handler-agent.js +187 -0
  23. package/cli/agents/git-history-scanner.js +169 -0
  24. package/cli/agents/governance-audits.js +138 -0
  25. package/cli/agents/hermes-security-agent.js +536 -0
  26. package/cli/agents/html-reporter.js +1125 -0
  27. package/cli/agents/index.js +147 -0
  28. package/cli/agents/injection-tester.js +502 -0
  29. package/cli/agents/legal-risk-agent.js +328 -0
  30. package/cli/agents/llm-redteam.js +199 -0
  31. package/cli/agents/managed-agent-scanner.js +333 -0
  32. package/cli/agents/mcp-security-agent.js +588 -0
  33. package/cli/agents/memory-poisoning-agent.js +305 -0
  34. package/cli/agents/mobile-scanner.js +231 -0
  35. package/cli/agents/model-file-scanner.js +259 -0
  36. package/cli/agents/orchestrator.js +355 -0
  37. package/cli/agents/pii-compliance-agent.js +301 -0
  38. package/cli/agents/policy-engine.js +229 -0
  39. package/cli/agents/prompt-injection-prober.js +224 -0
  40. package/cli/agents/rag-security-agent.js +204 -0
  41. package/cli/agents/recon-agent.js +207 -0
  42. package/cli/agents/sbom-generator.js +265 -0
  43. package/cli/agents/scoring-engine.js +273 -0
  44. package/cli/agents/ssrf-prober.js +130 -0
  45. package/cli/agents/stateful-watcher.js +238 -0
  46. package/cli/agents/supabase-rls-agent.js +154 -0
  47. package/cli/agents/supply-chain-agent.js +857 -0
  48. package/cli/agents/swarm-orchestrator.js +200 -0
  49. package/cli/agents/verifier-agent.js +303 -0
  50. package/cli/agents/vibe-coding-agent.js +250 -0
  51. package/cli/bin/praxis.js +866 -0
  52. package/cli/commands/abom.js +73 -0
  53. package/cli/commands/agent-fix.js +1245 -0
  54. package/cli/commands/audit.js +1180 -0
  55. package/cli/commands/autofix.js +383 -0
  56. package/cli/commands/baseline.js +193 -0
  57. package/cli/commands/benchmark.js +327 -0
  58. package/cli/commands/checklist.js +223 -0
  59. package/cli/commands/ci.js +403 -0
  60. package/cli/commands/deps.js +516 -0
  61. package/cli/commands/diff.js +200 -0
  62. package/cli/commands/doctor.js +195 -0
  63. package/cli/commands/env-audit.js +349 -0
  64. package/cli/commands/fix.js +218 -0
  65. package/cli/commands/guard.js +396 -0
  66. package/cli/commands/hooks.js +278 -0
  67. package/cli/commands/init.js +514 -0
  68. package/cli/commands/legal.js +158 -0
  69. package/cli/commands/live-advisories.js +241 -0
  70. package/cli/commands/mcp.js +660 -0
  71. package/cli/commands/openclaw.js +386 -0
  72. package/cli/commands/red-team.js +350 -0
  73. package/cli/commands/redteam.js +78 -0
  74. package/cli/commands/remediate.js +797 -0
  75. package/cli/commands/rotate.js +768 -0
  76. package/cli/commands/rules.js +196 -0
  77. package/cli/commands/scan-mcp.js +534 -0
  78. package/cli/commands/scan-skill.js +588 -0
  79. package/cli/commands/scan-standard.js +251 -0
  80. package/cli/commands/scan.js +524 -0
  81. package/cli/commands/score.js +449 -0
  82. package/cli/commands/shell.js +514 -0
  83. package/cli/commands/team-report.js +398 -0
  84. package/cli/commands/undo.js +161 -0
  85. package/cli/commands/update-intel.js +126 -0
  86. package/cli/commands/vibe-check.js +276 -0
  87. package/cli/commands/watch.js +757 -0
  88. package/cli/commands/web.js +63 -0
  89. package/cli/core/ast/guardrail-detector.js +141 -0
  90. package/cli/core/ast/index.js +22 -0
  91. package/cli/core/ast/parser.js +676 -0
  92. package/cli/core/ast/scope-tree.js +287 -0
  93. package/cli/core/ast/taint-tracker.js +158 -0
  94. package/cli/core/branding.js +37 -0
  95. package/cli/core/env.js +38 -0
  96. package/cli/core/errors.js +61 -0
  97. package/cli/core/fs.js +62 -0
  98. package/cli/core/output/compliance.js +90 -0
  99. package/cli/core/output/html-theme.js +158 -0
  100. package/cli/core/output/index.js +57 -0
  101. package/cli/core/output/json.js +48 -0
  102. package/cli/core/output/sarif.js +240 -0
  103. package/cli/core/version.js +67 -0
  104. package/cli/core/web/jobs.js +183 -0
  105. package/cli/core/web/projects.js +146 -0
  106. package/cli/core/web/server.js +439 -0
  107. package/cli/data/atlas-knowledge.json +5640 -0
  108. package/cli/data/eaa-catalog.json +39 -0
  109. package/cli/data/known-mcps.json +26 -0
  110. package/cli/data/probes/prompt-injection-corpus.json +271 -0
  111. package/cli/data/threat-intel.json +85 -0
  112. package/cli/data/threatpacks/latest.json +41 -0
  113. package/cli/hooks/patterns.js +313 -0
  114. package/cli/hooks/post-tool-use.js +140 -0
  115. package/cli/hooks/pre-tool-use.js +186 -0
  116. package/cli/index.js +90 -0
  117. package/cli/providers/llm-provider.js +766 -0
  118. package/cli/utils/autofix-rules.js +74 -0
  119. package/cli/utils/cache-manager.js +310 -0
  120. package/cli/utils/compliance-map.js +191 -0
  121. package/cli/utils/entropy.js +132 -0
  122. package/cli/utils/fix-ledger.js +127 -0
  123. package/cli/utils/hermes-tool-registry.js +252 -0
  124. package/cli/utils/intel/cache.js +61 -0
  125. package/cli/utils/intel/http.js +88 -0
  126. package/cli/utils/intel/index.js +235 -0
  127. package/cli/utils/intel/merge.js +229 -0
  128. package/cli/utils/intel/sources/epss.js +54 -0
  129. package/cli/utils/intel/sources/ghsa.js +81 -0
  130. package/cli/utils/intel/sources/gitguardian.js +40 -0
  131. package/cli/utils/intel/sources/gitleaks.js +101 -0
  132. package/cli/utils/intel/sources/kev.js +38 -0
  133. package/cli/utils/intel/sources/nvd.js +84 -0
  134. package/cli/utils/intel/sources/osv.js +132 -0
  135. package/cli/utils/intel/sources/phylum.js +44 -0
  136. package/cli/utils/intel/sources/snyk.js +46 -0
  137. package/cli/utils/intel/sources/socket.js +69 -0
  138. package/cli/utils/intel/sources/sonatype.js +84 -0
  139. package/cli/utils/intel/sources/threatpack.js +69 -0
  140. package/cli/utils/mcp-trust.js +60 -0
  141. package/cli/utils/output.js +251 -0
  142. package/cli/utils/patterns.js +1130 -0
  143. package/cli/utils/pdf-generator.js +94 -0
  144. package/cli/utils/plugin-loader.js +364 -0
  145. package/cli/utils/rule-import.js +228 -0
  146. package/cli/utils/rule-registry.js +426 -0
  147. package/cli/utils/scan-fingerprint.js +109 -0
  148. package/cli/utils/scan-playbook.js +312 -0
  149. package/cli/utils/score-history.js +119 -0
  150. package/cli/utils/secrets-verifier.js +247 -0
  151. package/cli/utils/security-memory.js +296 -0
  152. package/cli/utils/standards/atlas-knowledge.js +87 -0
  153. package/cli/utils/standards/index.js +127 -0
  154. package/cli/utils/standards/sources/avid.js +45 -0
  155. package/cli/utils/standards/sources/eu-ai-act.js +89 -0
  156. package/cli/utils/standards/sources/google-saif.js +39 -0
  157. package/cli/utils/standards/sources/iso-42001.js +94 -0
  158. package/cli/utils/standards/sources/mitre-atlas.js +54 -0
  159. package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
  160. package/cli/utils/standards/sources/owasp-llm.js +45 -0
  161. package/cli/utils/standards/sources/owasp-ml.js +45 -0
  162. package/cli/utils/threat-intel.js +265 -0
  163. package/configs/firebase/firestore-rules.txt +215 -0
  164. package/configs/firebase/security-checklist.md +236 -0
  165. package/configs/firebase/storage-rules.txt +206 -0
  166. package/configs/gitignore-template +258 -0
  167. package/configs/nextjs-security-headers.js +220 -0
  168. package/configs/praxisignore-template +50 -0
  169. package/configs/supabase/secure-client.ts +225 -0
  170. package/configs/supabase/security-checklist.md +278 -0
  171. package/docs/THIRD_PARTY_NOTICES.md +26 -0
  172. package/docs/THREAT_INTEL.md +292 -0
  173. package/docs/USAGE.md +1205 -0
  174. package/docs/design/WEB-UI.md +82 -0
  175. package/package.json +71 -0
  176. package/scripts/check-determinism.mjs +119 -0
  177. package/snippets/README.md +122 -0
  178. package/snippets/api-security/api-security-checklist.md +412 -0
  179. package/snippets/api-security/cors-config.ts +322 -0
  180. package/snippets/api-security/input-validation.ts +430 -0
  181. package/snippets/auth/jwt-checklist.md +322 -0
  182. package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
  183. package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Bounded job queue for the web UI.
3
+ * ============================================================================
4
+ *
5
+ * Runs scans through the existing `Orchestrator` so the web UI never reimplements
6
+ * detection — the numbers in the browser are the same numbers the CLI produces.
7
+ *
8
+ * Threat T7 (resource exhaustion): concurrency is capped and the queue length is
9
+ * capped, because a scan is expensive (28 agents) and an untrusted local caller could
10
+ * otherwise pile up work. Jobs live in memory; the report is what gets persisted, so // praxis-ignore AGENT_MEMORY_NO_EXPIRY — doc comment; JobQueue._trim() bounds retention to 50 finished jobs
11
+ * restarting the server does not lose scan *results* the report already captured.
12
+ */
13
+
14
+ import path from 'path';
15
+ import { EventEmitter } from 'events';
16
+
17
+ export const MAX_CONCURRENCY = 2;
18
+ export const MAX_QUEUE = 16;
19
+ const JOB_RETENTION = 50; // keep the most recent N finished jobs for inspection
20
+
21
+ let seq = 0;
22
+
23
+ export class JobQueue extends EventEmitter {
24
+ constructor({ concurrency = MAX_CONCURRENCY, runScan } = {}) {
25
+ super();
26
+ this.concurrency = Math.max(1, concurrency);
27
+ this.maxQueue = MAX_QUEUE;
28
+ this.runScan = runScan;
29
+ this.jobs = [];
30
+ this.pending = [];
31
+ this.running = 0;
32
+ }
33
+
34
+ /**
35
+ * Enqueues a scan for a registered project.
36
+ * @returns {{ok:true, job:object} | {ok:false, error:string}}
37
+ */
38
+ enqueue(project) {
39
+ const queued = this.jobs.length + this.pending.length;
40
+ if (queued >= this.maxQueue) {
41
+ return { ok: false, error: `queue is full (max ${this.maxQueue} scans)` };
42
+ }
43
+ const job = {
44
+ id: `job-${++seq}`,
45
+ projectId: project.id,
46
+ projectName: project.name,
47
+ root: project.root,
48
+ status: 'queued',
49
+ queuedAt: new Date().toISOString(),
50
+ startedAt: null,
51
+ finishedAt: null,
52
+ progress: { done: 0, total: 0, current: null },
53
+ result: null,
54
+ error: null,
55
+ };
56
+ this.jobs.push(job);
57
+ this.pending.push(job);
58
+ this._emitUpdate(job);
59
+ // Snapshot before draining: _drain() starts the job synchronously, so capturing
60
+ // afterwards would hand the client a job already marked 'running' and make the
61
+ // 'queued' state unobservable.
62
+ const snapshot = this._public(job);
63
+ this._drain();
64
+ return { ok: true, job: snapshot };
65
+ }
66
+
67
+ get(id) {
68
+ return this.jobs.find(j => j.id === id) || null;
69
+ }
70
+
71
+ /** All jobs, oldest first, as API-safe objects. */
72
+ list() {
73
+ return this.jobs.map(j => this._public(j));
74
+ }
75
+
76
+ cancel(id) {
77
+ const job = this.get(id);
78
+ if (!job) return { ok: false, error: 'unknown job' };
79
+ if (job.status === 'running') {
80
+ // The orchestrator is not abortable mid-scan; be honest rather than pretend.
81
+ return { ok: false, error: 'scan already running and cannot be cancelled' };
82
+ }
83
+ if (job.status === 'queued') {
84
+ this.pending = this.pending.filter(j => j.id !== id);
85
+ job.status = 'cancelled';
86
+ job.finishedAt = new Date().toISOString();
87
+ this._emitUpdate(job);
88
+ return { ok: true };
89
+ }
90
+ return { ok: false, error: `job is already ${job.status}` };
91
+ }
92
+
93
+ _public(job) {
94
+ const { result, root, ...rest } = job;
95
+ return { ...rest, hasResult: Boolean(result) };
96
+ }
97
+
98
+ _emitUpdate(job) {
99
+ this.emit('update', this._public(job));
100
+ }
101
+
102
+ _drain() {
103
+ while (this.running < this.concurrency && this.pending.length > 0) {
104
+ const job = this.pending.shift();
105
+ this._run(job);
106
+ }
107
+ }
108
+
109
+ async _run(job) {
110
+ this.running++;
111
+ job.status = 'running';
112
+ job.startedAt = new Date().toISOString();
113
+ job.progress = { done: 0, total: 0, current: null };
114
+ this._emitUpdate(job);
115
+
116
+ try {
117
+ if (typeof this.runScan !== 'function') {
118
+ throw new Error('no scan runner configured');
119
+ }
120
+ const result = await this.runScan(job.root, progress => {
121
+ // Real per-agent progress from the orchestrator hook. If the hook never fires
122
+ // (no agents applicable), progress stays 0/0 rather than inventing a number.
123
+ if (progress) {
124
+ job.progress = {
125
+ done: Number(progress.done) || 0,
126
+ total: Number(progress.total) || 0,
127
+ current: progress.agent || null,
128
+ };
129
+ this._emitUpdate(job);
130
+ }
131
+ });
132
+ job.result = result;
133
+ job.status = 'done';
134
+ job.finishedAt = new Date().toISOString();
135
+ } catch (err) {
136
+ job.status = 'failed';
137
+ job.error = err.message || String(err);
138
+ job.finishedAt = new Date().toISOString();
139
+ } finally {
140
+ this.running--;
141
+ this._emitUpdate(job);
142
+ this._trim();
143
+ this._drain();
144
+ }
145
+ }
146
+
147
+ _trim() {
148
+ // Bound memory: drop the oldest finished jobs past the retention window.
149
+ const finished = this.jobs.filter(j => j.status !== 'running' && j.status !== 'queued');
150
+ if (finished.length <= JOB_RETENTION) return;
151
+ const drop = new Set(finished.slice(0, finished.length - JOB_RETENTION).map(j => j.id));
152
+ this.jobs = this.jobs.filter(j => !drop.has(j.id));
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Default scan runner: drives the real Orchestrator so the UI cannot drift from the CLI.
158
+ * Returns the same shape the CLI's JSON output produces.
159
+ */
160
+ export async function runScanWithOrchestrator(rootPath, onProgress) {
161
+ const { buildOrchestratorAsync } = await import('../../agents/index.js');
162
+ const orchestrator = await buildOrchestratorAsync(rootPath, { quiet: true });
163
+
164
+ // runAll returns { recon, findings, agentResults } — the same telemetry the CLI
165
+ // report renders, so the UI cannot drift from `praxis scan`.
166
+ // `quiet` must be passed to runAll itself, not just the builder: runAll reads its own
167
+ // options object, so omitting it would print CLI spinners into the HTTP server's stdout.
168
+ const { findings, agentResults, recon } = await orchestrator.runAll(rootPath, { // praxis-ignore AGENT_ESCALATED_PERMISSIONS — destructuring runAll()'s result; no permission handling
169
+ quiet: true,
170
+ onProgress: info => {
171
+ if (typeof onProgress === 'function') onProgress(info);
172
+ },
173
+ });
174
+
175
+ return {
176
+ root: path.basename(rootPath),
177
+ agentCount: (agentResults || []).length,
178
+ totalFindings: (findings || []).length,
179
+ agents: agentResults || [],
180
+ recon: recon || null,
181
+ findings: findings || [],
182
+ };
183
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Project registry for the web UI.
3
+ * ============================================================================
4
+ *
5
+ * The security-relevant decision (threats T2/T3 in docs/design/WEB-UI.md): the browser
6
+ * never sends a filesystem path.
7
+ *
8
+ * A client-supplied path would let any caller ask the server to scan `/`, read a home
9
+ * directory, or enumerate the host — turning "scan my code" into arbitrary file
10
+ * disclosure. So an operator registers projects here, by path, through the CLI or an
11
+ * explicit API call, and the API thereafter addresses projects by **id** only.
12
+ *
13
+ * Each registered path is resolved once and pinned, so a symlink swapped afterwards
14
+ * cannot silently redirect a scan somewhere else.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import os from 'os';
19
+ import path from 'path';
20
+ import crypto from 'crypto';
21
+
22
+ /** Hard ceiling so a runaway caller cannot fill the registry. */
23
+ export const MAX_PROJECTS = 64;
24
+
25
+ export function registryDir() {
26
+ // Overridable so tests (and sandboxed runs) do not write into the real home
27
+ // directory. Defaults to ~/.praxis/web, which is outside any scanned project so a
28
+ // scan never picks up the UI's own state.
29
+ const base = process.env.PRAXIS_WEB_HOME || path.join(os.homedir(), '.praxis', 'web');
30
+ return base;
31
+ }
32
+
33
+ function registryPath() {
34
+ return path.join(registryDir(), 'projects.json');
35
+ }
36
+
37
+ /**
38
+ * Validates and pins a candidate project path.
39
+ * @returns {{ok: true, root: string, name: string} | {ok: false, error: string}}
40
+ */
41
+ export function resolveProject(candidate) {
42
+ if (typeof candidate !== 'string' || candidate.trim() === '') {
43
+ return { ok: false, error: 'path must be a non-empty string' };
44
+ }
45
+ let resolved;
46
+ try {
47
+ resolved = fs.realpathSync(path.resolve(candidate.trim()));
48
+ } catch (err) {
49
+ return { ok: false, error: `cannot resolve path: ${err.message}` };
50
+ }
51
+ let stat;
52
+ try {
53
+ stat = fs.statSync(resolved);
54
+ } catch (err) {
55
+ return { ok: false, error: `cannot stat path: ${err.message}` };
56
+ }
57
+ if (!stat.isDirectory()) {
58
+ return { ok: false, error: 'project path must be a directory' };
59
+ }
60
+ return { ok: true, root: resolved, name: path.basename(resolved) || resolved };
61
+ }
62
+
63
+ /** Reads the registry; a missing or corrupt file yields an empty registry. */
64
+ export function readRegistry() {
65
+ try {
66
+ const data = JSON.parse(fs.readFileSync(registryPath(), 'utf8'));
67
+ if (!data || !Array.isArray(data.projects)) return { projects: [] };
68
+ return {
69
+ projects: data.projects.filter(p => p && typeof p.id === 'string' && typeof p.root === 'string'),
70
+ };
71
+ } catch {
72
+ return { projects: [] };
73
+ }
74
+ }
75
+
76
+ function writeRegistry(state) {
77
+ fs.mkdirSync(registryDir(), { recursive: true });
78
+ fs.writeFileSync(registryPath(), JSON.stringify(state, null, 2), 'utf8');
79
+ }
80
+
81
+ /** Lists registered projects, annotated with whether the pinned path still exists. */
82
+ export function listProjects() {
83
+ return readRegistry().projects.map(p => {
84
+ let present = false;
85
+ try {
86
+ present = fs.statSync(p.root).isDirectory();
87
+ } catch { present = false; }
88
+ return { id: p.id, name: p.name, root: p.root, present };
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Registers a project directory. Idempotent per resolved path.
94
+ * @returns {{ok: true, project: object, created: boolean} | {ok: false, error: string}}
95
+ */
96
+ export function addProject(candidate) {
97
+ const resolved = resolveProject(candidate);
98
+ if (!resolved.ok) return { ok: false, error: resolved.error };
99
+
100
+ const state = readRegistry();
101
+ const existing = state.projects.find(p => p.root === resolved.root);
102
+ if (existing) {
103
+ return { ok: true, project: existing, created: false };
104
+ }
105
+ if (state.projects.length >= MAX_PROJECTS) {
106
+ return { ok: false, error: `registry is full (max ${MAX_PROJECTS} projects)` };
107
+ }
108
+
109
+ const project = {
110
+ id: crypto.randomUUID(),
111
+ name: resolved.name,
112
+ root: resolved.root,
113
+ addedAt: new Date().toISOString(),
114
+ };
115
+ state.projects.push(project);
116
+ writeRegistry(state);
117
+ return { ok: true, project, created: true };
118
+ }
119
+
120
+ /** Removes a project by id. */
121
+ export function removeProject(id) {
122
+ const state = readRegistry();
123
+ const before = state.projects.length;
124
+ state.projects = state.projects.filter(p => p.id !== id);
125
+ if (state.projects.length === before) return { ok: false, error: 'no such project' };
126
+ writeRegistry(state);
127
+ return { ok: true };
128
+ }
129
+
130
+ /**
131
+ * Looks up a project by id. This is the only way the API turns a client value into a
132
+ * path — a client-supplied path is never resolved here.
133
+ *
134
+ * Returns `{ok:false}` for an unknown id, and flags a project whose pinned path has
135
+ * since disappeared so the caller can report that honestly rather than failing opaquely.
136
+ */
137
+ export function getProject(id) {
138
+ if (typeof id !== 'string' || id === '') return { ok: false, error: 'project id required' };
139
+ const found = readRegistry().projects.find(p => p.id === id);
140
+ if (!found) return { ok: false, error: 'unknown project id' };
141
+ let present = true;
142
+ try {
143
+ present = fs.statSync(found.root).isDirectory();
144
+ } catch { present = false; }
145
+ return { ok: true, project: { ...found, present } };
146
+ }