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,132 @@
1
+ /**
2
+ * Shannon Entropy Scoring
3
+ * =======================
4
+ *
5
+ * Used to reduce false positives in secret detection.
6
+ *
7
+ * CONCEPT:
8
+ * Real secrets (API keys, tokens) are randomly generated and have HIGH entropy.
9
+ * Placeholder values like "your-api-key-here" or "example123" have LOW entropy
10
+ * because they follow predictable patterns or use common words.
11
+ *
12
+ * Shannon entropy measures the "randomness" of a string on a scale of 0-8.
13
+ * - 0: Completely uniform ("aaaaaaaaaaaaaaaa")
14
+ * - 2-3: Low entropy ("your-api-key-here", "example_value")
15
+ * - 3.5+: High entropy ("xK9mP2nQ8vL4jR7s") - likely a real secret
16
+ * - 5+: Very high entropy (random bytes, base64)
17
+ *
18
+ * We only apply entropy checks to "generic" patterns that lack specific prefixes.
19
+ * Patterns with known prefixes (sk-ant-, ghp_, AKIA...) are already precise enough.
20
+ */
21
+
22
+ // =============================================================================
23
+ // ENTROPY CALCULATION
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Calculate Shannon entropy of a string.
28
+ * Returns a number between 0 and log2(charset size).
29
+ */
30
+ export function shannonEntropy(str) {
31
+ if (!str || str.length === 0) return 0;
32
+
33
+ const freq = {};
34
+ for (const char of str) {
35
+ freq[char] = (freq[char] || 0) + 1;
36
+ }
37
+
38
+ return Object.values(freq).reduce((sum, count) => {
39
+ const p = count / str.length;
40
+ return sum - p * Math.log2(p);
41
+ }, 0);
42
+ }
43
+
44
+ // Minimum entropy to consider a match a real secret
45
+ // Real secrets: >3.5 | Placeholders: <3.0 | Safe buffer: 3.5
46
+ export const ENTROPY_THRESHOLD = 3.5;
47
+
48
+ // Strings shorter than this are unreliable for entropy analysis
49
+ const MIN_ENTROPY_LENGTH = 16;
50
+
51
+ // =============================================================================
52
+ // VALUE EXTRACTION
53
+ // =============================================================================
54
+
55
+ /**
56
+ * Extract the actual secret value from a matched string.
57
+ *
58
+ * Patterns often match the full assignment, e.g.:
59
+ * apiKey = "abc123xyz..."
60
+ *
61
+ * We want to score just the value part, not the variable name,
62
+ * because variable names are low-entropy and would skew the score.
63
+ */
64
+ function extractSecretValue(matched) {
65
+ // Match: = "value" or : "value" or = value
66
+ const assignmentMatch = matched.match(/[:=]\s*["']?([a-zA-Z0-9_\-+/=.]{12,})["']?\s*$/);
67
+ if (assignmentMatch) return assignmentMatch[1];
68
+
69
+ // Match: Bearer <token>
70
+ const bearerMatch = matched.match(/Bearer\s+([a-zA-Z0-9_\-+/=.]{12,})/i);
71
+ if (bearerMatch) return bearerMatch[1];
72
+
73
+ // Match: quoted value anywhere
74
+ const quotedMatch = matched.match(/["']([a-zA-Z0-9_\-+/=.]{12,})["']/);
75
+ if (quotedMatch) return quotedMatch[1];
76
+
77
+ return matched;
78
+ }
79
+
80
+ // =============================================================================
81
+ // PUBLIC API
82
+ // =============================================================================
83
+
84
+ /**
85
+ * Determine if a regex match looks like a real secret based on entropy.
86
+ *
87
+ * Returns true → keep the finding (high entropy or can't determine)
88
+ * Returns false → filter it out (low entropy, likely a placeholder)
89
+ */
90
+ export function isHighEntropyMatch(matched) {
91
+ const value = extractSecretValue(matched);
92
+
93
+ // If we can't extract a meaningful value, keep the finding
94
+ if (!value || value.length < MIN_ENTROPY_LENGTH) return true;
95
+
96
+ // Common placeholder patterns - fast path rejection
97
+ const PLACEHOLDER_PATTERNS = [
98
+ /^(your[-_]?|my[-_]?|example[-_]?|test[-_]?|dummy[-_]?|fake[-_]?|sample[-_]?)/i,
99
+ /^(xxx+|yyy+|zzz+|aaa+|000+)/i,
100
+ /^(insert|replace|changeme|placeholder|todo|fixme)/i,
101
+ /([-_]here|[-_]goes|[-_]key|[-_]token|[-_]secret)$/i,
102
+ /^[a-z]+[-_][a-z]+[-_][a-z]+$/, // looks like-a-passphrase not a key
103
+ /^(add[-_]?your|put[-_]?your|enter[-_]?your|set[-_]?your)/i,
104
+ /^(secret|password|token|apikey|api_key|key|value)[-_]?[0-9]*$/i,
105
+ /^(n\/a|null|undefined|none|empty|blank)/i,
106
+ /^(demo|staging|dev|development|local)[-_]/i,
107
+ /^(abcdef|qwerty|asdfgh|123456|letmein)/i,
108
+ /(.)\1{5,}/, // 6+ repeated chars: aaaaaaa, 111111
109
+ ];
110
+
111
+ if (PLACEHOLDER_PATTERNS.some(p => p.test(value))) return false;
112
+
113
+ const entropy = shannonEntropy(value);
114
+ return entropy >= ENTROPY_THRESHOLD;
115
+ }
116
+
117
+ /**
118
+ * Get a human-readable confidence label for a finding.
119
+ */
120
+ export function getConfidence(pattern, matched) {
121
+ // Strict prefix patterns (e.g. sk-ant-, ghp_, AKIA) are always high confidence
122
+ if (!pattern.requiresEntropyCheck) return 'high';
123
+
124
+ const value = extractSecretValue(matched);
125
+ if (!value || value.length < MIN_ENTROPY_LENGTH) return 'medium';
126
+
127
+ const entropy = shannonEntropy(value);
128
+
129
+ if (entropy >= 4.5) return 'high';
130
+ if (entropy >= ENTROPY_THRESHOLD) return 'medium';
131
+ return 'low'; // Should have been filtered, but just in case
132
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Fix ledger — read the find→fix→verify record for a project.
3
+ * ============================================================================
4
+ *
5
+ * The scanner's stated moat is "find → fix → verify", but the report stops at
6
+ * findings, so the differentiator is only visible in the terminal. `.praxis/fixes.jsonl`
7
+ * already records every applied change (with its verification outcome and the plan
8
+ * needed to reverse it), and `.praxis/failures.jsonl` records every plan that was
9
+ * proposed and *rejected* — which is the more interesting half of the story, since a
10
+ * rejected fix is the loop declining to make a change it cannot justify.
11
+ *
12
+ * Semantics worth stating plainly, because it is easy to misreport:
13
+ *
14
+ * - `fixes.jsonl` means **currently applied**. `praxis undo` *removes* reverted
15
+ * entries from the log rather than annotating them, so this module reports no
16
+ * "undone" count — there is no such data, and inventing one would be a false
17
+ * negative (the exact failure mode this project keeps refusing to ship).
18
+ * - An entry is **reversible** when its plan still carries enough information to
19
+ * undo it: a file creation, an append, or line edits.
20
+ * - Unreadable or malformed lines are counted, never silently dropped.
21
+ *
22
+ * Everything here is best-effort and never throws: a missing ledger must not break a
23
+ * report, and a broken ledger must not invent activity.
24
+ */
25
+
26
+ import fs from 'fs';
27
+ import path from 'path';
28
+
29
+ const FIX_LOG = path.join('.praxis', 'fixes.jsonl');
30
+ const FAILURE_LOG = path.join('.praxis', 'failures.jsonl');
31
+
32
+ /**
33
+ * Reads a JSONL log, tolerating absence and malformed lines.
34
+ * @returns {{entries: object[], unreadable: number, error: string|null}}
35
+ */
36
+ function readJsonl(absolutePath) {
37
+ let raw;
38
+ try {
39
+ raw = fs.readFileSync(absolutePath, 'utf8');
40
+ } catch (err) {
41
+ const missing = err.code === 'ENOENT';
42
+ return { entries: [], unreadable: 0, error: missing ? null : err.message };
43
+ }
44
+
45
+ const entries = [];
46
+ let unreadable = 0;
47
+ for (const line of raw.split('\n')) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed) continue;
50
+ try {
51
+ const parsed = JSON.parse(trimmed);
52
+ if (parsed && typeof parsed === 'object') entries.push(parsed);
53
+ else unreadable++;
54
+ } catch {
55
+ unreadable++;
56
+ }
57
+ }
58
+ return { entries, unreadable, error: null };
59
+ }
60
+
61
+ /** True when the plan still carries enough information for `praxis undo` to reverse it. */
62
+ export function isReversible(plan) {
63
+ const files = plan?.files;
64
+ if (!Array.isArray(files) || files.length === 0) return false;
65
+ return files.some(fc =>
66
+ fc && (fc.create === true || fc.append !== undefined || Array.isArray(fc.edits) && fc.edits.length > 0)
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Reads the fix ledger for a project.
72
+ *
73
+ * @param {string} rootPath Project root (the directory containing `.praxis/`).
74
+ * @returns {{
75
+ * applied: object[], rejected: object[],
76
+ * appliedUnreadable: number, rejectedUnreadable: number,
77
+ * error: string|null, hasLog: boolean
78
+ * }}
79
+ */
80
+ export function readFixLedger(rootPath = process.cwd()) {
81
+ // Coerce before any path work: `path.join(null, …)` throws a TypeError outside the
82
+ // readJsonl try/catch, which would break the "never throws" contract this module
83
+ // advertises. A missing root falls back to the cwd, which simply yields no ledger.
84
+ const root = typeof rootPath === 'string' && rootPath.length > 0 ? rootPath : process.cwd();
85
+
86
+ const fixes = readJsonl(path.join(root, FIX_LOG));
87
+ const failures = readJsonl(path.join(root, FAILURE_LOG));
88
+
89
+ return {
90
+ applied: fixes.entries,
91
+ rejected: failures.entries,
92
+ appliedUnreadable: fixes.unreadable,
93
+ rejectedUnreadable: failures.unreadable,
94
+ error: fixes.error || failures.error,
95
+ hasLog: fixes.entries.length > 0 || fixes.error !== null,
96
+ };
97
+ }
98
+
99
+ /** Aggregate counters for the ledger panel. */
100
+ export function summarizeFixLedger(ledger) {
101
+ const applied = ledger?.applied || [];
102
+ const rejected = ledger?.rejected || [];
103
+
104
+ const verified = applied.filter(e => e.verified === true).length;
105
+ const reversible = applied.filter(e => isReversible(e.plan)).length;
106
+
107
+ // Findings addressed by an applied fix. Summed across entries, so a finding fixed in
108
+ // two files counts once per file — that is what the log actually records.
109
+ const findingsFixed = applied.reduce((sum, e) => sum + (Array.isArray(e.findings) ? e.findings.length : 0), 0);
110
+
111
+ const rejectedByReason = {};
112
+ for (const r of rejected) {
113
+ const reason = r.reason || 'unknown';
114
+ rejectedByReason[reason] = (rejectedByReason[reason] || 0) + 1;
115
+ }
116
+
117
+ return {
118
+ appliedCount: applied.length,
119
+ verified,
120
+ unverified: applied.length - verified,
121
+ reversible,
122
+ irreversible: applied.length - reversible,
123
+ findingsFixed,
124
+ rejectedCount: rejected.length,
125
+ rejectedByReason,
126
+ };
127
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Hermes Tool Registry — Praxis × Hermes Agent
3
+ * =================================================
4
+ *
5
+ * Declares Praxis's five security tools in the Hermes Agent tool-registry
6
+ * format. Import this module in your Hermes agent bootstrap to register
7
+ * Praxis as a first-class citizen in the tool registry.
8
+ *
9
+ * USAGE:
10
+ * import { HERMES_TOOLS, registerWithHermes } from './hermes-tool-registry.js';
11
+ *
12
+ * // Option A — register all tools at once
13
+ * await registerWithHermes(agent.toolRegistry);
14
+ *
15
+ * // Option B — use the raw definitions
16
+ * for (const tool of HERMES_TOOLS) agent.toolRegistry.register(tool);
17
+ *
18
+ * SECURITY NOTE:
19
+ * These definitions are pinned, hardcoded, and integrity-verified at load
20
+ * time. They are never fetched from a remote URL. Do not replace the
21
+ * INTEGRITY_HASH values without auditing the updated definitions.
22
+ */
23
+
24
+ import { createHash } from 'crypto';
25
+ import path from 'path';
26
+ import { fileURLToPath } from 'url';
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+
30
+ // =============================================================================
31
+ // TOOL DEFINITIONS (Hermes function-call schema format)
32
+ // =============================================================================
33
+
34
+ export const HERMES_TOOLS = [
35
+ {
36
+ name: 'praxis_audit',
37
+ description:
38
+ 'Run a Praxis security audit on a local codebase directory. ' +
39
+ 'Returns a findings report with severity-graded issues, CWE/OWASP mappings, ' +
40
+ 'and remediation guidance. Use before deploying any code or merging PRs.',
41
+ parameters: {
42
+ type: 'object',
43
+ properties: {
44
+ path: {
45
+ type: 'string',
46
+ description: 'Absolute path to the project root directory to scan.',
47
+ },
48
+ severity: {
49
+ type: 'string',
50
+ enum: ['critical', 'high', 'medium', 'low'],
51
+ description: 'Minimum severity threshold for reported findings.',
52
+ default: 'medium',
53
+ },
54
+ deep: {
55
+ type: 'boolean',
56
+ description: 'Enable deep LLM-powered taint analysis (Haiku→Sonnet→Opus pipeline). Slower but more accurate.',
57
+ default: false,
58
+ },
59
+ },
60
+ required: ['path'],
61
+ additionalProperties: false,
62
+ },
63
+ handler: async ({ path: scanPath, severity = 'medium', deep = false }) => {
64
+ const { auditCommand } = await import('../commands/audit.js');
65
+ return auditCommand(scanPath, { severity, deep, json: true, quiet: true });
66
+ },
67
+ },
68
+
69
+ {
70
+ name: 'praxis_scan_mcp',
71
+ description:
72
+ 'Analyze an MCP server manifest (URL or local file path) for security issues ' +
73
+ 'before connecting. Checks for prompt injection in tool descriptions, credential ' +
74
+ 'harvesting patterns, Hermes function-call poisoning, schema bypass (additionalProperties: true), ' +
75
+ 'and known-malicious server hashes. Returns per-tool findings.',
76
+ parameters: {
77
+ type: 'object',
78
+ properties: {
79
+ target: {
80
+ type: 'string',
81
+ description: 'URL (https://...) or absolute local file path to the MCP manifest JSON.',
82
+ },
83
+ },
84
+ required: ['target'],
85
+ additionalProperties: false,
86
+ },
87
+ handler: async ({ target }) => {
88
+ const { scanMcpCommand } = await import('../commands/scan-mcp.js');
89
+ return scanMcpCommand(target, { json: true });
90
+ },
91
+ },
92
+
93
+ {
94
+ name: 'praxis_get_findings',
95
+ description:
96
+ 'Retrieve findings from the last saved Praxis scan report for a project. ' +
97
+ 'Optionally filter by minimum severity. Returns an array of findings with rule, ' +
98
+ 'title, severity, file, line, and remediation guidance.',
99
+ parameters: {
100
+ type: 'object',
101
+ properties: {
102
+ path: {
103
+ type: 'string',
104
+ description: 'Absolute path to the project root (used to locate the saved report).',
105
+ },
106
+ severity: {
107
+ type: 'string',
108
+ enum: ['critical', 'high', 'medium', 'low'],
109
+ description: 'Minimum severity to include in results.',
110
+ default: 'medium',
111
+ },
112
+ },
113
+ required: ['path'],
114
+ additionalProperties: false,
115
+ },
116
+ handler: async ({ path: projectPath, severity = 'medium' }) => {
117
+ const { mcpGetFindings } = await import('../commands/mcp.js');
118
+ return mcpGetFindings({ projectPath, severity });
119
+ },
120
+ },
121
+
122
+ {
123
+ name: 'praxis_suppress_finding',
124
+ description:
125
+ 'Suppress a known-safe finding by inserting an inline praxis-ignore comment ' +
126
+ 'in the source file before the flagged line. Use only when the finding is a ' +
127
+ 'confirmed false positive and you can document why it is safe.',
128
+ parameters: {
129
+ type: 'object',
130
+ properties: {
131
+ file: {
132
+ type: 'string',
133
+ description: 'Absolute path to the source file containing the finding.',
134
+ },
135
+ line: {
136
+ type: 'number',
137
+ description: 'Line number of the finding (1-based).',
138
+ },
139
+ reason: {
140
+ type: 'string',
141
+ description: 'Human-readable explanation of why this finding is safe to suppress.',
142
+ },
143
+ },
144
+ required: ['file', 'line', 'reason'],
145
+ additionalProperties: false,
146
+ },
147
+ handler: async ({ file, line, reason }) => {
148
+ const { mcpSuppressFinding } = await import('../commands/mcp.js');
149
+ return mcpSuppressFinding({ file, line, reason });
150
+ },
151
+ },
152
+
153
+ {
154
+ name: 'praxis_memory_list',
155
+ description:
156
+ 'List all entries in the Praxis security memory for a project. ' +
157
+ 'The memory stores learned false positives that are automatically filtered ' +
158
+ 'from future scans. Returns each entry with its rule, file pattern, and ' +
159
+ 'the snippet that was suppressed.',
160
+ parameters: {
161
+ type: 'object',
162
+ properties: {
163
+ path: {
164
+ type: 'string',
165
+ description: 'Absolute path to the project root.',
166
+ },
167
+ },
168
+ required: ['path'],
169
+ additionalProperties: false,
170
+ },
171
+ handler: async ({ path: projectPath }) => {
172
+ const { SecurityMemory } = await import('./security-memory.js');
173
+ const mem = new SecurityMemory(projectPath);
174
+ return mem.list();
175
+ },
176
+ },
177
+ ];
178
+
179
+ // =============================================================================
180
+ // INTEGRITY VERIFICATION
181
+ // Each tool definition is hashed at module load time. If the registry is
182
+ // tampered with (e.g. supply-chain attack), the hash check will fail.
183
+ // Run `node -e "import('./hermes-tool-registry.js').then(m => m.printHashes())"` to regenerate.
184
+ // =============================================================================
185
+
186
+ const KNOWN_HASHES = {
187
+ praxis_audit: '4d282d29e44fcc01',
188
+ praxis_scan_mcp: 'f967aea9626ca840',
189
+ praxis_get_findings: 'c09c9447efd574b3',
190
+ praxis_suppress_finding: '3b7339419fe52ac7',
191
+ praxis_memory_list: 'c71c996716d1805b',
192
+ };
193
+
194
+ function toolHash(tool) {
195
+ // Hash name + description + parameter schema only (not handler function)
196
+ const canonical = JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters });
197
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
198
+ }
199
+
200
+ export function verifyIntegrity() {
201
+ const mismatches = [];
202
+ for (const tool of HERMES_TOOLS) {
203
+ const actual = toolHash(tool);
204
+ const expected = KNOWN_HASHES[tool.name];
205
+ if (expected && actual !== expected) {
206
+ mismatches.push({ tool: tool.name, expected, actual });
207
+ }
208
+ }
209
+ return mismatches;
210
+ }
211
+
212
+ export function printHashes() {
213
+ console.log('// Current tool definition hashes — paste into KNOWN_HASHES:');
214
+ for (const tool of HERMES_TOOLS) {
215
+ console.log(` ${tool.name}: '${toolHash(tool)}',`);
216
+ }
217
+ }
218
+
219
+ // =============================================================================
220
+ // REGISTRATION HELPER
221
+ // =============================================================================
222
+
223
+ /**
224
+ * Register all Praxis tools with a Hermes tool registry instance.
225
+ *
226
+ * @param {object} toolRegistry — Hermes ToolRegistry instance with a .register() method
227
+ * @param {object} options
228
+ * @param {boolean} [options.skipVerification=false] — bypass hash verification (not recommended)
229
+ * @param {boolean} [options.quiet=false] — suppress registration log lines
230
+ */
231
+ export async function registerWithHermes(toolRegistry, options = {}) {
232
+ if (!options.skipVerification) { // praxis-ignore — this is the integrity-check implementation, not a bypass
233
+ const mismatches = verifyIntegrity();
234
+ if (mismatches.length > 0) {
235
+ const msg = mismatches.map(m => ` ${m.tool}: expected ${m.expected}, got ${m.actual}`).join('\n');
236
+ throw new Error(`Praxis tool registry integrity check failed:\n${msg}\n\nThis may indicate a supply-chain attack. Run praxis --version to verify your installation.`);
237
+ }
238
+ }
239
+
240
+ for (const tool of HERMES_TOOLS) {
241
+ if (typeof toolRegistry.register === 'function') {
242
+ toolRegistry.register(tool);
243
+ } else if (typeof toolRegistry.registerTool === 'function') {
244
+ toolRegistry.registerTool(tool);
245
+ } else {
246
+ throw new Error('toolRegistry must have a .register() or .registerTool() method');
247
+ }
248
+ if (!options.quiet) {
249
+ console.log(` [praxis] Registered tool: ${tool.name}`);
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Per-source disk cache with TTL.
3
+ *
4
+ * Lives at ~/.praxis/intel/<source>.json. Each entry is wrapped in:
5
+ * { fetchedAt: ISO string, ttlMs: number, payload: ... }
6
+ *
7
+ * Sources call cache.read('osv') to get the last good payload (or null),
8
+ * and cache.write('osv', data, ttlMs) after a successful fetch.
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import os from 'os';
14
+
15
+ // Lazy — resolve home each call so HOME overrides (e.g. in tests) take effect.
16
+ const rootDir = () => path.join(os.homedir(), '.praxis', 'intel');
17
+
18
+ function ensureDir() {
19
+ const dir = rootDir();
20
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
21
+ }
22
+
23
+ function pathFor(source) {
24
+ return path.join(rootDir(), `${source}.json`);
25
+ }
26
+
27
+ export function read(source) {
28
+ try {
29
+ const file = pathFor(source);
30
+ if (!fs.existsSync(file)) return null;
31
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export function write(source, payload, ttlMs) {
38
+ ensureDir();
39
+ const entry = {
40
+ fetchedAt: new Date().toISOString(),
41
+ ttlMs,
42
+ payload,
43
+ };
44
+ fs.writeFileSync(pathFor(source), JSON.stringify(entry, null, 2));
45
+ return entry;
46
+ }
47
+
48
+ export function isFresh(entry) {
49
+ if (!entry || !entry.fetchedAt || !entry.ttlMs) return false;
50
+ const age = Date.now() - new Date(entry.fetchedAt).getTime();
51
+ return age < entry.ttlMs;
52
+ }
53
+
54
+ export function ageMs(entry) {
55
+ if (!entry || !entry.fetchedAt) return Infinity;
56
+ return Date.now() - new Date(entry.fetchedAt).getTime();
57
+ }
58
+
59
+ export function root() {
60
+ return rootDir();
61
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * HTTP helper used by all intel source fetchers.
3
+ *
4
+ * - 30s timeout (overridable)
5
+ * - Retries on 5xx and network errors with exponential backoff
6
+ * - User-Agent header so providers can identify praxis traffic
7
+ */
8
+
9
+ const DEFAULT_TIMEOUT = 30_000;
10
+ const DEFAULT_RETRIES = 2;
11
+ const USER_AGENT = 'praxis-intel/1.0 (+https://github.com/Ganron007/Praxis)';
12
+
13
+ export async function safeFetch(url, options = {}) {
14
+ const { timeout = DEFAULT_TIMEOUT, retries = DEFAULT_RETRIES, ...rest } = options;
15
+
16
+ // Scanner hardening: only allow http/https to external feed endpoints.
17
+ // Prevents SSRF if a future intel source ever resolves a fetch target from
18
+ // untrusted data (file://, ftp://, and internal hosts are rejected).
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(url);
22
+ } catch {
23
+ throw new Error(`safeFetch: invalid URL ${String(url).slice(0, 80)}`);
24
+ }
25
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
26
+ throw new Error(`safeFetch: non-http(s) scheme rejected: ${parsed.protocol}`);
27
+ }
28
+ const LOOPBACK_RE = /^(?:127\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|0\.0\.0\.0|::1|localhost)(?:[:/]|$)/i;
29
+ if (LOOPBACK_RE.test(parsed.hostname)) {
30
+ throw new Error(`safeFetch: loopback/private host rejected: ${parsed.hostname}`);
31
+ }
32
+
33
+ let lastErr;
34
+ for (let attempt = 0; attempt <= retries; attempt++) {
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), timeout);
37
+ try {
38
+ const res = await fetch(url, { // praxis-ignore SSRF_USER_URL_FETCH — scheme + loopback guarded above
39
+ ...rest,
40
+ signal: controller.signal,
41
+ headers: {
42
+ 'User-Agent': USER_AGENT,
43
+ ...(rest.headers || {}),
44
+ },
45
+ });
46
+ clearTimeout(timer);
47
+
48
+ // Retry on 5xx, otherwise return whatever we got (caller decides)
49
+ if (res.status >= 500 && attempt < retries) {
50
+ await sleep(backoffMs(attempt));
51
+ continue;
52
+ }
53
+ return res;
54
+ } catch (err) {
55
+ clearTimeout(timer);
56
+ lastErr = err;
57
+ if (attempt < retries) {
58
+ await sleep(backoffMs(attempt));
59
+ continue;
60
+ }
61
+ }
62
+ }
63
+ throw lastErr || new Error(`Failed to fetch ${url}`);
64
+ }
65
+
66
+ export async function fetchJson(url, options = {}) {
67
+ const res = await safeFetch(url, options);
68
+ if (!res.ok) {
69
+ throw new Error(`HTTP ${res.status} from ${url}`);
70
+ }
71
+ return res.json();
72
+ }
73
+
74
+ export async function fetchText(url, options = {}) {
75
+ const res = await safeFetch(url, options);
76
+ if (!res.ok) {
77
+ throw new Error(`HTTP ${res.status} from ${url}`);
78
+ }
79
+ return res.text();
80
+ }
81
+
82
+ function sleep(ms) {
83
+ return new Promise(r => setTimeout(r, ms));
84
+ }
85
+
86
+ function backoffMs(attempt) {
87
+ return Math.min(1000 * Math.pow(2, attempt), 8000);
88
+ }