circle-ir-ai 1.1.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/CHANGELOG.md +105 -0
- package/LICENSE +15 -0
- package/README.md +336 -0
- package/dist/action-queue/aggregator.d.ts +40 -0
- package/dist/action-queue/aggregator.d.ts.map +1 -0
- package/dist/action-queue/aggregator.js +375 -0
- package/dist/action-queue/aggregator.js.map +1 -0
- package/dist/action-queue/index.d.ts +14 -0
- package/dist/action-queue/index.d.ts.map +1 -0
- package/dist/action-queue/index.js +17 -0
- package/dist/action-queue/index.js.map +1 -0
- package/dist/action-queue/queue.d.ts +74 -0
- package/dist/action-queue/queue.d.ts.map +1 -0
- package/dist/action-queue/queue.js +433 -0
- package/dist/action-queue/queue.js.map +1 -0
- package/dist/action-queue/types.d.ts +162 -0
- package/dist/action-queue/types.d.ts.map +1 -0
- package/dist/action-queue/types.js +44 -0
- package/dist/action-queue/types.js.map +1 -0
- package/dist/agents/enrichment-agent.d.ts +16 -0
- package/dist/agents/enrichment-agent.d.ts.map +1 -0
- package/dist/agents/enrichment-agent.js +102 -0
- package/dist/agents/enrichment-agent.js.map +1 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +15 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/mastra/agents.d.ts +373 -0
- package/dist/agents/mastra/agents.d.ts.map +1 -0
- package/dist/agents/mastra/agents.js +347 -0
- package/dist/agents/mastra/agents.js.map +1 -0
- package/dist/agents/mastra/index.d.ts +12 -0
- package/dist/agents/mastra/index.d.ts.map +1 -0
- package/dist/agents/mastra/index.js +17 -0
- package/dist/agents/mastra/index.js.map +1 -0
- package/dist/agents/mastra/instance.d.ts +383 -0
- package/dist/agents/mastra/instance.d.ts.map +1 -0
- package/dist/agents/mastra/instance.js +37 -0
- package/dist/agents/mastra/instance.js.map +1 -0
- package/dist/agents/mastra/steps.d.ts +300 -0
- package/dist/agents/mastra/steps.d.ts.map +1 -0
- package/dist/agents/mastra/steps.js +468 -0
- package/dist/agents/mastra/steps.js.map +1 -0
- package/dist/agents/mastra/swarm.d.ts +106 -0
- package/dist/agents/mastra/swarm.d.ts.map +1 -0
- package/dist/agents/mastra/swarm.js +501 -0
- package/dist/agents/mastra/swarm.js.map +1 -0
- package/dist/agents/mastra/workflow.d.ts +81 -0
- package/dist/agents/mastra/workflow.d.ts.map +1 -0
- package/dist/agents/mastra/workflow.js +460 -0
- package/dist/agents/mastra/workflow.js.map +1 -0
- package/dist/agents/multi/agents/security.d.ts +29 -0
- package/dist/agents/multi/agents/security.d.ts.map +1 -0
- package/dist/agents/multi/agents/security.js +830 -0
- package/dist/agents/multi/agents/security.js.map +1 -0
- package/dist/agents/multi/extractor.d.ts +21 -0
- package/dist/agents/multi/extractor.d.ts.map +1 -0
- package/dist/agents/multi/extractor.js +483 -0
- package/dist/agents/multi/extractor.js.map +1 -0
- package/dist/agents/multi/index.d.ts +32 -0
- package/dist/agents/multi/index.d.ts.map +1 -0
- package/dist/agents/multi/index.js +34 -0
- package/dist/agents/multi/index.js.map +1 -0
- package/dist/agents/multi/runner.d.ts +79 -0
- package/dist/agents/multi/runner.d.ts.map +1 -0
- package/dist/agents/multi/runner.js +323 -0
- package/dist/agents/multi/runner.js.map +1 -0
- package/dist/agents/security-agent.d.ts +16 -0
- package/dist/agents/security-agent.d.ts.map +1 -0
- package/dist/agents/security-agent.js +299 -0
- package/dist/agents/security-agent.js.map +1 -0
- package/dist/agents/types.d.ts +373 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +14 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/agents/verification-agent.d.ts +23 -0
- package/dist/agents/verification-agent.d.ts.map +1 -0
- package/dist/agents/verification-agent.js +217 -0
- package/dist/agents/verification-agent.js.map +1 -0
- package/dist/agents/workflow.d.ts +30 -0
- package/dist/agents/workflow.d.ts.map +1 -0
- package/dist/agents/workflow.js +79 -0
- package/dist/agents/workflow.js.map +1 -0
- package/dist/analysis/enriched.d.ts +16 -0
- package/dist/analysis/enriched.d.ts.map +1 -0
- package/dist/analysis/enriched.js +297 -0
- package/dist/analysis/enriched.js.map +1 -0
- package/dist/analysis/llm-correlated-predicates.d.ts +80 -0
- package/dist/analysis/llm-correlated-predicates.d.ts.map +1 -0
- package/dist/analysis/llm-correlated-predicates.js +255 -0
- package/dist/analysis/llm-correlated-predicates.js.map +1 -0
- package/dist/analysis/llm-cross-file-taint.d.ts +86 -0
- package/dist/analysis/llm-cross-file-taint.d.ts.map +1 -0
- package/dist/analysis/llm-cross-file-taint.js +264 -0
- package/dist/analysis/llm-cross-file-taint.js.map +1 -0
- package/dist/analysis/pattern-discovery.d.ts +79 -0
- package/dist/analysis/pattern-discovery.d.ts.map +1 -0
- package/dist/analysis/pattern-discovery.js +447 -0
- package/dist/analysis/pattern-discovery.js.map +1 -0
- package/dist/cache/file-cache.d.ts +89 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/file-cache.js +208 -0
- package/dist/cache/file-cache.js.map +1 -0
- package/dist/cache/index.d.ts +6 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cli/args.d.ts +52 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +422 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/colors.d.ts +31 -0
- package/dist/cli/colors.d.ts.map +1 -0
- package/dist/cli/colors.js +80 -0
- package/dist/cli/colors.js.map +1 -0
- package/dist/cli/commands/analyze-skill.d.ts +33 -0
- package/dist/cli/commands/analyze-skill.d.ts.map +1 -0
- package/dist/cli/commands/analyze-skill.js +217 -0
- package/dist/cli/commands/analyze-skill.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +18 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +30 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/benchmark-runner.d.ts +42 -0
- package/dist/cli/commands/benchmark-runner.d.ts.map +1 -0
- package/dist/cli/commands/benchmark-runner.js +18 -0
- package/dist/cli/commands/benchmark-runner.js.map +1 -0
- package/dist/cli/commands/benchmark.d.ts +11 -0
- package/dist/cli/commands/benchmark.d.ts.map +1 -0
- package/dist/cli/commands/benchmark.js +90 -0
- package/dist/cli/commands/benchmark.js.map +1 -0
- package/dist/cli/commands/dead-code.d.ts +11 -0
- package/dist/cli/commands/dead-code.d.ts.map +1 -0
- package/dist/cli/commands/dead-code.js +65 -0
- package/dist/cli/commands/dead-code.js.map +1 -0
- package/dist/cli/commands/generate-spec.d.ts +11 -0
- package/dist/cli/commands/generate-spec.d.ts.map +1 -0
- package/dist/cli/commands/generate-spec.js +67 -0
- package/dist/cli/commands/generate-spec.js.map +1 -0
- package/dist/cli/commands/health.d.ts +11 -0
- package/dist/cli/commands/health.d.ts.map +1 -0
- package/dist/cli/commands/health.js +67 -0
- package/dist/cli/commands/health.js.map +1 -0
- package/dist/cli/commands/project.d.ts +21 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +92 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +11 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +68 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/secrets.d.ts +11 -0
- package/dist/cli/commands/secrets.d.ts.map +1 -0
- package/dist/cli/commands/secrets.js +71 -0
- package/dist/cli/commands/secrets.js.map +1 -0
- package/dist/cli/commands/swarm.d.ts +20 -0
- package/dist/cli/commands/swarm.d.ts.map +1 -0
- package/dist/cli/commands/swarm.js +174 -0
- package/dist/cli/commands/swarm.js.map +1 -0
- package/dist/cli/config.d.ts +103 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +307 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/discovery.d.ts +31 -0
- package/dist/cli/discovery.d.ts.map +1 -0
- package/dist/cli/discovery.js +212 -0
- package/dist/cli/discovery.js.map +1 -0
- package/dist/cli/formatters/index.d.ts +15 -0
- package/dist/cli/formatters/index.d.ts.map +1 -0
- package/dist/cli/formatters/index.js +51 -0
- package/dist/cli/formatters/index.js.map +1 -0
- package/dist/cli/formatters/json.d.ts +11 -0
- package/dist/cli/formatters/json.d.ts.map +1 -0
- package/dist/cli/formatters/json.js +12 -0
- package/dist/cli/formatters/json.js.map +1 -0
- package/dist/cli/formatters/project-json.d.ts +11 -0
- package/dist/cli/formatters/project-json.d.ts.map +1 -0
- package/dist/cli/formatters/project-json.js +12 -0
- package/dist/cli/formatters/project-json.js.map +1 -0
- package/dist/cli/formatters/project-sarif.d.ts +11 -0
- package/dist/cli/formatters/project-sarif.d.ts.map +1 -0
- package/dist/cli/formatters/project-sarif.js +127 -0
- package/dist/cli/formatters/project-sarif.js.map +1 -0
- package/dist/cli/formatters/project-summary.d.ts +11 -0
- package/dist/cli/formatters/project-summary.d.ts.map +1 -0
- package/dist/cli/formatters/project-summary.js +202 -0
- package/dist/cli/formatters/project-summary.js.map +1 -0
- package/dist/cli/formatters/sarif-shared.d.ts +101 -0
- package/dist/cli/formatters/sarif-shared.d.ts.map +1 -0
- package/dist/cli/formatters/sarif-shared.js +57 -0
- package/dist/cli/formatters/sarif-shared.js.map +1 -0
- package/dist/cli/formatters/sarif.d.ts +12 -0
- package/dist/cli/formatters/sarif.d.ts.map +1 -0
- package/dist/cli/formatters/sarif.js +92 -0
- package/dist/cli/formatters/sarif.js.map +1 -0
- package/dist/cli/formatters/summary.d.ts +11 -0
- package/dist/cli/formatters/summary.d.ts.map +1 -0
- package/dist/cli/formatters/summary.js +240 -0
- package/dist/cli/formatters/summary.js.map +1 -0
- package/dist/cli/formatters/two-phase-summary.d.ts +11 -0
- package/dist/cli/formatters/two-phase-summary.d.ts.map +1 -0
- package/dist/cli/formatters/two-phase-summary.js +188 -0
- package/dist/cli/formatters/two-phase-summary.js.map +1 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +555 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/components/clustering.d.ts +60 -0
- package/dist/components/clustering.d.ts.map +1 -0
- package/dist/components/clustering.js +129 -0
- package/dist/components/clustering.js.map +1 -0
- package/dist/components/enrichment.d.ts +45 -0
- package/dist/components/enrichment.d.ts.map +1 -0
- package/dist/components/enrichment.js +193 -0
- package/dist/components/enrichment.js.map +1 -0
- package/dist/components/index.d.ts +29 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +56 -0
- package/dist/components/index.js.map +1 -0
- package/dist/dead-code/detector.d.ts +200 -0
- package/dist/dead-code/detector.d.ts.map +1 -0
- package/dist/dead-code/detector.js +1003 -0
- package/dist/dead-code/detector.js.map +1 -0
- package/dist/dead-code/index.d.ts +7 -0
- package/dist/dead-code/index.d.ts.map +1 -0
- package/dist/dead-code/index.js +7 -0
- package/dist/dead-code/index.js.map +1 -0
- package/dist/extractors/index.d.ts +15 -0
- package/dist/extractors/index.d.ts.map +1 -0
- package/dist/extractors/index.js +14 -0
- package/dist/extractors/index.js.map +1 -0
- package/dist/extractors/natural-language.d.ts +46 -0
- package/dist/extractors/natural-language.d.ts.map +1 -0
- package/dist/extractors/natural-language.js +228 -0
- package/dist/extractors/natural-language.js.map +1 -0
- package/dist/extractors/tree-sitter.d.ts +33 -0
- package/dist/extractors/tree-sitter.d.ts.map +1 -0
- package/dist/extractors/tree-sitter.js +69 -0
- package/dist/extractors/tree-sitter.js.map +1 -0
- package/dist/extractors/types.d.ts +62 -0
- package/dist/extractors/types.d.ts.map +1 -0
- package/dist/extractors/types.js +54 -0
- package/dist/extractors/types.js.map +1 -0
- package/dist/health-score/calculator.d.ts +123 -0
- package/dist/health-score/calculator.d.ts.map +1 -0
- package/dist/health-score/calculator.js +444 -0
- package/dist/health-score/calculator.js.map +1 -0
- package/dist/health-score/index.d.ts +12 -0
- package/dist/health-score/index.d.ts.map +1 -0
- package/dist/health-score/index.js +14 -0
- package/dist/health-score/index.js.map +1 -0
- package/dist/health-score/metrics.d.ts +142 -0
- package/dist/health-score/metrics.d.ts.map +1 -0
- package/dist/health-score/metrics.js +332 -0
- package/dist/health-score/metrics.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/ax-client.d.ts +477 -0
- package/dist/llm/ax-client.d.ts.map +1 -0
- package/dist/llm/ax-client.js +1641 -0
- package/dist/llm/ax-client.js.map +1 -0
- package/dist/llm/config.d.ts +58 -0
- package/dist/llm/config.d.ts.map +1 -0
- package/dist/llm/config.js +97 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/discovery.d.ts +123 -0
- package/dist/llm/discovery.d.ts.map +1 -0
- package/dist/llm/discovery.js +505 -0
- package/dist/llm/discovery.js.map +1 -0
- package/dist/llm/enrichment.d.ts +108 -0
- package/dist/llm/enrichment.d.ts.map +1 -0
- package/dist/llm/enrichment.js +312 -0
- package/dist/llm/enrichment.js.map +1 -0
- package/dist/llm/index.d.ts +13 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +22 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/language-context.d.ts +64 -0
- package/dist/llm/language-context.d.ts.map +1 -0
- package/dist/llm/language-context.js +492 -0
- package/dist/llm/language-context.js.map +1 -0
- package/dist/llm/pattern-verification.d.ts +39 -0
- package/dist/llm/pattern-verification.d.ts.map +1 -0
- package/dist/llm/pattern-verification.js +127 -0
- package/dist/llm/pattern-verification.js.map +1 -0
- package/dist/llm/prompt-security.d.ts +120 -0
- package/dist/llm/prompt-security.d.ts.map +1 -0
- package/dist/llm/prompt-security.js +301 -0
- package/dist/llm/prompt-security.js.map +1 -0
- package/dist/llm/prompts/index.d.ts +31 -0
- package/dist/llm/prompts/index.d.ts.map +1 -0
- package/dist/llm/prompts/index.js +92 -0
- package/dist/llm/prompts/index.js.map +1 -0
- package/dist/llm/prompts/rust.d.ts +30 -0
- package/dist/llm/prompts/rust.d.ts.map +1 -0
- package/dist/llm/prompts/rust.js +121 -0
- package/dist/llm/prompts/rust.js.map +1 -0
- package/dist/llm/schemas.d.ts +892 -0
- package/dist/llm/schemas.d.ts.map +1 -0
- package/dist/llm/schemas.js +258 -0
- package/dist/llm/schemas.js.map +1 -0
- package/dist/llm/verification.d.ts +127 -0
- package/dist/llm/verification.d.ts.map +1 -0
- package/dist/llm/verification.js +394 -0
- package/dist/llm/verification.js.map +1 -0
- package/dist/project/analyzer.d.ts +30 -0
- package/dist/project/analyzer.d.ts.map +1 -0
- package/dist/project/analyzer.js +358 -0
- package/dist/project/analyzer.js.map +1 -0
- package/dist/project/call-graph.d.ts +22 -0
- package/dist/project/call-graph.d.ts.map +1 -0
- package/dist/project/call-graph.js +246 -0
- package/dist/project/call-graph.js.map +1 -0
- package/dist/project/index.d.ts +18 -0
- package/dist/project/index.d.ts.map +1 -0
- package/dist/project/index.js +20 -0
- package/dist/project/index.js.map +1 -0
- package/dist/project/taint-paths.d.ts +22 -0
- package/dist/project/taint-paths.d.ts.map +1 -0
- package/dist/project/taint-paths.js +265 -0
- package/dist/project/taint-paths.js.map +1 -0
- package/dist/project/two-phase-analyzer.d.ts +143 -0
- package/dist/project/two-phase-analyzer.d.ts.map +1 -0
- package/dist/project/two-phase-analyzer.js +646 -0
- package/dist/project/two-phase-analyzer.js.map +1 -0
- package/dist/project/type-hierarchy.d.ts +28 -0
- package/dist/project/type-hierarchy.d.ts.map +1 -0
- package/dist/project/type-hierarchy.js +218 -0
- package/dist/project/type-hierarchy.js.map +1 -0
- package/dist/secret-scan/index.d.ts +12 -0
- package/dist/secret-scan/index.d.ts.map +1 -0
- package/dist/secret-scan/index.js +14 -0
- package/dist/secret-scan/index.js.map +1 -0
- package/dist/secret-scan/patterns.d.ts +38 -0
- package/dist/secret-scan/patterns.d.ts.map +1 -0
- package/dist/secret-scan/patterns.js +473 -0
- package/dist/secret-scan/patterns.js.map +1 -0
- package/dist/secret-scan/scanner.d.ts +162 -0
- package/dist/secret-scan/scanner.d.ts.map +1 -0
- package/dist/secret-scan/scanner.js +511 -0
- package/dist/secret-scan/scanner.js.map +1 -0
- package/dist/security-scan/index.d.ts +12 -0
- package/dist/security-scan/index.d.ts.map +1 -0
- package/dist/security-scan/index.js +15 -0
- package/dist/security-scan/index.js.map +1 -0
- package/dist/security-scan/owasp-mapping.d.ts +29 -0
- package/dist/security-scan/owasp-mapping.d.ts.map +1 -0
- package/dist/security-scan/owasp-mapping.js +246 -0
- package/dist/security-scan/owasp-mapping.js.map +1 -0
- package/dist/security-scan/scanner.d.ts +204 -0
- package/dist/security-scan/scanner.d.ts.map +1 -0
- package/dist/security-scan/scanner.js +693 -0
- package/dist/security-scan/scanner.js.map +1 -0
- package/dist/security-scan/trend-tracker.d.ts +150 -0
- package/dist/security-scan/trend-tracker.d.ts.map +1 -0
- package/dist/security-scan/trend-tracker.js +299 -0
- package/dist/security-scan/trend-tracker.js.map +1 -0
- package/dist/skills/bundle-loader.d.ts +26 -0
- package/dist/skills/bundle-loader.d.ts.map +1 -0
- package/dist/skills/bundle-loader.js +284 -0
- package/dist/skills/bundle-loader.js.map +1 -0
- package/dist/skills/capability-mismatch.d.ts +21 -0
- package/dist/skills/capability-mismatch.d.ts.map +1 -0
- package/dist/skills/capability-mismatch.js +188 -0
- package/dist/skills/capability-mismatch.js.map +1 -0
- package/dist/skills/index.d.ts +10 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +9 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skill-analyzer.d.ts +16 -0
- package/dist/skills/skill-analyzer.d.ts.map +1 -0
- package/dist/skills/skill-analyzer.js +361 -0
- package/dist/skills/skill-analyzer.js.map +1 -0
- package/dist/skills/types.d.ts +195 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +7 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/specifica/conflict-resolver.d.ts +23 -0
- package/dist/specifica/conflict-resolver.d.ts.map +1 -0
- package/dist/specifica/conflict-resolver.js +129 -0
- package/dist/specifica/conflict-resolver.js.map +1 -0
- package/dist/specifica/evidence-aggregator.d.ts +33 -0
- package/dist/specifica/evidence-aggregator.d.ts.map +1 -0
- package/dist/specifica/evidence-aggregator.js +236 -0
- package/dist/specifica/evidence-aggregator.js.map +1 -0
- package/dist/specifica/evidence-extractor.d.ts +13 -0
- package/dist/specifica/evidence-extractor.d.ts.map +1 -0
- package/dist/specifica/evidence-extractor.js +431 -0
- package/dist/specifica/evidence-extractor.js.map +1 -0
- package/dist/specifica/feature-clustering.d.ts +19 -0
- package/dist/specifica/feature-clustering.d.ts.map +1 -0
- package/dist/specifica/feature-clustering.js +231 -0
- package/dist/specifica/feature-clustering.js.map +1 -0
- package/dist/specifica/generator.d.ts +16 -0
- package/dist/specifica/generator.d.ts.map +1 -0
- package/dist/specifica/generator.js +277 -0
- package/dist/specifica/generator.js.map +1 -0
- package/dist/specifica/index.d.ts +15 -0
- package/dist/specifica/index.d.ts.map +1 -0
- package/dist/specifica/index.js +18 -0
- package/dist/specifica/index.js.map +1 -0
- package/dist/specifica/prompts.d.ts +21 -0
- package/dist/specifica/prompts.d.ts.map +1 -0
- package/dist/specifica/prompts.js +196 -0
- package/dist/specifica/prompts.js.map +1 -0
- package/dist/specifica/spec-generator.d.ts +22 -0
- package/dist/specifica/spec-generator.d.ts.map +1 -0
- package/dist/specifica/spec-generator.js +229 -0
- package/dist/specifica/spec-generator.js.map +1 -0
- package/dist/specifica/types.d.ts +213 -0
- package/dist/specifica/types.d.ts.map +1 -0
- package/dist/specifica/types.js +7 -0
- package/dist/specifica/types.js.map +1 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +51 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,1641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ax-LLM Client
|
|
3
|
+
*
|
|
4
|
+
* DSPy-style LLM client using @ax-llm/ax library.
|
|
5
|
+
* Provides typed signatures and structured outputs.
|
|
6
|
+
*/
|
|
7
|
+
import { AxAIOpenAI, AxGen, AxSignature, } from '@ax-llm/ax';
|
|
8
|
+
import { getDefaultLLMConfig, validateLLMConfig } from './config.js';
|
|
9
|
+
import { RoleClassificationResponseSchema, SourceDiscoveryResponseSchema, SinkDiscoveryResponseSchema, VerificationResponseSchema, } from './schemas.js';
|
|
10
|
+
import { sanitizeCodeForPrompt, sanitizeListForPrompt, formatSystemPrompt, logInjectionAttempt, } from './prompt-security.js';
|
|
11
|
+
import { getPrompt } from './prompts/index.js';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Signatures for Security Analysis
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Signature for source/sink enrichment
|
|
17
|
+
*/
|
|
18
|
+
export const enrichmentSignature = AxSignature.create(`
|
|
19
|
+
code:string, className:string, methodName:string, existingSources:string[], existingSinks:string[] ->
|
|
20
|
+
additionalSources:object[], additionalSinks:object[], role:string, reasoning:string
|
|
21
|
+
`);
|
|
22
|
+
/**
|
|
23
|
+
* Signature for role classification
|
|
24
|
+
*/
|
|
25
|
+
export const roleClassificationSignature = AxSignature.create(`
|
|
26
|
+
className:string, methodNames:string, annotations:string, imports:string ->
|
|
27
|
+
role:string, confidence:number, reasoning:string, indicators:string[]
|
|
28
|
+
`);
|
|
29
|
+
/**
|
|
30
|
+
* Signature for source discovery
|
|
31
|
+
*/
|
|
32
|
+
export const sourceDiscoverySignature = AxSignature.create(`
|
|
33
|
+
methodCode:string, methodName:string, classRole:string, existingSources:string ->
|
|
34
|
+
additionalSources:object[]
|
|
35
|
+
`);
|
|
36
|
+
/**
|
|
37
|
+
* Signature for sink discovery
|
|
38
|
+
*/
|
|
39
|
+
export const sinkDiscoverySignature = AxSignature.create(`
|
|
40
|
+
methodCode:string, methodName:string, methodCalls:string, existingSinks:string ->
|
|
41
|
+
additionalSinks:object[]
|
|
42
|
+
`);
|
|
43
|
+
/**
|
|
44
|
+
* Signature for virtual call resolution
|
|
45
|
+
*/
|
|
46
|
+
export const virtualCallResolutionSignature = AxSignature.create(`
|
|
47
|
+
callExpression:string, interfaceType:string, availableImplementations:string, context:string ->
|
|
48
|
+
resolvedImplementation:string, confidence:number, reasoning:string
|
|
49
|
+
`);
|
|
50
|
+
/**
|
|
51
|
+
* Signature for pattern verification
|
|
52
|
+
*/
|
|
53
|
+
export const patternVerificationSignature = AxSignature.create(`
|
|
54
|
+
patterns:string, codeContext:string ->
|
|
55
|
+
verifications:object[]
|
|
56
|
+
`);
|
|
57
|
+
/**
|
|
58
|
+
* Signature for vulnerability verification
|
|
59
|
+
*/
|
|
60
|
+
export const verificationSignature = AxSignature.create(`
|
|
61
|
+
sourceCode:string, sourceLine:number, sourceType:string,
|
|
62
|
+
sinkCode:string, sinkLine:number, sinkType:string, cwe:string,
|
|
63
|
+
methodCode:string, methodName:string, className:string,
|
|
64
|
+
sanitizersInPath:string[] ->
|
|
65
|
+
verdict:string, confidence:number, reasoning:string, exploitability:string,
|
|
66
|
+
sanitizersFound:string[], attackVector:string
|
|
67
|
+
`);
|
|
68
|
+
/**
|
|
69
|
+
* Signature for list index tracking
|
|
70
|
+
* Uses JSON strings for compatibility with non-structured LLMs
|
|
71
|
+
*/
|
|
72
|
+
export const listIndexSignature = AxSignature.create(`
|
|
73
|
+
code:string, listVariable:string, operationsJson:string "JSON array of operations" ->
|
|
74
|
+
finalIndexMappingJson:string "JSON object mapping original to final indices",
|
|
75
|
+
taintedIndicesJson:string "JSON array of tainted index numbers",
|
|
76
|
+
reasoning:string
|
|
77
|
+
`);
|
|
78
|
+
/**
|
|
79
|
+
* Signature for correlated predicate analysis
|
|
80
|
+
* Uses JSON strings for compatibility with non-structured LLMs
|
|
81
|
+
*/
|
|
82
|
+
export const correlatedPredicateSignature = AxSignature.create(`
|
|
83
|
+
code:string, predicateLocationsJson:string "JSON array of predicate locations" ->
|
|
84
|
+
correlatedGroupsJson:string "JSON array of correlated predicate groups",
|
|
85
|
+
reasoning:string
|
|
86
|
+
`);
|
|
87
|
+
/**
|
|
88
|
+
* Signature for cross-file taint tracking
|
|
89
|
+
* Uses JSON string for taintFlows to avoid structured output requirement
|
|
90
|
+
*/
|
|
91
|
+
export const crossFileTaintSignature = AxSignature.create(`
|
|
92
|
+
sourceFile:string, sourceCode:string, targetFile:string, targetCode:string,
|
|
93
|
+
exportedTaintJson:string "JSON array of exported taint: [{variable, type, line}]",
|
|
94
|
+
importedSymbolsJson:string "JSON array of imported symbol names" ->
|
|
95
|
+
taintFlowsJson:string "JSON array of flows: [{sourceVar, sinkMethod, sinkLine, confidence, reasoning}]",
|
|
96
|
+
reasoning:string
|
|
97
|
+
`);
|
|
98
|
+
/**
|
|
99
|
+
* Maximum consecutive LLM failures before skipping further calls
|
|
100
|
+
*/
|
|
101
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
102
|
+
/**
|
|
103
|
+
* Maximum code context length to send to LLM (in characters)
|
|
104
|
+
* Prevents 400 errors from exceeding model context limits
|
|
105
|
+
*/
|
|
106
|
+
const MAX_CODE_CONTEXT_LENGTH = 8000;
|
|
107
|
+
/**
|
|
108
|
+
* Truncate code to fit within context limits
|
|
109
|
+
*/
|
|
110
|
+
function truncateCode(code, maxLength = MAX_CODE_CONTEXT_LENGTH) {
|
|
111
|
+
if (code.length <= maxLength)
|
|
112
|
+
return code;
|
|
113
|
+
// Try to truncate at a line boundary
|
|
114
|
+
const truncated = code.substring(0, maxLength);
|
|
115
|
+
const lastNewline = truncated.lastIndexOf('\n');
|
|
116
|
+
if (lastNewline > maxLength * 0.8) {
|
|
117
|
+
return truncated.substring(0, lastNewline) + '\n// ... (truncated)';
|
|
118
|
+
}
|
|
119
|
+
return truncated + '\n// ... (truncated)';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Ax-LLM based client for Circle-IR
|
|
123
|
+
*/
|
|
124
|
+
export class AxLLMClient {
|
|
125
|
+
config;
|
|
126
|
+
ai;
|
|
127
|
+
generators = new Map();
|
|
128
|
+
consecutiveFailures = 0;
|
|
129
|
+
llmDisabled = false;
|
|
130
|
+
constructor(config) {
|
|
131
|
+
this.config = { ...getDefaultLLMConfig(), ...config };
|
|
132
|
+
// Validate required configuration
|
|
133
|
+
validateLLMConfig(this.config);
|
|
134
|
+
// Initialize OpenAI-compatible provider with default model
|
|
135
|
+
this.ai = new AxAIOpenAI({
|
|
136
|
+
apiKey: this.config.apiKey,
|
|
137
|
+
apiURL: this.config.baseUrl,
|
|
138
|
+
config: {
|
|
139
|
+
model: this.config.phases.enrichment.model,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if LLM is currently available (not disabled due to failures)
|
|
145
|
+
*/
|
|
146
|
+
isAvailable() {
|
|
147
|
+
return !this.llmDisabled;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Reset the circuit breaker so LLM calls are re-enabled.
|
|
151
|
+
* Call this between files to prevent one file's failures from disabling
|
|
152
|
+
* LLM for the entire project analysis.
|
|
153
|
+
*/
|
|
154
|
+
resetCircuitBreaker() {
|
|
155
|
+
this.consecutiveFailures = 0;
|
|
156
|
+
this.llmDisabled = false;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Reset failure counter (call when LLM succeeds)
|
|
160
|
+
*/
|
|
161
|
+
resetFailures() {
|
|
162
|
+
this.consecutiveFailures = 0;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Track a failure and potentially disable LLM
|
|
166
|
+
*/
|
|
167
|
+
trackFailure() {
|
|
168
|
+
this.consecutiveFailures++;
|
|
169
|
+
if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
170
|
+
console.warn(`LLM disabled after ${MAX_CONSECUTIVE_FAILURES} consecutive failures`);
|
|
171
|
+
this.llmDisabled = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get or create a generator for a signature
|
|
176
|
+
*/
|
|
177
|
+
getGenerator(name, signature, options) {
|
|
178
|
+
if (!this.generators.has(name)) {
|
|
179
|
+
const gen = new AxGen(signature, {
|
|
180
|
+
description: options?.description,
|
|
181
|
+
});
|
|
182
|
+
this.generators.set(name, gen);
|
|
183
|
+
}
|
|
184
|
+
return this.generators.get(name);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get category-specific guidance for verification prompts
|
|
188
|
+
*/
|
|
189
|
+
getCategoryGuidance(cwe) {
|
|
190
|
+
const cweNum = cwe.replace(/\D/g, '');
|
|
191
|
+
switch (cweNum) {
|
|
192
|
+
case '501': // Trust Boundary Violation
|
|
193
|
+
return `
|
|
194
|
+
TRUST BOUNDARY (CWE-501) SPECIFIC:
|
|
195
|
+
- VULNERABLE: User input used as session ATTRIBUTE NAME: setAttribute(userInput, value)
|
|
196
|
+
- VULNERABLE: Unvalidated user data stored in session without sanitization
|
|
197
|
+
- SAFE: Constant/hardcoded attribute name with sanitized value
|
|
198
|
+
- SAFE: User data that has been validated against whitelist
|
|
199
|
+
- Key insight: The ATTRIBUTE NAME being user-controlled is the vulnerability, not just the value`;
|
|
200
|
+
case '79': // XSS
|
|
201
|
+
case '80':
|
|
202
|
+
case '81':
|
|
203
|
+
case '83':
|
|
204
|
+
return `
|
|
205
|
+
XSS (CWE-79) SPECIFIC SANITIZERS - Mark as FALSE_POSITIVE if ANY of these are used:
|
|
206
|
+
- encodeForHTML(), escapeHtml(), escapeHtml4()
|
|
207
|
+
- HtmlUtils.htmlEscape(), StringEscapeUtils.escapeHtml()
|
|
208
|
+
- ESAPI.encoder().encodeForHTML()
|
|
209
|
+
- org.owasp.benchmark.helpers.Utils.encodeForHTML()
|
|
210
|
+
- Writing to response HEADERS (not body) is generally safe
|
|
211
|
+
- Check if output is HTML-encoded before reaching response.getWriter()
|
|
212
|
+
|
|
213
|
+
FEW-SHOT EXAMPLES:
|
|
214
|
+
TRUE_POSITIVE:
|
|
215
|
+
String name = request.getParameter("name");
|
|
216
|
+
response.getWriter().println("<h1>" + name + "</h1>");
|
|
217
|
+
// User input directly output without encoding -> VULNERABLE
|
|
218
|
+
|
|
219
|
+
FALSE_POSITIVE:
|
|
220
|
+
String name = request.getParameter("name");
|
|
221
|
+
String safe = ESAPI.encoder().encodeForHTML(name);
|
|
222
|
+
response.getWriter().println("<h1>" + safe + "</h1>");
|
|
223
|
+
// Input is HTML-encoded before output -> SAFE`;
|
|
224
|
+
case '89': // SQL Injection
|
|
225
|
+
return `
|
|
226
|
+
SQL INJECTION (CWE-89) SPECIFIC:
|
|
227
|
+
- SAFE: PreparedStatement with ? placeholders and setString/setInt
|
|
228
|
+
- SAFE: Parameterized queries, named parameters
|
|
229
|
+
- VULNERABLE: String concatenation in SQL query
|
|
230
|
+
- VULNERABLE: Statement.executeQuery(string + userInput)
|
|
231
|
+
- Check if the query uses concatenation vs parameterization
|
|
232
|
+
|
|
233
|
+
FEW-SHOT EXAMPLES:
|
|
234
|
+
TRUE_POSITIVE:
|
|
235
|
+
String id = request.getParameter("id");
|
|
236
|
+
String sql = "SELECT * FROM users WHERE id = '" + id + "'";
|
|
237
|
+
stmt.executeQuery(sql);
|
|
238
|
+
// Concatenation of user input into SQL -> VULNERABLE
|
|
239
|
+
|
|
240
|
+
FALSE_POSITIVE:
|
|
241
|
+
String id = request.getParameter("id");
|
|
242
|
+
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
|
|
243
|
+
ps.setString(1, id);
|
|
244
|
+
ps.executeQuery();
|
|
245
|
+
// Parameterized query with ? placeholder -> SAFE`;
|
|
246
|
+
case '78': // Command Injection
|
|
247
|
+
return `
|
|
248
|
+
COMMAND INJECTION (CWE-78) SPECIFIC:
|
|
249
|
+
- SAFE: ProcessBuilder with String[] array (no shell interpretation)
|
|
250
|
+
- SAFE: Runtime.exec(String[]) with separate args
|
|
251
|
+
- VULNERABLE: Runtime.exec(String) with concatenated command
|
|
252
|
+
- VULNERABLE: Shell commands with user input
|
|
253
|
+
- Check if command is built via concatenation or array
|
|
254
|
+
|
|
255
|
+
FEW-SHOT EXAMPLES:
|
|
256
|
+
TRUE_POSITIVE:
|
|
257
|
+
String cmd = request.getParameter("cmd");
|
|
258
|
+
Runtime.getRuntime().exec(cmd);
|
|
259
|
+
// User input directly passed to exec() -> VULNERABLE
|
|
260
|
+
|
|
261
|
+
FALSE_POSITIVE:
|
|
262
|
+
String filename = request.getParameter("file");
|
|
263
|
+
String[] cmd = new String[] {"ls", "-l", filename};
|
|
264
|
+
ProcessBuilder pb = new ProcessBuilder(cmd);
|
|
265
|
+
pb.start();
|
|
266
|
+
// Array-based args (no shell), but still potentially dangerous
|
|
267
|
+
// Mark UNCERTAIN if filename not validated, TRUE_POSITIVE if clearly exploitable`;
|
|
268
|
+
case '22': // Path Traversal
|
|
269
|
+
return `
|
|
270
|
+
PATH TRAVERSAL (CWE-22) SPECIFIC:
|
|
271
|
+
- SAFE: Path normalized with getCanonicalPath() then validated
|
|
272
|
+
- SAFE: Paths validated against whitelist/base directory
|
|
273
|
+
- SAFE: FilenameUtils.getName() extracts just filename
|
|
274
|
+
- VULNERABLE: Direct file access with user-controlled path
|
|
275
|
+
- Check for path normalization and validation
|
|
276
|
+
|
|
277
|
+
FEW-SHOT EXAMPLES:
|
|
278
|
+
TRUE_POSITIVE:
|
|
279
|
+
String file = request.getParameter("file");
|
|
280
|
+
File f = new File("/uploads/" + file);
|
|
281
|
+
FileInputStream fis = new FileInputStream(f);
|
|
282
|
+
// User input directly used in file path -> VULNERABLE (../../../etc/passwd)
|
|
283
|
+
|
|
284
|
+
FALSE_POSITIVE:
|
|
285
|
+
String file = request.getParameter("file");
|
|
286
|
+
String safeName = FilenameUtils.getName(file); // Strips directories
|
|
287
|
+
File f = new File("/uploads/" + safeName);
|
|
288
|
+
FileInputStream fis = new FileInputStream(f);
|
|
289
|
+
// Path is sanitized by extracting only filename -> SAFE`;
|
|
290
|
+
case '90': // LDAP Injection
|
|
291
|
+
return `
|
|
292
|
+
LDAP INJECTION (CWE-90) SPECIFIC:
|
|
293
|
+
- SAFE: LDAP search with properly escaped filter
|
|
294
|
+
- SAFE: Using parameterized LDAP APIs
|
|
295
|
+
- VULNERABLE: String concatenation in LDAP filter
|
|
296
|
+
- Check for filter escaping functions`;
|
|
297
|
+
case '643': // XPath Injection
|
|
298
|
+
return `
|
|
299
|
+
XPATH INJECTION (CWE-643) SPECIFIC:
|
|
300
|
+
- SAFE: Parameterized XPath with variable binding
|
|
301
|
+
- SAFE: Input validated against whitelist
|
|
302
|
+
- VULNERABLE: String concatenation in XPath expression
|
|
303
|
+
- Check for XPath parameterization`;
|
|
304
|
+
case '918': // SSRF
|
|
305
|
+
return `
|
|
306
|
+
SSRF (CWE-918) SPECIFIC:
|
|
307
|
+
- VULNERABLE: User input used in URL construction without validation
|
|
308
|
+
- VULNERABLE: URL.openConnection() or HttpClient with user-controlled URL
|
|
309
|
+
- VULNERABLE: RestTemplate, WebClient with user-controlled URLs
|
|
310
|
+
- SAFE: URL validated against allowlist of hosts/protocols
|
|
311
|
+
- SAFE: Internal-only endpoints with no external access
|
|
312
|
+
- Key insight: Any user-controlled URL component is dangerous
|
|
313
|
+
|
|
314
|
+
FEW-SHOT EXAMPLES:
|
|
315
|
+
TRUE_POSITIVE:
|
|
316
|
+
String url = request.getParameter("url");
|
|
317
|
+
URL u = new URL(url);
|
|
318
|
+
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
|
|
319
|
+
conn.getInputStream();
|
|
320
|
+
// User-controlled URL without validation -> VULNERABLE
|
|
321
|
+
|
|
322
|
+
FALSE_POSITIVE:
|
|
323
|
+
String id = request.getParameter("id");
|
|
324
|
+
URL u = new URL("https://api.internal.com/data/" + URLEncoder.encode(id, "UTF-8"));
|
|
325
|
+
// Fixed host, only ID is user-controlled and encoded -> Generally SAFE`;
|
|
326
|
+
case '611': // XXE
|
|
327
|
+
return `
|
|
328
|
+
XXE (CWE-611) SPECIFIC:
|
|
329
|
+
- VULNERABLE: XMLParser without disabled external entities
|
|
330
|
+
- VULNERABLE: DocumentBuilderFactory without setFeature(DISALLOW_DTD)
|
|
331
|
+
- VULNERABLE: SAXParser, XMLReader without secure configuration
|
|
332
|
+
- SAFE: Factory configured with FEATURE_SECURE_PROCESSING
|
|
333
|
+
- SAFE: setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
|
|
334
|
+
- Key insight: Default XML parsers are often vulnerable
|
|
335
|
+
|
|
336
|
+
FEW-SHOT EXAMPLES:
|
|
337
|
+
TRUE_POSITIVE:
|
|
338
|
+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
|
339
|
+
DocumentBuilder db = dbf.newDocumentBuilder();
|
|
340
|
+
Document doc = db.parse(request.getInputStream());
|
|
341
|
+
// Default XML parser without disabling external entities -> VULNERABLE
|
|
342
|
+
|
|
343
|
+
FALSE_POSITIVE:
|
|
344
|
+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
|
345
|
+
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
|
346
|
+
DocumentBuilder db = dbf.newDocumentBuilder();
|
|
347
|
+
Document doc = db.parse(request.getInputStream());
|
|
348
|
+
// External entities disabled -> SAFE`;
|
|
349
|
+
case '502': // Deserialization
|
|
350
|
+
return `
|
|
351
|
+
DESERIALIZATION (CWE-502) SPECIFIC:
|
|
352
|
+
- VULNERABLE: ObjectInputStream.readObject() on untrusted data
|
|
353
|
+
- VULNERABLE: XMLDecoder.readObject() on untrusted input
|
|
354
|
+
- VULNERABLE: Yaml.load() without SafeConstructor
|
|
355
|
+
- VULNERABLE: JSON libraries with polymorphic type handling
|
|
356
|
+
- SAFE: Deserialization with allowlist of classes
|
|
357
|
+
- SAFE: SafeYAML, safe JSON configuration
|
|
358
|
+
- Key insight: Any deserialization of untrusted data is dangerous
|
|
359
|
+
|
|
360
|
+
FEW-SHOT EXAMPLES:
|
|
361
|
+
TRUE_POSITIVE:
|
|
362
|
+
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
|
|
363
|
+
Object obj = ois.readObject();
|
|
364
|
+
// Deserializing untrusted HTTP input -> VULNERABLE
|
|
365
|
+
|
|
366
|
+
FALSE_POSITIVE:
|
|
367
|
+
ObjectInputStream ois = new ObjectInputStream(fileInput); // Internal file
|
|
368
|
+
Object obj = ois.readObject();
|
|
369
|
+
// Deserializing from trusted internal source -> Generally SAFE
|
|
370
|
+
// (but depends on whether the file content is user-controlled)`;
|
|
371
|
+
case '94': // Code Injection
|
|
372
|
+
case '95': // Eval Injection
|
|
373
|
+
return `
|
|
374
|
+
CODE INJECTION (CWE-94/95) SPECIFIC:
|
|
375
|
+
- VULNERABLE: ScriptEngine.eval() with user input
|
|
376
|
+
- VULNERABLE: Runtime.exec() with user-controlled command
|
|
377
|
+
- VULNERABLE: Reflection with user-controlled class/method names
|
|
378
|
+
- VULNERABLE: Expression language with user input (OGNL, SpEL, EL)
|
|
379
|
+
- SAFE: Sandboxed script execution with restricted context
|
|
380
|
+
- Key insight: Any dynamic code execution with user input is dangerous
|
|
381
|
+
|
|
382
|
+
FEW-SHOT EXAMPLES:
|
|
383
|
+
TRUE_POSITIVE:
|
|
384
|
+
String expr = request.getParameter("expr");
|
|
385
|
+
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
|
|
386
|
+
engine.eval(expr);
|
|
387
|
+
// User input directly executed as script -> VULNERABLE
|
|
388
|
+
|
|
389
|
+
FALSE_POSITIVE:
|
|
390
|
+
String key = request.getParameter("key");
|
|
391
|
+
String value = config.get(key); // Looking up config by key
|
|
392
|
+
// User controls config key but not code execution -> SAFE`;
|
|
393
|
+
case '327': // Weak Crypto
|
|
394
|
+
case '328': // Reversible One-Way Hash
|
|
395
|
+
return `
|
|
396
|
+
WEAK CRYPTOGRAPHY (CWE-327/328) SPECIFIC:
|
|
397
|
+
- VULNERABLE: MD5, SHA1 for password hashing
|
|
398
|
+
- VULNERABLE: DES, 3DES, RC4 for encryption
|
|
399
|
+
- VULNERABLE: ECB mode for block ciphers
|
|
400
|
+
- SAFE: bcrypt, scrypt, Argon2 for passwords
|
|
401
|
+
- SAFE: AES-GCM, ChaCha20-Poly1305 for encryption`;
|
|
402
|
+
case '352': // CSRF
|
|
403
|
+
return `
|
|
404
|
+
CSRF (CWE-352) SPECIFIC:
|
|
405
|
+
- VULNERABLE: State-changing operations without CSRF token
|
|
406
|
+
- VULNERABLE: POST/PUT/DELETE endpoints without token validation
|
|
407
|
+
- SAFE: CSRF token validated on state-changing requests
|
|
408
|
+
- SAFE: SameSite cookie attributes with proper configuration`;
|
|
409
|
+
case '200': // Information Exposure
|
|
410
|
+
case '209': // Error Information Exposure
|
|
411
|
+
return `
|
|
412
|
+
INFORMATION EXPOSURE (CWE-200/209) SPECIFIC:
|
|
413
|
+
- VULNERABLE: Stack traces shown to users
|
|
414
|
+
- VULNERABLE: Internal paths, database info in error messages
|
|
415
|
+
- VULNERABLE: Debug information in production
|
|
416
|
+
- SAFE: Generic error messages with logged details
|
|
417
|
+
- SAFE: Custom error pages without sensitive info`;
|
|
418
|
+
default:
|
|
419
|
+
return '';
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Make a raw JSON chat completion call
|
|
424
|
+
* Public to allow batch verification and other custom prompts
|
|
425
|
+
* @param phase - Which phase config to use ('enrichment', 'verification', or 'componentEnrichment')
|
|
426
|
+
* @param retryCount - Internal retry counter (don't set manually)
|
|
427
|
+
*/
|
|
428
|
+
async chatJSON(systemPrompt, userPrompt, phase = 'enrichment', retryCount = 0) {
|
|
429
|
+
// Skip if LLM has been disabled due to repeated failures
|
|
430
|
+
if (this.llmDisabled) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
// Use the correct phase config
|
|
434
|
+
const phaseConfig = this.config.phases[phase];
|
|
435
|
+
// On retry, request fewer tokens to reduce truncation risk
|
|
436
|
+
const maxRetries = 1;
|
|
437
|
+
const maxTokens = retryCount > 0
|
|
438
|
+
? Math.min(phaseConfig.maxTokens, 4000) // Reduced tokens on retry
|
|
439
|
+
: phaseConfig.maxTokens;
|
|
440
|
+
try {
|
|
441
|
+
// Create abort controller with timeout
|
|
442
|
+
const controller = new AbortController();
|
|
443
|
+
const timeoutMs = phaseConfig.timeout || 60000;
|
|
444
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
445
|
+
try {
|
|
446
|
+
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
headers: {
|
|
449
|
+
'Content-Type': 'application/json',
|
|
450
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
451
|
+
},
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
model: phaseConfig.model,
|
|
454
|
+
messages: [
|
|
455
|
+
{ role: 'system', content: systemPrompt },
|
|
456
|
+
{ role: 'user', content: userPrompt },
|
|
457
|
+
],
|
|
458
|
+
max_tokens: maxTokens,
|
|
459
|
+
temperature: phaseConfig.temperature,
|
|
460
|
+
}),
|
|
461
|
+
signal: controller.signal,
|
|
462
|
+
});
|
|
463
|
+
clearTimeout(timeoutId);
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
console.error('LLM call failed: HTTP', response.status);
|
|
466
|
+
this.trackFailure();
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
// Success - reset failure counter
|
|
470
|
+
this.resetFailures();
|
|
471
|
+
const data = await response.json();
|
|
472
|
+
const rawContent = data.choices?.[0]?.message?.content;
|
|
473
|
+
// Handle case where LLM returns JSON object directly (not as string)
|
|
474
|
+
if (rawContent && typeof rawContent === 'object') {
|
|
475
|
+
return rawContent;
|
|
476
|
+
}
|
|
477
|
+
const content = typeof rawContent === 'string' ? rawContent : '';
|
|
478
|
+
// Parse JSON from response (handle markdown code blocks)
|
|
479
|
+
let jsonStr = content;
|
|
480
|
+
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
481
|
+
if (jsonMatch) {
|
|
482
|
+
jsonStr = jsonMatch[1];
|
|
483
|
+
}
|
|
484
|
+
// Fix common JSON issues from LLM
|
|
485
|
+
jsonStr = jsonStr.trim();
|
|
486
|
+
// Fix word-numbers (e.g., "0. nine" -> "0.9", "0. Nine" -> "0.9")
|
|
487
|
+
const wordToNum = {
|
|
488
|
+
'zero': '0', 'one': '1', 'two': '2', 'three': '3', 'four': '4',
|
|
489
|
+
'five': '5', 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9',
|
|
490
|
+
};
|
|
491
|
+
jsonStr = jsonStr.replace(/(\d+)\.\s*(zero|one|two|three|four|five|six|seven|eight|nine)/gi, (_, int, word) => `${int}.${wordToNum[word.toLowerCase()]}`);
|
|
492
|
+
// Fix trailing decimal points followed by any non-digit (e.g., 0. -> 0.0)
|
|
493
|
+
jsonStr = jsonStr.replace(/(\d+)\.\s*([,}\]\n\r])/g, '$1.0$2');
|
|
494
|
+
// Fix decimal points followed by space then digit (e.g., 0. 5 -> 0.5)
|
|
495
|
+
jsonStr = jsonStr.replace(/(\d+)\.\s+(\d)/g, '$1.$2');
|
|
496
|
+
// Fix missing quotes around string values that look like identifiers
|
|
497
|
+
jsonStr = jsonStr.replace(/:\s*([A-Z_]+)\s*([,}\]])/g, ': "$1"$2');
|
|
498
|
+
try {
|
|
499
|
+
return JSON.parse(jsonStr);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// Try to recover truncated JSON by completing brackets
|
|
503
|
+
const recovered = this.tryRecoverTruncatedJSON(jsonStr);
|
|
504
|
+
if (recovered) {
|
|
505
|
+
try {
|
|
506
|
+
return JSON.parse(recovered);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Recovery failed, try verification-specific recovery
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Try to extract verification response from truncated JSON
|
|
513
|
+
const verificationRecovered = this.tryRecoverVerificationJSON(jsonStr);
|
|
514
|
+
if (verificationRecovered) {
|
|
515
|
+
return verificationRecovered;
|
|
516
|
+
}
|
|
517
|
+
// Log the problematic JSON for debugging
|
|
518
|
+
console.error('Failed to parse JSON:', jsonStr.substring(0, 100));
|
|
519
|
+
// Retry with reduced tokens if this was the first attempt
|
|
520
|
+
if (retryCount < maxRetries) {
|
|
521
|
+
console.log('Retrying LLM call with reduced tokens...');
|
|
522
|
+
return this.chatJSON(systemPrompt, userPrompt, phase, retryCount + 1);
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch (fetchError) {
|
|
528
|
+
clearTimeout(timeoutId);
|
|
529
|
+
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
530
|
+
console.error(`LLM call timed out (attempt ${retryCount + 1}/${maxRetries + 1})`);
|
|
531
|
+
// Retry on timeout with extended timeout
|
|
532
|
+
if (retryCount < maxRetries) {
|
|
533
|
+
console.log('Retrying LLM call with reduced tokens...');
|
|
534
|
+
return this.chatJSON(systemPrompt, userPrompt, phase, retryCount + 1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
console.error('LLM fetch failed:', fetchError);
|
|
539
|
+
// Retry on network errors if this was the first attempt
|
|
540
|
+
if (retryCount < maxRetries) {
|
|
541
|
+
console.log('Retrying LLM call due to network error...');
|
|
542
|
+
return this.chatJSON(systemPrompt, userPrompt, phase, retryCount + 1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
this.trackFailure();
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
console.error('LLM JSON call failed:', error);
|
|
551
|
+
this.trackFailure();
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Try to recover truncated JSON by completing missing brackets
|
|
557
|
+
* Handles cases where LLM output was cut off mid-response
|
|
558
|
+
*/
|
|
559
|
+
tryRecoverTruncatedJSON(jsonStr) {
|
|
560
|
+
// Count open brackets
|
|
561
|
+
let braces = 0;
|
|
562
|
+
let brackets = 0;
|
|
563
|
+
let inString = false;
|
|
564
|
+
let escaped = false;
|
|
565
|
+
for (const char of jsonStr) {
|
|
566
|
+
if (escaped) {
|
|
567
|
+
escaped = false;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (char === '\\') {
|
|
571
|
+
escaped = true;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (char === '"') {
|
|
575
|
+
inString = !inString;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (inString)
|
|
579
|
+
continue;
|
|
580
|
+
if (char === '{')
|
|
581
|
+
braces++;
|
|
582
|
+
if (char === '}')
|
|
583
|
+
braces--;
|
|
584
|
+
if (char === '[')
|
|
585
|
+
brackets++;
|
|
586
|
+
if (char === ']')
|
|
587
|
+
brackets--;
|
|
588
|
+
}
|
|
589
|
+
// If we're truncated mid-string, close it
|
|
590
|
+
if (inString) {
|
|
591
|
+
jsonStr = jsonStr + '"';
|
|
592
|
+
}
|
|
593
|
+
// Truncate to last complete property (find last comma or colon not in value position)
|
|
594
|
+
// If we ended mid-value, try to find the last complete key-value pair
|
|
595
|
+
const lastValidEnd = this.findLastCompleteValue(jsonStr);
|
|
596
|
+
if (lastValidEnd > 0 && lastValidEnd < jsonStr.length - 1) {
|
|
597
|
+
jsonStr = jsonStr.substring(0, lastValidEnd);
|
|
598
|
+
}
|
|
599
|
+
// Add missing brackets
|
|
600
|
+
while (brackets > 0) {
|
|
601
|
+
jsonStr += ']';
|
|
602
|
+
brackets--;
|
|
603
|
+
}
|
|
604
|
+
while (braces > 0) {
|
|
605
|
+
jsonStr += '}';
|
|
606
|
+
braces--;
|
|
607
|
+
}
|
|
608
|
+
// Quick validation - should start with { or [
|
|
609
|
+
const trimmed = jsonStr.trim();
|
|
610
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
return jsonStr;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Find the position of the last complete JSON value
|
|
617
|
+
*/
|
|
618
|
+
findLastCompleteValue(jsonStr) {
|
|
619
|
+
// Look for patterns that indicate a complete value:
|
|
620
|
+
// - number followed by comma or bracket: 0.9, 0.9] 0.9}
|
|
621
|
+
// - string followed by comma or bracket: "text", "text"] "text"}
|
|
622
|
+
// - boolean/null followed by comma or bracket: true, false] null}
|
|
623
|
+
// - closing bracket followed by comma or bracket: ], ]] ]} }, }] }}
|
|
624
|
+
let lastGoodPos = jsonStr.length;
|
|
625
|
+
let inString = false;
|
|
626
|
+
let escaped = false;
|
|
627
|
+
for (let i = jsonStr.length - 1; i >= 0; i--) {
|
|
628
|
+
const char = jsonStr[i];
|
|
629
|
+
// Handle string detection (going backwards)
|
|
630
|
+
if (char === '"' && !escaped) {
|
|
631
|
+
// Check if this quote is escaped (odd number of preceding backslashes)
|
|
632
|
+
let backslashes = 0;
|
|
633
|
+
for (let j = i - 1; j >= 0 && jsonStr[j] === '\\'; j--) {
|
|
634
|
+
backslashes++;
|
|
635
|
+
}
|
|
636
|
+
if (backslashes % 2 === 0) {
|
|
637
|
+
inString = !inString;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (inString)
|
|
641
|
+
continue;
|
|
642
|
+
// Found a good endpoint - comma, closing bracket not followed by incomplete value
|
|
643
|
+
if (char === ',' || char === ']' || char === '}') {
|
|
644
|
+
// Check what comes before to ensure it's a complete value
|
|
645
|
+
const before = jsonStr.substring(0, i + 1).trim();
|
|
646
|
+
try {
|
|
647
|
+
// Try to parse as partial JSON to validate
|
|
648
|
+
JSON.parse(before + (char === ',' ? '{}]}' : ''));
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// If that fails, this is a good truncation point
|
|
652
|
+
return i + 1;
|
|
653
|
+
}
|
|
654
|
+
return i + 1;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return lastGoodPos;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Try to recover a truncated verification response by extracting key fields
|
|
661
|
+
* Even if reasoning is cut off, we can still use the verdict and confidence
|
|
662
|
+
*/
|
|
663
|
+
tryRecoverVerificationJSON(jsonStr) {
|
|
664
|
+
// Look for verdict pattern
|
|
665
|
+
const verdictMatch = jsonStr.match(/"verdict"\s*:\s*"(TRUE_POSITIVE|FALSE_POSITIVE|UNCERTAIN)"/i);
|
|
666
|
+
if (!verdictMatch)
|
|
667
|
+
return null;
|
|
668
|
+
const verdict = verdictMatch[1].toUpperCase();
|
|
669
|
+
// Look for confidence pattern
|
|
670
|
+
const confidenceMatch = jsonStr.match(/"confidence"\s*:\s*(0?\.\d+|1(?:\.0)?|\d+\.\d+)/);
|
|
671
|
+
const confidence = confidenceMatch ? Math.min(1, Math.max(0, parseFloat(confidenceMatch[1]))) : 0.5;
|
|
672
|
+
// Look for exploitability pattern
|
|
673
|
+
const exploitabilityMatch = jsonStr.match(/"exploitability"\s*:\s*"(high|medium|low|none)"/i);
|
|
674
|
+
const exploitability = exploitabilityMatch ? exploitabilityMatch[1].toLowerCase() : 'none';
|
|
675
|
+
// Extract partial reasoning (up to truncation)
|
|
676
|
+
let reasoning = 'Response truncated';
|
|
677
|
+
const reasoningMatch = jsonStr.match(/"reasoning"\s*:\s*"([^"]*)/);
|
|
678
|
+
if (reasoningMatch && reasoningMatch[1].length > 10) {
|
|
679
|
+
reasoning = reasoningMatch[1] + '... [truncated]';
|
|
680
|
+
}
|
|
681
|
+
// Try to extract sanitizersFound if present
|
|
682
|
+
const sanitizersMatch = jsonStr.match(/"sanitizersFound"\s*:\s*\[(.*?)\]/);
|
|
683
|
+
let sanitizersFound = [];
|
|
684
|
+
if (sanitizersMatch) {
|
|
685
|
+
const sanitizerStrings = sanitizersMatch[1].match(/"([^"]+)"/g);
|
|
686
|
+
if (sanitizerStrings) {
|
|
687
|
+
sanitizersFound = sanitizerStrings.map(s => s.replace(/"/g, ''));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Try to extract attackVector if present
|
|
691
|
+
const attackVectorMatch = jsonStr.match(/"attackVector"\s*:\s*"([^"]*)/);
|
|
692
|
+
const attackVector = attackVectorMatch ? attackVectorMatch[1] : '';
|
|
693
|
+
return {
|
|
694
|
+
verdict,
|
|
695
|
+
confidence,
|
|
696
|
+
reasoning,
|
|
697
|
+
exploitability,
|
|
698
|
+
sanitizersFound,
|
|
699
|
+
attackVector,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Classify the role of a class
|
|
704
|
+
*/
|
|
705
|
+
async classifyRole(input) {
|
|
706
|
+
// Sanitize inputs to prevent prompt injection
|
|
707
|
+
const safeClassName = sanitizeCodeForPrompt(input.className);
|
|
708
|
+
const safeMethodNames = sanitizeCodeForPrompt(input.methodNames);
|
|
709
|
+
const safeAnnotations = sanitizeCodeForPrompt(input.annotations);
|
|
710
|
+
const safeImports = sanitizeCodeForPrompt(input.imports);
|
|
711
|
+
// Log potential injection attempts
|
|
712
|
+
logInjectionAttempt(input.imports, 'classifyRole.imports');
|
|
713
|
+
const model = this.config.phases.enrichment.model;
|
|
714
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code. Respond only in valid JSON format.', model);
|
|
715
|
+
const userPrompt = `Classify the role of this Java class:
|
|
716
|
+
Class: ${safeClassName}
|
|
717
|
+
Methods: ${safeMethodNames}
|
|
718
|
+
Annotations: ${safeAnnotations}
|
|
719
|
+
Imports: ${safeImports}
|
|
720
|
+
|
|
721
|
+
Respond with JSON: {"role": "controller|service|repository|utility|entity|unknown", "confidence": 0.0-1.0, "reasoning": "explanation", "indicators": ["list", "of", "indicators"]}`;
|
|
722
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
723
|
+
const parseResult = RoleClassificationResponseSchema.safeParse(rawResult);
|
|
724
|
+
if (!parseResult.success) {
|
|
725
|
+
console.warn('classifyRole validation failed:', parseResult.error.issues);
|
|
726
|
+
return { role: 'unknown', confidence: 0, reasoning: 'LLM call failed', indicators: [] };
|
|
727
|
+
}
|
|
728
|
+
return parseResult.data;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Discover additional taint sources in a method
|
|
732
|
+
*/
|
|
733
|
+
async discoverSources(input) {
|
|
734
|
+
// Sanitize and truncate inputs to prevent prompt injection and context overflow
|
|
735
|
+
const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
|
|
736
|
+
const safeMethodName = sanitizeCodeForPrompt(input.methodName);
|
|
737
|
+
const safeClassRole = sanitizeCodeForPrompt(input.classRole);
|
|
738
|
+
const safeExistingSources = sanitizeCodeForPrompt(input.existingSources);
|
|
739
|
+
// Log potential injection attempts
|
|
740
|
+
logInjectionAttempt(input.methodCode, 'discoverSources.methodCode');
|
|
741
|
+
const model = this.config.phases.enrichment.model;
|
|
742
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code for taint sources. Respond only in valid JSON format.', model);
|
|
743
|
+
const userPrompt = `Find additional user-controlled input sources in this method:
|
|
744
|
+
|
|
745
|
+
Method: ${safeMethodName}
|
|
746
|
+
Class role: ${safeClassRole}
|
|
747
|
+
Already identified sources: ${safeExistingSources}
|
|
748
|
+
|
|
749
|
+
[CODE START]
|
|
750
|
+
${safeMethodCode}
|
|
751
|
+
[CODE END]
|
|
752
|
+
|
|
753
|
+
Look for: HTTP parameters, headers, cookies, request body, file input, environment variables.
|
|
754
|
+
Ignore: constants, internal config, hardcoded values.
|
|
755
|
+
|
|
756
|
+
Respond with JSON: {"additionalSources": [{"line": 10, "variable": "param", "type": "http_param", "confidence": 0.9, "reasoning": "..."}]}`;
|
|
757
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
758
|
+
const parseResult = SourceDiscoveryResponseSchema.safeParse(rawResult);
|
|
759
|
+
if (!parseResult.success) {
|
|
760
|
+
console.warn('discoverSources validation failed:', parseResult.error.issues);
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
return parseResult.data.additionalSources;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Discover additional taint sinks in a method
|
|
767
|
+
*/
|
|
768
|
+
async discoverSinks(input) {
|
|
769
|
+
// Sanitize and truncate inputs to prevent prompt injection and context overflow
|
|
770
|
+
const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
|
|
771
|
+
const safeMethodName = sanitizeCodeForPrompt(input.methodName);
|
|
772
|
+
const safeMethodCalls = sanitizeCodeForPrompt(input.methodCalls);
|
|
773
|
+
const safeExistingSinks = sanitizeCodeForPrompt(input.existingSinks);
|
|
774
|
+
// Log potential injection attempts
|
|
775
|
+
logInjectionAttempt(input.methodCode, 'discoverSinks.methodCode');
|
|
776
|
+
const model = this.config.phases.enrichment.model;
|
|
777
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code for dangerous sinks. Respond only in valid JSON format.', model);
|
|
778
|
+
const userPrompt = `Find additional dangerous operations (sinks) in this method:
|
|
779
|
+
|
|
780
|
+
Method: ${safeMethodName}
|
|
781
|
+
Method calls: ${safeMethodCalls}
|
|
782
|
+
Already identified sinks: ${safeExistingSinks}
|
|
783
|
+
|
|
784
|
+
[CODE START]
|
|
785
|
+
${safeMethodCode}
|
|
786
|
+
[CODE END]
|
|
787
|
+
|
|
788
|
+
Look for: SQL queries, command execution, file operations, XSS output, deserialization.
|
|
789
|
+
Ignore: PreparedStatement with ?, logging, safe APIs.
|
|
790
|
+
|
|
791
|
+
Respond with JSON: {"additionalSinks": [{"line": 15, "method": "executeQuery", "type": "sql_injection", "cwe": "CWE-89", "argPositions": [0], "confidence": 0.9, "reasoning": "..."}]}`;
|
|
792
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
793
|
+
const parseResult = SinkDiscoveryResponseSchema.safeParse(rawResult);
|
|
794
|
+
if (!parseResult.success) {
|
|
795
|
+
console.warn('discoverSinks validation failed:', parseResult.error.issues);
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
return parseResult.data.additionalSinks;
|
|
799
|
+
}
|
|
800
|
+
// ==========================================================================
|
|
801
|
+
// Language-Aware Methods (for multi-language support)
|
|
802
|
+
// ==========================================================================
|
|
803
|
+
/**
|
|
804
|
+
* Classify role using a custom language-specific prompt
|
|
805
|
+
*/
|
|
806
|
+
async classifyRoleWithPrompt(promptTemplate, input) {
|
|
807
|
+
// Sanitize inputs to prevent prompt injection
|
|
808
|
+
const safeClassName = sanitizeCodeForPrompt(input.className);
|
|
809
|
+
const safeMethodNames = sanitizeCodeForPrompt(input.methodNames);
|
|
810
|
+
const safeAnnotations = sanitizeCodeForPrompt(input.annotations);
|
|
811
|
+
const safeImports = sanitizeCodeForPrompt(input.imports);
|
|
812
|
+
// Log potential injection attempts
|
|
813
|
+
logInjectionAttempt(input.imports, 'classifyRoleWithPrompt.imports');
|
|
814
|
+
const model = this.config.phases.enrichment.model;
|
|
815
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing code. Respond only in valid JSON format.', model);
|
|
816
|
+
const userPrompt = promptTemplate
|
|
817
|
+
.replace('{className}', safeClassName)
|
|
818
|
+
.replace('{methodNames}', safeMethodNames)
|
|
819
|
+
.replace('{annotations}', safeAnnotations)
|
|
820
|
+
.replace('{imports}', safeImports);
|
|
821
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
822
|
+
if (!result) {
|
|
823
|
+
return { role: 'unknown', confidence: 0, reasoning: 'LLM call failed', indicators: [] };
|
|
824
|
+
}
|
|
825
|
+
const role = (result.role || 'unknown').toLowerCase();
|
|
826
|
+
const validRoles = ['controller', 'service', 'repository', 'utility', 'entity', 'unknown'];
|
|
827
|
+
return {
|
|
828
|
+
role: validRoles.includes(role) ? role : 'unknown',
|
|
829
|
+
confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
|
|
830
|
+
reasoning: result.reasoning || '',
|
|
831
|
+
indicators: result.indicators || [],
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Discover sources using a custom language-specific prompt
|
|
836
|
+
*/
|
|
837
|
+
async discoverSourcesWithPrompt(promptTemplate, input) {
|
|
838
|
+
// Sanitize and truncate inputs to prevent prompt injection and context overflow
|
|
839
|
+
const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
|
|
840
|
+
const safeMethodName = sanitizeCodeForPrompt(input.methodName);
|
|
841
|
+
const safeClassRole = sanitizeCodeForPrompt(input.classRole);
|
|
842
|
+
const safeExistingSources = sanitizeCodeForPrompt(input.existingSources);
|
|
843
|
+
// Log potential injection attempts
|
|
844
|
+
logInjectionAttempt(input.methodCode, 'discoverSourcesWithPrompt.methodCode');
|
|
845
|
+
const model = this.config.phases.enrichment.model;
|
|
846
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing code for taint sources. Respond only in valid JSON format.', model);
|
|
847
|
+
const userPrompt = promptTemplate
|
|
848
|
+
.replace('{methodCode}', safeMethodCode)
|
|
849
|
+
.replace('{methodName}', safeMethodName)
|
|
850
|
+
.replace('{classRole}', safeClassRole)
|
|
851
|
+
.replace('{existingSources}', safeExistingSources);
|
|
852
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
853
|
+
if (!result)
|
|
854
|
+
return [];
|
|
855
|
+
return (result.additionalSources || []).map(s => ({
|
|
856
|
+
line: s.line || 0,
|
|
857
|
+
variable: s.variable || '',
|
|
858
|
+
type: s.type || 'unknown',
|
|
859
|
+
confidence: s.confidence || 0.5,
|
|
860
|
+
reasoning: s.reasoning || '',
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Discover sinks using a custom language-specific prompt
|
|
865
|
+
*/
|
|
866
|
+
async discoverSinksWithPrompt(promptTemplate, input) {
|
|
867
|
+
// Sanitize and truncate inputs to prevent prompt injection and context overflow
|
|
868
|
+
const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
|
|
869
|
+
const safeMethodName = sanitizeCodeForPrompt(input.methodName);
|
|
870
|
+
const safeMethodCalls = sanitizeCodeForPrompt(input.methodCalls);
|
|
871
|
+
const safeExistingSinks = sanitizeCodeForPrompt(input.existingSinks);
|
|
872
|
+
// Log potential injection attempts
|
|
873
|
+
logInjectionAttempt(input.methodCode, 'discoverSinksWithPrompt.methodCode');
|
|
874
|
+
const model = this.config.phases.enrichment.model;
|
|
875
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing code for dangerous sinks. Respond only in valid JSON format.', model);
|
|
876
|
+
const userPrompt = promptTemplate
|
|
877
|
+
.replace('{methodCode}', safeMethodCode)
|
|
878
|
+
.replace('{methodName}', safeMethodName)
|
|
879
|
+
.replace('{methodCalls}', safeMethodCalls)
|
|
880
|
+
.replace('{existingSinks}', safeExistingSinks);
|
|
881
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
882
|
+
if (!result)
|
|
883
|
+
return [];
|
|
884
|
+
return (result.additionalSinks || []).map(s => ({
|
|
885
|
+
line: s.line || 0,
|
|
886
|
+
method: s.method || '',
|
|
887
|
+
type: s.type || 'unknown',
|
|
888
|
+
cwe: s.cwe || 'CWE-unknown',
|
|
889
|
+
argPositions: s.argPositions || [0],
|
|
890
|
+
confidence: s.confidence || 0.5,
|
|
891
|
+
reasoning: s.reasoning || '',
|
|
892
|
+
}));
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Resolve virtual/interface method calls to implementations
|
|
896
|
+
*/
|
|
897
|
+
async resolveVirtualCall(input) {
|
|
898
|
+
// Sanitize inputs to prevent prompt injection
|
|
899
|
+
const safeCallExpression = sanitizeCodeForPrompt(input.callExpression);
|
|
900
|
+
const safeInterfaceType = sanitizeCodeForPrompt(input.interfaceType);
|
|
901
|
+
const safeImplementations = sanitizeCodeForPrompt(input.availableImplementations);
|
|
902
|
+
const safeContext = sanitizeCodeForPrompt(input.context);
|
|
903
|
+
// Log potential injection attempts
|
|
904
|
+
logInjectionAttempt(input.context, 'resolveVirtualCall.context');
|
|
905
|
+
const model = this.config.phases.enrichment.model;
|
|
906
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code. Respond only in valid JSON format.', model);
|
|
907
|
+
const userPrompt = `Which implementation is most likely being called?
|
|
908
|
+
|
|
909
|
+
Call: ${safeCallExpression}
|
|
910
|
+
Interface: ${safeInterfaceType}
|
|
911
|
+
Available implementations: ${safeImplementations}
|
|
912
|
+
Context: ${safeContext}
|
|
913
|
+
|
|
914
|
+
Respond with JSON: {"resolvedImplementation": "ClassName", "confidence": 0.9, "reasoning": "..."}`;
|
|
915
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
916
|
+
if (!result) {
|
|
917
|
+
return { resolvedImplementation: '', confidence: 0, reasoning: 'LLM call failed' };
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
resolvedImplementation: result.resolvedImplementation || '',
|
|
921
|
+
confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
|
|
922
|
+
reasoning: result.reasoning || '',
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Verify a batch of discovered patterns
|
|
927
|
+
*/
|
|
928
|
+
async verifyPatterns(input) {
|
|
929
|
+
// Sanitize inputs to prevent prompt injection
|
|
930
|
+
const safePatterns = sanitizeCodeForPrompt(input.patterns);
|
|
931
|
+
const safeCodeContext = sanitizeCodeForPrompt(input.codeContext);
|
|
932
|
+
// Log potential injection attempts
|
|
933
|
+
logInjectionAttempt(input.codeContext, 'verifyPatterns.codeContext');
|
|
934
|
+
const model = this.config.phases.enrichment.model;
|
|
935
|
+
const systemPrompt = formatSystemPrompt('You are a security expert verifying potential taint sources and sinks. Respond only in valid JSON format.', model);
|
|
936
|
+
const userPrompt = `Verify these potential security patterns:
|
|
937
|
+
|
|
938
|
+
${safePatterns}
|
|
939
|
+
|
|
940
|
+
${safeCodeContext ? `Code context:\n[CODE START]\n${safeCodeContext}\n[CODE END]` : ''}
|
|
941
|
+
|
|
942
|
+
For each pattern, determine if it's a valid source/sink. Respond with JSON: {"verifications": [{"method": "...", "class": "...", "isValid": true, "confidence": 0.9, "reasoning": "..."}]}`;
|
|
943
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
944
|
+
if (!result) {
|
|
945
|
+
return { verifications: [] };
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
verifications: (result.verifications || []).map(v => ({
|
|
949
|
+
method: v.method || '',
|
|
950
|
+
class: v.class || '',
|
|
951
|
+
isValid: v.isValid === true,
|
|
952
|
+
confidence: typeof v.confidence === 'number' ? v.confidence : 0.5,
|
|
953
|
+
reasoning: v.reasoning || 'No reasoning provided',
|
|
954
|
+
suggestedType: v.suggestedType,
|
|
955
|
+
suggestedCwe: v.suggestedCwe,
|
|
956
|
+
})),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Run enrichment to discover additional sources/sinks
|
|
961
|
+
*/
|
|
962
|
+
async enrich(input) {
|
|
963
|
+
// Sanitize inputs to prevent prompt injection
|
|
964
|
+
const safeCode = sanitizeCodeForPrompt(input.code);
|
|
965
|
+
const safeClassName = sanitizeCodeForPrompt(input.className);
|
|
966
|
+
const safeMethodName = sanitizeCodeForPrompt(input.methodName);
|
|
967
|
+
const safeSources = sanitizeListForPrompt(input.existingSources);
|
|
968
|
+
const safeSinks = sanitizeListForPrompt(input.existingSinks);
|
|
969
|
+
// Log potential injection attempts
|
|
970
|
+
logInjectionAttempt(input.code, 'enrich.code');
|
|
971
|
+
const model = this.config.phases.enrichment.model;
|
|
972
|
+
const systemPrompt = formatSystemPrompt('You are a security expert analyzing Java code for taint sources and sinks. Respond only in valid JSON format.', model);
|
|
973
|
+
const userPrompt = `Analyze this code for additional sources and sinks:
|
|
974
|
+
|
|
975
|
+
Class: ${safeClassName}
|
|
976
|
+
Method: ${safeMethodName}
|
|
977
|
+
Existing sources: ${safeSources.join(', ') || 'none'}
|
|
978
|
+
Existing sinks: ${safeSinks.join(', ') || 'none'}
|
|
979
|
+
|
|
980
|
+
[CODE START]
|
|
981
|
+
${safeCode}
|
|
982
|
+
[CODE END]
|
|
983
|
+
|
|
984
|
+
Respond with JSON: {"additionalSources": [{"line": 10, "type": "http_param", "variable": "input", "confidence": 0.9}], "additionalSinks": [{"line": 20, "type": "sql_injection", "method": "executeQuery", "cwe": "CWE-89", "confidence": 0.9}], "role": "controller", "reasoning": "..."}`;
|
|
985
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
986
|
+
if (!result) {
|
|
987
|
+
return { additionalSources: [], additionalSinks: [], role: 'unknown', reasoning: 'LLM call failed' };
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
additionalSources: result.additionalSources || [],
|
|
991
|
+
additionalSinks: result.additionalSinks || [],
|
|
992
|
+
role: result.role || 'unknown',
|
|
993
|
+
reasoning: result.reasoning || '',
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Pre-check for obvious sanitization patterns to improve accuracy.
|
|
998
|
+
* Returns null if no obvious pattern is detected (proceed with LLM).
|
|
999
|
+
*/
|
|
1000
|
+
preCheckSanitization(methodCode, cwe, sanitizersInPath) {
|
|
1001
|
+
const lowerCode = methodCode.toLowerCase();
|
|
1002
|
+
// Check explicit sanitizers from static analysis
|
|
1003
|
+
if (sanitizersInPath.length > 0) {
|
|
1004
|
+
const knownSanitizers = sanitizersInPath.filter(s =>
|
|
1005
|
+
// SQL/Injection
|
|
1006
|
+
s.includes('prepareStatement') ||
|
|
1007
|
+
s.includes('setString') ||
|
|
1008
|
+
s.includes('setInt') ||
|
|
1009
|
+
// XSS
|
|
1010
|
+
s.includes('escapeHtml') ||
|
|
1011
|
+
s.includes('encodeForHTML') ||
|
|
1012
|
+
s.includes('htmlEscape') ||
|
|
1013
|
+
// Path validation
|
|
1014
|
+
s.includes('PathUtils') ||
|
|
1015
|
+
s.includes('FilenameUtils') ||
|
|
1016
|
+
s.includes('getCanonicalPath') ||
|
|
1017
|
+
// Allowlist
|
|
1018
|
+
s.includes('RedirectUtils') ||
|
|
1019
|
+
s.includes('verify') ||
|
|
1020
|
+
s.includes('validate'));
|
|
1021
|
+
if (knownSanitizers.length > 0) {
|
|
1022
|
+
return { isSafe: true, reason: `Known sanitizer found: ${knownSanitizers[0]}` };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// CWE-022 (Path Traversal) - Check for path validation patterns
|
|
1026
|
+
if (cwe === 'CWE-022') {
|
|
1027
|
+
// PathUtils validation
|
|
1028
|
+
if ((lowerCode.includes('pathutils.isinvalidpath') ||
|
|
1029
|
+
lowerCode.includes('pathutils.isinvalidencodedpath')) &&
|
|
1030
|
+
lowerCode.includes('throw')) {
|
|
1031
|
+
return { isSafe: true, reason: 'PathUtils validation detected' };
|
|
1032
|
+
}
|
|
1033
|
+
// FilenameUtils
|
|
1034
|
+
if (lowerCode.includes('filenameutils.getname')) {
|
|
1035
|
+
return { isSafe: true, reason: 'FilenameUtils.getName() detected' };
|
|
1036
|
+
}
|
|
1037
|
+
// Canonical path checks (Zip Slip mitigation)
|
|
1038
|
+
if (lowerCode.includes('getcanonicalpath') &&
|
|
1039
|
+
lowerCode.includes('startswith') &&
|
|
1040
|
+
(lowerCode.includes('throw') || lowerCode.includes('return'))) {
|
|
1041
|
+
return { isSafe: true, reason: 'Canonical path check detected (Zip Slip mitigation)' };
|
|
1042
|
+
}
|
|
1043
|
+
// Custom path validation functions
|
|
1044
|
+
if (lowerCode.includes('ensurefilewithinbundledir') ||
|
|
1045
|
+
lowerCode.includes('checkdestinationfilefortraversal')) {
|
|
1046
|
+
return { isSafe: true, reason: 'Custom path validation function detected' };
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// CWE-079 (XSS/Open Redirect) - Check for encoding and validation
|
|
1050
|
+
if (cwe === 'CWE-079') {
|
|
1051
|
+
// HTML encoding
|
|
1052
|
+
if (lowerCode.includes('encodeforhtml') ||
|
|
1053
|
+
lowerCode.includes('escapehtml') ||
|
|
1054
|
+
lowerCode.includes('htmlescape') ||
|
|
1055
|
+
lowerCode.includes('htmlutils.htmlescape') ||
|
|
1056
|
+
lowerCode.includes('esapi.encoder')) {
|
|
1057
|
+
return { isSafe: true, reason: 'HTML encoding function detected' };
|
|
1058
|
+
}
|
|
1059
|
+
// Redirect validation
|
|
1060
|
+
if (lowerCode.includes('redirectutils.verifyredirecturi') ||
|
|
1061
|
+
(lowerCode.includes('verify') && lowerCode.includes('redirect') && lowerCode.includes('uri'))) {
|
|
1062
|
+
return { isSafe: true, reason: 'Redirect URI validation detected' };
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
// CWE-089 (SQL Injection) - Check for prepared statements
|
|
1066
|
+
if (cwe === 'CWE-089' || cwe === 'CWE-89') {
|
|
1067
|
+
if (lowerCode.includes('preparestatement') &&
|
|
1068
|
+
(lowerCode.includes('setstring') || lowerCode.includes('setint') || lowerCode.includes('setlong'))) {
|
|
1069
|
+
return { isSafe: true, reason: 'PreparedStatement with parameter binding detected' };
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// CWE-094 (Code Injection) - Check for safe APIs
|
|
1073
|
+
if (cwe === 'CWE-094') {
|
|
1074
|
+
// SafeConstructor for YAML
|
|
1075
|
+
if (lowerCode.includes('safeconstructor')) {
|
|
1076
|
+
return { isSafe: true, reason: 'SafeConstructor detected (safe YAML deserialization)' };
|
|
1077
|
+
}
|
|
1078
|
+
// ProcessBuilder with array arguments
|
|
1079
|
+
if (lowerCode.includes('processbuilder') &&
|
|
1080
|
+
(lowerCode.includes('new string[]') || lowerCode.includes('arrays.aslist'))) {
|
|
1081
|
+
return { isSafe: true, reason: 'ProcessBuilder with array arguments detected' };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
// CWE-078 (Command Injection) - Check for ProcessBuilder
|
|
1085
|
+
if (cwe === 'CWE-078') {
|
|
1086
|
+
if (lowerCode.includes('processbuilder') &&
|
|
1087
|
+
(lowerCode.includes('new string[]') || lowerCode.includes('arrays.aslist'))) {
|
|
1088
|
+
return { isSafe: true, reason: 'ProcessBuilder with array arguments detected' };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
// No obvious pattern detected
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Verify if a potential vulnerability is exploitable
|
|
1096
|
+
*/
|
|
1097
|
+
async verify(input) {
|
|
1098
|
+
// Pre-check for obvious sanitization patterns
|
|
1099
|
+
const preCheck = this.preCheckSanitization(input.methodCode, input.cwe, input.sanitizersInPath);
|
|
1100
|
+
if (preCheck && preCheck.isSafe) {
|
|
1101
|
+
return {
|
|
1102
|
+
verdict: 'FALSE_POSITIVE',
|
|
1103
|
+
confidence: 0.9,
|
|
1104
|
+
reasoning: `Pre-check detected safe pattern: ${preCheck.reason}`,
|
|
1105
|
+
exploitability: 'none',
|
|
1106
|
+
sanitizersFound: input.sanitizersInPath,
|
|
1107
|
+
attackVector: '',
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// Simple prompt - ask if there's sanitization
|
|
1111
|
+
const systemPrompt = `You verify security findings. Check if data is sanitized between source and sink.
|
|
1112
|
+
|
|
1113
|
+
SANITIZER = a function that transforms data to remove/escape dangerous characters:
|
|
1114
|
+
- Known: encodeForHTML, escapeHtml, PreparedStatement.setString, clean(), sanitize(), encode()
|
|
1115
|
+
- Custom: any function that escapes <>&" or uses parameterized queries
|
|
1116
|
+
|
|
1117
|
+
RULES:
|
|
1118
|
+
1. Default: TRUE_POSITIVE (vulnerable)
|
|
1119
|
+
2. FALSE_POSITIVE only if: sanitizer function is called on tainted data before reaching sink
|
|
1120
|
+
3. If unsure: TRUE_POSITIVE
|
|
1121
|
+
|
|
1122
|
+
JSON only.`;
|
|
1123
|
+
// Sanitize and truncate inputs to prevent prompt injection and context overflow
|
|
1124
|
+
const safeSourceCode = sanitizeCodeForPrompt(input.sourceCode);
|
|
1125
|
+
const safeSinkCode = sanitizeCodeForPrompt(input.sinkCode);
|
|
1126
|
+
const safeMethodCode = truncateCode(sanitizeCodeForPrompt(input.methodCode));
|
|
1127
|
+
const safeClassName = sanitizeCodeForPrompt(input.className);
|
|
1128
|
+
const safeMethodName = sanitizeCodeForPrompt(input.methodName);
|
|
1129
|
+
const safeSanitizers = sanitizeListForPrompt(input.sanitizersInPath);
|
|
1130
|
+
// Log potential injection attempts
|
|
1131
|
+
logInjectionAttempt(input.methodCode, 'verify.methodCode');
|
|
1132
|
+
const userPrompt = `${input.cwe}: Line ${input.sourceLine} → Line ${input.sinkLine}
|
|
1133
|
+
|
|
1134
|
+
\`\`\`java
|
|
1135
|
+
${safeMethodCode}
|
|
1136
|
+
\`\`\`
|
|
1137
|
+
|
|
1138
|
+
Does the tainted data pass through ANY function that escapes/encodes it before reaching the sink?
|
|
1139
|
+
|
|
1140
|
+
Respond: {"verdict": "TRUE_POSITIVE|FALSE_POSITIVE", "confidence": 0.9, "reasoning": "why", "exploitability": "high|low|none", "sanitizersFound": ["function_name"], "attackVector": "attack description"}`;
|
|
1141
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt, 'verification');
|
|
1142
|
+
const parseResult = VerificationResponseSchema.safeParse(rawResult);
|
|
1143
|
+
if (!parseResult.success) {
|
|
1144
|
+
console.warn('verify validation failed:', parseResult.error.issues);
|
|
1145
|
+
return {
|
|
1146
|
+
verdict: 'UNCERTAIN',
|
|
1147
|
+
confidence: 0,
|
|
1148
|
+
reasoning: 'LLM call failed',
|
|
1149
|
+
exploitability: 'none',
|
|
1150
|
+
sanitizersFound: [],
|
|
1151
|
+
attackVector: '',
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
const result = parseResult.data;
|
|
1155
|
+
// Post-processing: FALSE_POSITIVE must have evidence
|
|
1156
|
+
// If LLM says FALSE_POSITIVE but can't cite a specific sanitizer, flip to TRUE_POSITIVE
|
|
1157
|
+
let finalVerdict = result.verdict;
|
|
1158
|
+
if (result.verdict === 'FALSE_POSITIVE') {
|
|
1159
|
+
const hasSanitizerEvidence = result.sanitizersFound.length > 0 ||
|
|
1160
|
+
result.reasoning.toLowerCase().includes('preparedstatement') ||
|
|
1161
|
+
result.reasoning.toLowerCase().includes('encodefor') ||
|
|
1162
|
+
result.reasoning.toLowerCase().includes('escapehtml') ||
|
|
1163
|
+
result.reasoning.toLowerCase().includes('htmlescape') ||
|
|
1164
|
+
result.reasoning.toLowerCase().includes('esapi') ||
|
|
1165
|
+
result.reasoning.toLowerCase().includes('parameterized') ||
|
|
1166
|
+
result.reasoning.toLowerCase().includes('constant') ||
|
|
1167
|
+
result.reasoning.toLowerCase().includes('hardcoded') ||
|
|
1168
|
+
result.reasoning.toLowerCase().includes('literal');
|
|
1169
|
+
if (!hasSanitizerEvidence) {
|
|
1170
|
+
// No clear sanitizer evidence - flip to TRUE_POSITIVE
|
|
1171
|
+
finalVerdict = 'TRUE_POSITIVE';
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
verdict: finalVerdict,
|
|
1176
|
+
confidence: result.confidence,
|
|
1177
|
+
reasoning: result.reasoning,
|
|
1178
|
+
exploitability: result.exploitability,
|
|
1179
|
+
sanitizersFound: result.sanitizersFound,
|
|
1180
|
+
attackVector: result.attackVector,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Analyze list operations to track taint through index changes
|
|
1185
|
+
*/
|
|
1186
|
+
async analyzeListIndices(input) {
|
|
1187
|
+
// Sanitize inputs to prevent prompt injection
|
|
1188
|
+
const safeCode = sanitizeCodeForPrompt(input.code);
|
|
1189
|
+
const safeListVariable = sanitizeCodeForPrompt(input.listVariable);
|
|
1190
|
+
const safeOperations = sanitizeListForPrompt(input.operations);
|
|
1191
|
+
// Log potential injection attempts
|
|
1192
|
+
logInjectionAttempt(input.code, 'analyzeListIndices.code');
|
|
1193
|
+
const model = this.config.phases.enrichment.model;
|
|
1194
|
+
const systemPrompt = formatSystemPrompt(`You are a security expert analyzing list/array operations. Track how indices shift when elements are added or removed.
|
|
1195
|
+
|
|
1196
|
+
CRITICAL: When list.remove(i) is called, ALL indices > i shift down by 1.
|
|
1197
|
+
Example: [a, TAINTED, c] after remove(0) becomes [TAINTED, c] - taint is now at index 0!
|
|
1198
|
+
|
|
1199
|
+
Respond only in valid JSON format.`, model);
|
|
1200
|
+
const userPrompt = `Analyze list operations for variable "${safeListVariable}":
|
|
1201
|
+
|
|
1202
|
+
Code:
|
|
1203
|
+
[CODE START]
|
|
1204
|
+
${safeCode}
|
|
1205
|
+
[CODE END]
|
|
1206
|
+
|
|
1207
|
+
Operations in order: ${safeOperations.join(', ')}
|
|
1208
|
+
|
|
1209
|
+
Track each element through all add/remove operations. Determine which indices contain tainted values after ALL operations complete.
|
|
1210
|
+
|
|
1211
|
+
Respond with JSON: {"finalIndexMapping": {"0": {"value": "name", "tainted": true}, "1": {"value": "safe", "tainted": false}}, "taintedIndices": [0], "reasoning": "step-by-step trace"}`;
|
|
1212
|
+
const result = await this.chatJSON(systemPrompt, userPrompt);
|
|
1213
|
+
if (!result) {
|
|
1214
|
+
return { finalIndexMapping: {}, taintedIndices: [], reasoning: 'LLM call failed' };
|
|
1215
|
+
}
|
|
1216
|
+
// Convert string keys to numbers
|
|
1217
|
+
const mapping = {};
|
|
1218
|
+
for (const [key, val] of Object.entries(result.finalIndexMapping || {})) {
|
|
1219
|
+
mapping[parseInt(key, 10)] = val;
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
finalIndexMapping: mapping,
|
|
1223
|
+
taintedIndices: result.taintedIndices || [],
|
|
1224
|
+
reasoning: result.reasoning || '',
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Analyze correlated predicates to detect related conditions
|
|
1229
|
+
*/
|
|
1230
|
+
async analyzeCorrelatedPredicates(input) {
|
|
1231
|
+
const gen = this.getGenerator('correlatedPredicate', correlatedPredicateSignature, {
|
|
1232
|
+
description: 'Identify predicates that are correlated (use same variables, imply each other)',
|
|
1233
|
+
});
|
|
1234
|
+
// Convert to JSON string for compatibility
|
|
1235
|
+
const result = await gen.forward(this.ai, {
|
|
1236
|
+
code: input.code,
|
|
1237
|
+
predicateLocationsJson: JSON.stringify(input.predicateLocations),
|
|
1238
|
+
});
|
|
1239
|
+
// Parse JSON response
|
|
1240
|
+
let correlatedGroups = [];
|
|
1241
|
+
try {
|
|
1242
|
+
const groupsJson = result.correlatedGroupsJson || '[]';
|
|
1243
|
+
const parsed = JSON.parse(groupsJson.replace(/```json\n?|\n?```/g, '').trim());
|
|
1244
|
+
correlatedGroups = Array.isArray(parsed) ? parsed : [];
|
|
1245
|
+
}
|
|
1246
|
+
catch {
|
|
1247
|
+
correlatedGroups = [];
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
correlatedGroups,
|
|
1251
|
+
reasoning: result.reasoning || '',
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Track taint across file boundaries
|
|
1256
|
+
* Uses direct chat completion for better compatibility with local LLMs
|
|
1257
|
+
*/
|
|
1258
|
+
async analyzeCrossFileTaint(input) {
|
|
1259
|
+
// Sanitize inputs to prevent prompt injection
|
|
1260
|
+
const safeSourceFile = sanitizeCodeForPrompt(input.sourceFile);
|
|
1261
|
+
const safeSourceCode = sanitizeCodeForPrompt(input.sourceCode);
|
|
1262
|
+
const safeTargetFile = sanitizeCodeForPrompt(input.targetFile);
|
|
1263
|
+
const safeTargetCode = sanitizeCodeForPrompt(input.targetCode);
|
|
1264
|
+
const safeImportedSymbols = sanitizeListForPrompt(input.importedSymbols);
|
|
1265
|
+
// Log potential injection attempts
|
|
1266
|
+
logInjectionAttempt(input.sourceCode, 'analyzeCrossFileTaint.sourceCode');
|
|
1267
|
+
logInjectionAttempt(input.targetCode, 'analyzeCrossFileTaint.targetCode');
|
|
1268
|
+
// Use direct chat completion for better compatibility
|
|
1269
|
+
const prompt = `Analyze taint flow between these two files:
|
|
1270
|
+
|
|
1271
|
+
SOURCE FILE: ${safeSourceFile}
|
|
1272
|
+
[CODE START]
|
|
1273
|
+
${safeSourceCode}
|
|
1274
|
+
[CODE END]
|
|
1275
|
+
|
|
1276
|
+
TARGET FILE: ${safeTargetFile}
|
|
1277
|
+
[CODE START]
|
|
1278
|
+
${safeTargetCode}
|
|
1279
|
+
[CODE END]
|
|
1280
|
+
|
|
1281
|
+
TAINTED DATA EXPORTED FROM SOURCE:
|
|
1282
|
+
${JSON.stringify(input.exportedTaint, null, 2)}
|
|
1283
|
+
|
|
1284
|
+
SYMBOLS IMPORTED BY TARGET:
|
|
1285
|
+
${safeImportedSymbols.join(', ')}
|
|
1286
|
+
|
|
1287
|
+
Does tainted data flow from the source file to the target file? If yes, trace the flow.
|
|
1288
|
+
${input.candidateContext ? `\nCANDIDATE TAINT PATHS (verify these):\n${input.candidateContext}\n` : ''}
|
|
1289
|
+
Respond in this exact JSON format:
|
|
1290
|
+
{
|
|
1291
|
+
"taintFlows": [
|
|
1292
|
+
{
|
|
1293
|
+
"sourceSymbol": "variable/method that exports taint",
|
|
1294
|
+
"targetSymbol": "variable/method that receives taint",
|
|
1295
|
+
"flowType": "direct|transitive|conditional",
|
|
1296
|
+
"confidence": 0.0-1.0
|
|
1297
|
+
}
|
|
1298
|
+
],
|
|
1299
|
+
"reasoning": "Brief explanation of the taint flow analysis"
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
If no taint flows exist, return: {"taintFlows": [], "reasoning": "No cross-file taint detected"}`;
|
|
1303
|
+
try {
|
|
1304
|
+
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
1305
|
+
method: 'POST',
|
|
1306
|
+
headers: {
|
|
1307
|
+
'Content-Type': 'application/json',
|
|
1308
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
1309
|
+
},
|
|
1310
|
+
body: JSON.stringify({
|
|
1311
|
+
model: this.config.phases.verification.model,
|
|
1312
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1313
|
+
max_tokens: 2000,
|
|
1314
|
+
temperature: 0.1,
|
|
1315
|
+
}),
|
|
1316
|
+
});
|
|
1317
|
+
if (!response.ok) {
|
|
1318
|
+
return { taintFlows: [], reasoning: `LLM request failed: ${response.status}` };
|
|
1319
|
+
}
|
|
1320
|
+
const data = await response.json();
|
|
1321
|
+
const rawContent = data.choices?.[0]?.message?.content;
|
|
1322
|
+
// Handle both string and object content (some providers return structured output directly)
|
|
1323
|
+
let parsed = null;
|
|
1324
|
+
if (typeof rawContent === 'object' && rawContent !== null && !Array.isArray(rawContent)) {
|
|
1325
|
+
// Content is already a structured object
|
|
1326
|
+
parsed = rawContent;
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
// Content is a string - need to parse JSON from it
|
|
1330
|
+
let content;
|
|
1331
|
+
if (typeof rawContent === 'string') {
|
|
1332
|
+
content = rawContent;
|
|
1333
|
+
}
|
|
1334
|
+
else if (Array.isArray(rawContent)) {
|
|
1335
|
+
content = rawContent.map((block) => {
|
|
1336
|
+
if (typeof block === 'string')
|
|
1337
|
+
return block;
|
|
1338
|
+
if (typeof block === 'object' && block !== null && 'text' in block) {
|
|
1339
|
+
return block.text;
|
|
1340
|
+
}
|
|
1341
|
+
return JSON.stringify(block);
|
|
1342
|
+
}).join('');
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
content = String(rawContent || '');
|
|
1346
|
+
}
|
|
1347
|
+
// Extract JSON from response
|
|
1348
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
1349
|
+
if (jsonMatch) {
|
|
1350
|
+
try {
|
|
1351
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1352
|
+
}
|
|
1353
|
+
catch {
|
|
1354
|
+
// JSON parse failed
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (!parsed) {
|
|
1359
|
+
return { taintFlows: [], reasoning: 'Could not parse LLM response' };
|
|
1360
|
+
}
|
|
1361
|
+
const taintFlows = Array.isArray(parsed.taintFlows)
|
|
1362
|
+
? parsed.taintFlows.filter((f) => {
|
|
1363
|
+
const flow = f;
|
|
1364
|
+
return typeof flow === 'object' &&
|
|
1365
|
+
flow !== null &&
|
|
1366
|
+
typeof flow.sourceSymbol === 'string' &&
|
|
1367
|
+
typeof flow.targetSymbol === 'string';
|
|
1368
|
+
})
|
|
1369
|
+
: [];
|
|
1370
|
+
return {
|
|
1371
|
+
taintFlows,
|
|
1372
|
+
reasoning: parsed.reasoning || '',
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
catch (error) {
|
|
1376
|
+
return { taintFlows: [], reasoning: `Error: ${error instanceof Error ? error.message : String(error)}` };
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Test connection to LLM
|
|
1381
|
+
*/
|
|
1382
|
+
async testConnection() {
|
|
1383
|
+
try {
|
|
1384
|
+
// Simple raw API call to test connectivity
|
|
1385
|
+
const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
1386
|
+
method: 'POST',
|
|
1387
|
+
headers: {
|
|
1388
|
+
'Content-Type': 'application/json',
|
|
1389
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
1390
|
+
},
|
|
1391
|
+
body: JSON.stringify({
|
|
1392
|
+
model: this.config.phases.enrichment.model,
|
|
1393
|
+
messages: [{ role: 'user', content: 'Say OK' }],
|
|
1394
|
+
max_tokens: 10,
|
|
1395
|
+
}),
|
|
1396
|
+
});
|
|
1397
|
+
if (!response.ok) {
|
|
1398
|
+
console.error('LLM connection test failed: HTTP', response.status);
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
const data = await response.json();
|
|
1402
|
+
const content = data.choices?.[0]?.message?.content || '';
|
|
1403
|
+
return content.toLowerCase().includes('ok');
|
|
1404
|
+
}
|
|
1405
|
+
catch (error) {
|
|
1406
|
+
console.error('LLM connection test failed:', error);
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Get current configuration
|
|
1412
|
+
*/
|
|
1413
|
+
getConfig() {
|
|
1414
|
+
return this.config;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Get the AI service instance
|
|
1418
|
+
*/
|
|
1419
|
+
getAI() {
|
|
1420
|
+
return this.ai;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Get enrichment-specific configuration
|
|
1424
|
+
*/
|
|
1425
|
+
getEnrichmentConfig() {
|
|
1426
|
+
return this.config.enrichment;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Get verification-specific configuration
|
|
1430
|
+
*/
|
|
1431
|
+
getVerificationConfig() {
|
|
1432
|
+
return this.config.verification;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Get phase-specific configuration
|
|
1436
|
+
*/
|
|
1437
|
+
getPhaseConfig(phase) {
|
|
1438
|
+
return this.config.phases[phase];
|
|
1439
|
+
}
|
|
1440
|
+
// ==========================================================================
|
|
1441
|
+
// Language-Aware Methods
|
|
1442
|
+
// ==========================================================================
|
|
1443
|
+
/**
|
|
1444
|
+
* Classify role using language-specific prompt
|
|
1445
|
+
*/
|
|
1446
|
+
async classifyRoleForLanguage(language, input) {
|
|
1447
|
+
const model = this.config.phases.enrichment.model;
|
|
1448
|
+
const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
|
|
1449
|
+
const userPrompt = getPrompt(language, 'classifyRole', {
|
|
1450
|
+
moduleName: sanitizeCodeForPrompt(input.moduleName),
|
|
1451
|
+
className: sanitizeCodeForPrompt(input.moduleName), // Java compat
|
|
1452
|
+
functionNames: sanitizeCodeForPrompt(input.functionNames),
|
|
1453
|
+
methodNames: sanitizeCodeForPrompt(input.functionNames), // Java compat
|
|
1454
|
+
attributes: sanitizeCodeForPrompt(input.attributes),
|
|
1455
|
+
annotations: sanitizeCodeForPrompt(input.attributes), // Java compat
|
|
1456
|
+
uses: sanitizeCodeForPrompt(input.uses),
|
|
1457
|
+
imports: sanitizeCodeForPrompt(input.uses), // Java compat
|
|
1458
|
+
});
|
|
1459
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
1460
|
+
const parseResult = RoleClassificationResponseSchema.safeParse(rawResult);
|
|
1461
|
+
if (!parseResult.success) {
|
|
1462
|
+
return { role: 'unknown', confidence: 0, reasoning: 'LLM call failed', indicators: [] };
|
|
1463
|
+
}
|
|
1464
|
+
return parseResult.data;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Discover sources using language-specific prompt
|
|
1468
|
+
*/
|
|
1469
|
+
async discoverSourcesForLanguage(language, input) {
|
|
1470
|
+
const safeCode = truncateCode(sanitizeCodeForPrompt(input.code));
|
|
1471
|
+
const model = this.config.phases.enrichment.model;
|
|
1472
|
+
const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
|
|
1473
|
+
const userPrompt = getPrompt(language, 'discoverSources', {
|
|
1474
|
+
code: safeCode,
|
|
1475
|
+
functionName: sanitizeCodeForPrompt(input.functionName),
|
|
1476
|
+
methodName: sanitizeCodeForPrompt(input.functionName), // Java compat
|
|
1477
|
+
moduleRole: sanitizeCodeForPrompt(input.moduleRole),
|
|
1478
|
+
classRole: sanitizeCodeForPrompt(input.moduleRole), // Java compat
|
|
1479
|
+
existingSources: sanitizeCodeForPrompt(input.existingSources),
|
|
1480
|
+
});
|
|
1481
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
1482
|
+
const parseResult = SourceDiscoveryResponseSchema.safeParse(rawResult);
|
|
1483
|
+
if (!parseResult.success) {
|
|
1484
|
+
return [];
|
|
1485
|
+
}
|
|
1486
|
+
return parseResult.data.additionalSources;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Discover sinks using language-specific prompt
|
|
1490
|
+
*/
|
|
1491
|
+
async discoverSinksForLanguage(language, input) {
|
|
1492
|
+
const safeCode = truncateCode(sanitizeCodeForPrompt(input.code));
|
|
1493
|
+
const model = this.config.phases.enrichment.model;
|
|
1494
|
+
const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
|
|
1495
|
+
const userPrompt = getPrompt(language, 'discoverSinks', {
|
|
1496
|
+
code: safeCode,
|
|
1497
|
+
functionName: sanitizeCodeForPrompt(input.functionName),
|
|
1498
|
+
methodName: sanitizeCodeForPrompt(input.functionName), // Java compat
|
|
1499
|
+
methodCalls: sanitizeCodeForPrompt(input.methodCalls),
|
|
1500
|
+
existingSinks: sanitizeCodeForPrompt(input.existingSinks),
|
|
1501
|
+
});
|
|
1502
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
1503
|
+
const parseResult = SinkDiscoveryResponseSchema.safeParse(rawResult);
|
|
1504
|
+
if (!parseResult.success) {
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1507
|
+
return parseResult.data.additionalSinks;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Verify vulnerability using language-specific prompt
|
|
1511
|
+
*/
|
|
1512
|
+
async verifyForLanguage(language, input) {
|
|
1513
|
+
const safeFunctionCode = truncateCode(sanitizeCodeForPrompt(input.functionCode));
|
|
1514
|
+
const model = this.config.phases.verification.model;
|
|
1515
|
+
const systemPrompt = formatSystemPrompt(getPrompt(language, 'system'), model);
|
|
1516
|
+
const userPrompt = getPrompt(language, 'verify', {
|
|
1517
|
+
cwe: input.cwe,
|
|
1518
|
+
sourceLine: String(input.sourceLine),
|
|
1519
|
+
sourceCode: sanitizeCodeForPrompt(input.sourceCode),
|
|
1520
|
+
sinkLine: String(input.sinkLine),
|
|
1521
|
+
sinkCode: sanitizeCodeForPrompt(input.sinkCode),
|
|
1522
|
+
functionCode: safeFunctionCode,
|
|
1523
|
+
methodCode: safeFunctionCode, // Java compat
|
|
1524
|
+
functionName: sanitizeCodeForPrompt(input.functionName),
|
|
1525
|
+
methodName: sanitizeCodeForPrompt(input.functionName), // Java compat
|
|
1526
|
+
moduleName: sanitizeCodeForPrompt(input.moduleName),
|
|
1527
|
+
className: sanitizeCodeForPrompt(input.moduleName), // Java compat
|
|
1528
|
+
sanitizers: input.sanitizers.join(', ') || 'none',
|
|
1529
|
+
});
|
|
1530
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt);
|
|
1531
|
+
const parseResult = VerificationResponseSchema.safeParse(rawResult);
|
|
1532
|
+
if (!parseResult.success) {
|
|
1533
|
+
return {
|
|
1534
|
+
verdict: 'UNCERTAIN',
|
|
1535
|
+
confidence: 0,
|
|
1536
|
+
reasoning: 'LLM call failed',
|
|
1537
|
+
exploitability: 'none',
|
|
1538
|
+
sanitizersFound: [],
|
|
1539
|
+
attackVector: '',
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
return parseResult.data;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Generate Specifica specification from code evidence
|
|
1546
|
+
*/
|
|
1547
|
+
async generateSpecification(input) {
|
|
1548
|
+
const model = this.config.phases.enrichment.model;
|
|
1549
|
+
const systemPrompt = formatSystemPrompt('You are a technical specification writer. Analyze code and generate requirements specifications in JSON format.', model);
|
|
1550
|
+
// Build concise description of the code
|
|
1551
|
+
const apiSummary = input.apiEndpoints.length > 0
|
|
1552
|
+
? `\nAPI Endpoints:\n${input.apiEndpoints.slice(0, 5).map(e => `- ${e.method} ${e.path} (${e.handler})`).join('\n')}`
|
|
1553
|
+
: '';
|
|
1554
|
+
const methodSummary = input.methods.length > 0
|
|
1555
|
+
? `\nMethods:\n${input.methods.slice(0, 10).map(m => `- ${m.signature}`).join('\n')}`
|
|
1556
|
+
: '';
|
|
1557
|
+
const vulnSummary = input.vulnerabilities.length > 0
|
|
1558
|
+
? `\nSecurity Issues:\n${input.vulnerabilities.map(v => `- ${v.cwe}: ${v.description}`).join('\n')}`
|
|
1559
|
+
: '';
|
|
1560
|
+
const userPrompt = `Generate a requirements specification for this code:
|
|
1561
|
+
|
|
1562
|
+
**File**: ${sanitizeCodeForPrompt(input.filePath)}
|
|
1563
|
+
**Language**: ${input.language}
|
|
1564
|
+
${input.framework ? `**Framework**: ${input.framework}` : ''}
|
|
1565
|
+
**Lines of Code**: ${input.linesOfCode}${apiSummary}${methodSummary}${vulnSummary}
|
|
1566
|
+
|
|
1567
|
+
Generate a JSON specification answering: "What does this code do?"
|
|
1568
|
+
|
|
1569
|
+
Output this exact JSON structure:
|
|
1570
|
+
{
|
|
1571
|
+
"title": "Feature Name",
|
|
1572
|
+
"summary": "One-paragraph description of what this feature does",
|
|
1573
|
+
"requirements": [
|
|
1574
|
+
{
|
|
1575
|
+
"id": "REQ-001",
|
|
1576
|
+
"description": "Testable requirement as a checkbox item",
|
|
1577
|
+
"category": "functional|security|performance|usability",
|
|
1578
|
+
"priority": "must|should|could",
|
|
1579
|
+
"tested": false
|
|
1580
|
+
}
|
|
1581
|
+
],
|
|
1582
|
+
"edgeCases": [
|
|
1583
|
+
"Edge case 1",
|
|
1584
|
+
"Edge case 2"
|
|
1585
|
+
],
|
|
1586
|
+
"errorScenarios": [
|
|
1587
|
+
"Error scenario 1",
|
|
1588
|
+
"Error scenario 2"
|
|
1589
|
+
]
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
Guidelines:
|
|
1593
|
+
1. Requirements must be testable - each should be something you can write a test for
|
|
1594
|
+
2. Focus on WHAT, not HOW - describe behavior, not implementation
|
|
1595
|
+
3. Extract from code behavior - infer requirements from what the code actually does
|
|
1596
|
+
4. Include security requirements based on detected sources/sinks/vulnerabilities
|
|
1597
|
+
5. Identify edge cases - unusual inputs, error conditions, boundary cases
|
|
1598
|
+
6. Error scenarios - what can go wrong?`;
|
|
1599
|
+
const rawResult = await this.chatJSON(systemPrompt, userPrompt, 'enrichment');
|
|
1600
|
+
if (!rawResult) {
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
// Validate basic structure
|
|
1604
|
+
if (typeof rawResult !== 'object' || rawResult === null) {
|
|
1605
|
+
console.warn('generateSpecification: Invalid response structure');
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1608
|
+
const spec = rawResult;
|
|
1609
|
+
if (!spec.title || !spec.summary || !Array.isArray(spec.requirements)) {
|
|
1610
|
+
console.warn('generateSpecification: Missing required fields');
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
// Ensure requirements have proper structure
|
|
1614
|
+
const requirements = spec.requirements.map((req, index) => ({
|
|
1615
|
+
id: req.id || `REQ-${String(index + 1).padStart(3, '0')}`,
|
|
1616
|
+
description: req.description || 'No description provided',
|
|
1617
|
+
category: ['functional', 'security', 'performance', 'usability'].includes(req.category)
|
|
1618
|
+
? req.category
|
|
1619
|
+
: 'functional',
|
|
1620
|
+
priority: ['must', 'should', 'could'].includes(req.priority)
|
|
1621
|
+
? req.priority
|
|
1622
|
+
: 'should',
|
|
1623
|
+
tested: Boolean(req.tested),
|
|
1624
|
+
}));
|
|
1625
|
+
return {
|
|
1626
|
+
title: spec.title,
|
|
1627
|
+
summary: spec.summary,
|
|
1628
|
+
requirements,
|
|
1629
|
+
edgeCases: Array.isArray(spec.edgeCases) ? spec.edgeCases : [],
|
|
1630
|
+
errorScenarios: Array.isArray(spec.errorScenarios) ? spec.errorScenarios : [],
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Get a new Ax-LLM client instance
|
|
1636
|
+
* Always creates a fresh instance for per-request isolation
|
|
1637
|
+
*/
|
|
1638
|
+
export function getAxLLMClient(config) {
|
|
1639
|
+
return new AxLLMClient(config);
|
|
1640
|
+
}
|
|
1641
|
+
//# sourceMappingURL=ax-client.js.map
|