agentdiscover 2.9.0__tar.gz → 2.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/PKG-INFO +29 -8
  2. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/README.md +28 -7
  3. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/pyproject.toml +1 -1
  4. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/known_apps.py +7 -2
  5. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/platform.py +32 -15
  6. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_platform_enrichment.py +104 -0
  7. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/uv.lock +1 -1
  8. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/.claude/commands/grow-scanner.md +0 -0
  9. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/.github/workflows/aibom.yml +0 -0
  10. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/.github/workflows/ci.yml +0 -0
  11. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/.github/workflows/scan.yml +0 -0
  12. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/.gitignore +0 -0
  13. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/.python-version +0 -0
  14. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/CHANGELOG.md +0 -0
  15. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/CLAUDE.md +0 -0
  16. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/COMMIT_NOTES.md +0 -0
  17. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/CONTRIBUTING.md +0 -0
  18. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/Dockerfile +0 -0
  19. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/LICENSE +0 -0
  20. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/README_BADGES.txt +0 -0
  21. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/RELEASE_NOTES.md +0 -0
  22. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/ROADMAP.md +0 -0
  23. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/SECURITY.md +0 -0
  24. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/action.yml +0 -0
  25. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/k8s/agents-configmap.yaml +0 -0
  26. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/k8s/crewai-agent.yaml +0 -0
  27. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/k8s/langchain-agent.yaml +0 -0
  28. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/k8s/shadow-agent.yaml +0 -0
  29. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/mcp-governance/DESIGN.md +0 -0
  30. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/mcp-governance/README.md +0 -0
  31. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/mcp-governance/sample-policy.yaml +0 -0
  32. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/run-demo.sh +0 -0
  33. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/sample-repo/agents/autogen_agent.py +0 -0
  34. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/sample-repo/agents/crewai_agent.py +0 -0
  35. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/sample-repo/agents/direct_client.py +0 -0
  36. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/sample-repo/agents/langchain_agent.py +0 -0
  37. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/sample-repo/requirements.txt +0 -0
  38. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/demo/setup.sh +0 -0
  39. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/deployment/kubernetes/tracing-policy.yaml +0 -0
  40. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/deployment/systemd/README.md +0 -0
  41. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/deployment/systemd/agent-discover-scanner.service +0 -0
  42. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docker-compose.yml +0 -0
  43. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/EXAMPLES.md +0 -0
  44. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/TETRAGON_SETUP.md +0 -0
  45. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/architecture.svg +0 -0
  46. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/comparisons/vs-grep.md +0 -0
  47. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/comparisons/vs-nudge-security.md +0 -0
  48. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/ghost-detection.svg +0 -0
  49. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/integrations/elastic.md +0 -0
  50. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/integrations/github-actions.md +0 -0
  51. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/integrations/kubernetes-daemonset.md +0 -0
  52. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/integrations/splunk.md +0 -0
  53. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/known-apps-example.txt +0 -0
  54. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/layer4-setup.md +0 -0
  55. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/docs/nist-mapping.md +0 -0
  56. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/examples/k8s/test-workload.yaml +0 -0
  57. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/examples/tetragon-sample-events.json +0 -0
  58. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/install.sh +0 -0
  59. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/__init__.py +0 -0
  60. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/aibom.py +0 -0
  61. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/audit_reports.py +0 -0
  62. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/behavioral_patterns.py +0 -0
  63. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/cli.py +0 -0
  64. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/correlator.py +0 -0
  65. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/__init__.py +0 -0
  66. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/cloud_audit/__init__.py +0 -0
  67. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/cloud_audit/aws_cloudtrail.py +0 -0
  68. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/cloud_audit/azure_monitor.py +0 -0
  69. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/cloud_audit/base.py +0 -0
  70. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/cloud_audit/gcp_audit.py +0 -0
  71. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/cloudtrail.py +0 -0
  72. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/sse_proxy/__init__.py +0 -0
  73. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/sse_proxy/base.py +0 -0
  74. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/sse_proxy/netskope.py +0 -0
  75. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/sse_proxy/prisma_access.py +0 -0
  76. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/detectors/sse_proxy/zscaler_zia.py +0 -0
  77. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/errors.py +0 -0
  78. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/exporters/__init__.py +0 -0
  79. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/exporters/mcpfw_policy.py +0 -0
  80. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/git_scanner.py +0 -0
  81. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/high_risk_agents.py +0 -0
  82. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/__init__.py +0 -0
  83. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/base.py +0 -0
  84. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/sse/__init__.py +0 -0
  85. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/sse/netskope.py +0 -0
  86. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/sse/prisma_access.py +0 -0
  87. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/sse/umbrella.py +0 -0
  88. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/interceptors/sse/zscaler.py +0 -0
  89. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/js_signatures.py +0 -0
  90. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/layer4/__init__.py +0 -0
  91. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/layer4/osquery_executor.py +0 -0
  92. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/layer4/osquery_queries.py +0 -0
  93. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/layer4/result_parser.py +0 -0
  94. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/macos_detector.py +0 -0
  95. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/mcp_detector.py +0 -0
  96. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/models/endpoint_discovery.py +0 -0
  97. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/monitors/__init__.py +0 -0
  98. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/monitors/json_output.py +0 -0
  99. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/monitors/k8s_monitor.py +0 -0
  100. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/monitors/tetragon_events.py +0 -0
  101. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/monitors/tetragon_monitor.py +0 -0
  102. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/monitors/vendor_mapping.py +0 -0
  103. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/network_monitor.py +0 -0
  104. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/process_introspection.py +0 -0
  105. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/reports/layer4_report.py +0 -0
  106. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/saas_detector.py +0 -0
  107. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/sarif_output.py +0 -0
  108. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/sbom_analyzer.py +0 -0
  109. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/scan_runner.py +0 -0
  110. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/scanner.py +0 -0
  111. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/signatures.py +0 -0
  112. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/visitor.py +0 -0
  113. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/src/agent_discover_scanner/windows_detector.py +0 -0
  114. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/__init__.py +0 -0
  115. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/autogen_safe.py +0 -0
  116. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/autogen_unsafe.py +0 -0
  117. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/bedrock_safe.py +0 -0
  118. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/bedrock_unsafe.py +0 -0
  119. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/clean_code.py +0 -0
  120. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/crewai_safe.py +0 -0
  121. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/crewai_unsafe.py +0 -0
  122. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/langchain_agents.py +0 -0
  123. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/langchain_js_agent.js +0 -0
  124. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/langgraph_workflow.py +0 -0
  125. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/safe_with_gateway.py +0 -0
  126. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/shadow_anthropic.py +0 -0
  127. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/shadow_openai.py +0 -0
  128. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/fixtures/shadow_openai_js.js +0 -0
  129. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_aibom.py +0 -0
  130. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_audit_bundle.py +0 -0
  131. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_bedrock_signatures.py +0 -0
  132. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_behavioral_patterns.py +0 -0
  133. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_cloud_audit_layer5.py +0 -0
  134. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_cloudtrail.py +0 -0
  135. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_cloudtrail_wiring.py +0 -0
  136. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_correlator.py +0 -0
  137. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_correlator_layer5.py +0 -0
  138. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_forward_dns.py +0 -0
  139. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_git_scanner.py +0 -0
  140. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_install.sh +0 -0
  141. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_interceptors.py +0 -0
  142. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_l2_process_introspection.py +0 -0
  143. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_mcpfw_export.py +0 -0
  144. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_network_monitor.py +0 -0
  145. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_scanner.py +0 -0
  146. {agentdiscover-2.9.0 → agentdiscover-2.9.2}/tests/test_sse_proxy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentdiscover
3
- Version: 2.9.0
3
+ Version: 2.9.2
4
4
  Summary: Discover every AI agent in your infrastructure. 5-layer detection: static analysis, network monitoring, eBPF/Kubernetes runtime, endpoint, and cloud audit (CloudTrail). Company-level correlation. AIBOM export. MCP server detection.
5
5
  Project-URL: Homepage, https://defendai.ai
6
6
  Project-URL: Documentation, https://github.com/Defend-AI-Tech-Inc/agent-discover-scanner/blob/main/README.md
@@ -372,12 +372,20 @@ Passive observation of outbound connections to AI providers — OpenAI, Anthropi
372
372
 
373
373
  Real scan output:
374
374
  ```
375
- [DETECT] Google AI connection from Mail (PID: 776) → generativelanguage.googleapis.com:993
376
375
  [DETECT] OpenAI connection from Microsoft Edge Helper (PID: 4172) → api.openai.com:443
377
376
  [DETECT] Anthropic connection from Cursor Helper (PID: 61436) → api.anthropic.com:443
378
377
  [DETECT] OpenAI connection from OneDrive (PID: 96089) → api.openai.com:443
379
378
  ```
380
379
 
380
+ **Forward-DNS correlation — reliable Bedrock detection without a CIDR list.**
381
+ AWS Bedrock Runtime endpoints rotate across hundreds of generic EC2 IPs (e.g. `ec2-52-94-238-10.compute-1.amazonaws.com`). Reverse-DNS alone cannot recover the service name. At startup, Layer 2 proactively resolves all known LLM hostnames — including all regional Bedrock runtime and agent-runtime variants — and caches the resulting `IP → hostname` mappings with a short TTL. When psutil reports a connection to `52.94.238.10`, the forward-DNS cache immediately recovers `bedrock-runtime.us-east-1.amazonaws.com` and classifies the connection correctly, even as the IP rotates.
382
+
383
+ **Port filter — eliminates false positives from shared IP space.**
384
+ External LLM APIs use HTTPS exclusively. Connections to provider-adjacent IPs on non-HTTPS ports (e.g. port 993 IMAP on a Google IP) are dropped before classification. Private IPs (Ollama, local inference servers, internal proxies) are exempt from this filter.
385
+
386
+ **Process introspection — framework detection at runtime.**
387
+ When Layer 2 observes a live connection, it resolves the process cmdline to its Python entry script and runs the same L1 detection rules (DAI001–DAI007) against that file in-process. If a framework is identified (LangChain, CrewAI, AutoGen, etc.), the agent is immediately promoted to **CONFIRMED** — no prior static code scan and no CloudTrail required. Each finding carries a `framework_confidence` of `"high"` (signal in the entry script directly) or `"medium"` (signal in a sibling project file), and the resolved `entry_script` path for identity reconciliation with any parallel L1 scan.
388
+
381
389
  ### Layer 5 — Cloud Audit (v2.7.0+)
382
390
 
383
391
  **Why Layer 2 misses AWS Bedrock.** AWS Bedrock Runtime endpoints rotate across hundreds of generic EC2 IPs with no published CIDR ranges and no stable reverse-DNS pattern. Passive socket monitoring (psutil) can observe the TCP connection but cannot reliably identify it as Bedrock without a complete, continuously-updated IP allowlist — which does not exist publicly. On VPC endpoints, traffic stays inside the AWS network and never appears on the host's socket table at all.
@@ -549,7 +557,16 @@ Scans developer machines, CI/CD runners, and workstations via osquery. Finds ins
549
557
 
550
558
  After all layers run, the correlator builds a unified agent identity. An agent seen in code (Layer 1), confirmed running in K8s (Layer 3), and observed making network calls (Layer 2) is a single correlated identity — not three separate findings.
551
559
 
552
- Agents present at runtime with **no Layer 1 match** become GHOST agents.
560
+ **CONFIRMED paths** (any of the following is sufficient):
561
+ - Layer 1 (code) + Layer 2 (live network connection)
562
+ - Layer 1 (code) + Layer 3 (K8s / eBPF runtime event)
563
+ - Layer 1 (code) + Layer 4 (osquery endpoint) + Layer 2 (network)
564
+ - Layer 1 (code) + Layer 5 (CloudTrail Bedrock invocation or SSE proxy event)
565
+ - **Layer 2 alone** — when process introspection identifies a framework from the running process entry script, a live connection is sufficient for CONFIRMED without a prior static scan
566
+
567
+ Agents present at runtime with **no Layer 1 match** and no framework identified by process introspection become GHOST agents.
568
+
569
+ The `entry_script` field carries the resolved Python entry-script path from Layer 2 process introspection. When both a static Layer 1 finding and a Layer 2 runtime finding exist for the same file, the platform merges them into a single agent record rather than treating them as two separate orphans.
553
570
 
554
571
  ### SaaS blast radius detection (v2.3.0+)
555
572
 
@@ -713,13 +730,17 @@ When connected to the platform, each scan triggers the **correlation engine** wh
713
730
  - **Cross-machine intelligence** — agent seen on 3 machines and crossed from dev into prod? Automatic risk escalation.
714
731
  - **SaaS blast radius** — platform aggregates confirmed SaaS connections across all scans and computes blast radius score.
715
732
 
716
- **What the scanner sends (v2.8.1+):**
733
+ **What the scanner sends (v2.9.0+):**
717
734
 
718
- The upload payload now includes full Layer 2 and Layer 5 telemetry, not just Layer 1 code findings:
735
+ The upload payload includes full Layer 2 and Layer 5 telemetry, not just Layer 1 code findings:
719
736
 
720
737
  | Field | Source | Contents |
721
738
  |---|---|---|
722
739
  | `agents[]` | All layers | Per-agent metadata including classification, risk, SaaS connections |
740
+ | `agents[].detected_hosts` | Layer 2 ForwardDNSCache | DNS-correlated LLM/provider hostnames (e.g. `bedrock-runtime.us-east-1.amazonaws.com`) recovered before psutil classification — reliable even as provider IPs rotate |
741
+ | `agents[].evidence` | Correlator | Ordered confirmation-source tags: `layer1_code_scan`, `layer2_network`, `layer2_process_introspection`, `layer2_dns_host`, `layer3_k8s_runtime`, `layer4_endpoint`, `layer5_cloudtrail`, `layer5_sse_proxy` |
742
+ | `agents[].entry_script` | Layer 2 process introspection | Absolute path to the Python entry script resolved from the OS process cmdline — used to reconcile L1 `code_file` identity with the L2 runtime finding so the same agent is not reported as two orphans |
743
+ | `agents[].framework_confidence` | Layer 2 process introspection | `"high"` = framework signal found in the entry script directly · `"medium"` = found in a sibling project file · `null` = not detected |
723
744
  | `agents[].metadata.bedrock_invocations` | Layer 5 CloudTrail | Number of Bedrock API calls attributed to this agent |
724
745
  | `agents[].metadata.models_called` | Layer 5 CloudTrail | Model IDs invoked (e.g. `anthropic.claude-3-5-sonnet-20241022-v2:0`) |
725
746
  | `agents[].metadata.iam_users` | Layer 5 CloudTrail | IAM principals observed making calls |
@@ -947,12 +968,12 @@ Full Kubernetes setup: `install.sh` handles Helm, runtime monitoring setup, and
947
968
 
948
969
  ## DefendAI platform
949
970
 
950
- AgentDiscover Scanner is the **discovery layer** of the DefendAI platform.
971
+ agentscanner is the **discovery layer** of the DefendAI platform.
951
972
 
952
973
  | Component | Status | Description |
953
974
  | ------------------------- | -------------- | --------------------------------------------------------------------- |
954
- | **AgentDiscover Scanner** | ✅ Open Source (v2.8.1) | Discover and classify AI agents across your environment |
955
- | **defendai-agent** | 🧪 Beta | MITM proxy for real-time AI traffic inspection and policy enforcement |
975
+ | **agentscanner** | ✅ Open Source (v2.9.0) | Discover and classify AI agents across your environment |
976
+ | **defendai-sensor** | 🧪 Beta | MITM proxy for real-time AI traffic inspection and policy enforcement |
956
977
  | **Correlation Engine** | ✅ Available | Cross-machine identity resolution and behavioral drift detection |
957
978
  | **Policy Engine** | 🚧 Coming Soon | Define and enforce agent behavior rules |
958
979
  | **DefendAI Platform** | 💼 Enterprise | Full lifecycle governance for autonomous AI |
@@ -329,12 +329,20 @@ Passive observation of outbound connections to AI providers — OpenAI, Anthropi
329
329
 
330
330
  Real scan output:
331
331
  ```
332
- [DETECT] Google AI connection from Mail (PID: 776) → generativelanguage.googleapis.com:993
333
332
  [DETECT] OpenAI connection from Microsoft Edge Helper (PID: 4172) → api.openai.com:443
334
333
  [DETECT] Anthropic connection from Cursor Helper (PID: 61436) → api.anthropic.com:443
335
334
  [DETECT] OpenAI connection from OneDrive (PID: 96089) → api.openai.com:443
336
335
  ```
337
336
 
337
+ **Forward-DNS correlation — reliable Bedrock detection without a CIDR list.**
338
+ AWS Bedrock Runtime endpoints rotate across hundreds of generic EC2 IPs (e.g. `ec2-52-94-238-10.compute-1.amazonaws.com`). Reverse-DNS alone cannot recover the service name. At startup, Layer 2 proactively resolves all known LLM hostnames — including all regional Bedrock runtime and agent-runtime variants — and caches the resulting `IP → hostname` mappings with a short TTL. When psutil reports a connection to `52.94.238.10`, the forward-DNS cache immediately recovers `bedrock-runtime.us-east-1.amazonaws.com` and classifies the connection correctly, even as the IP rotates.
339
+
340
+ **Port filter — eliminates false positives from shared IP space.**
341
+ External LLM APIs use HTTPS exclusively. Connections to provider-adjacent IPs on non-HTTPS ports (e.g. port 993 IMAP on a Google IP) are dropped before classification. Private IPs (Ollama, local inference servers, internal proxies) are exempt from this filter.
342
+
343
+ **Process introspection — framework detection at runtime.**
344
+ When Layer 2 observes a live connection, it resolves the process cmdline to its Python entry script and runs the same L1 detection rules (DAI001–DAI007) against that file in-process. If a framework is identified (LangChain, CrewAI, AutoGen, etc.), the agent is immediately promoted to **CONFIRMED** — no prior static code scan and no CloudTrail required. Each finding carries a `framework_confidence` of `"high"` (signal in the entry script directly) or `"medium"` (signal in a sibling project file), and the resolved `entry_script` path for identity reconciliation with any parallel L1 scan.
345
+
338
346
  ### Layer 5 — Cloud Audit (v2.7.0+)
339
347
 
340
348
  **Why Layer 2 misses AWS Bedrock.** AWS Bedrock Runtime endpoints rotate across hundreds of generic EC2 IPs with no published CIDR ranges and no stable reverse-DNS pattern. Passive socket monitoring (psutil) can observe the TCP connection but cannot reliably identify it as Bedrock without a complete, continuously-updated IP allowlist — which does not exist publicly. On VPC endpoints, traffic stays inside the AWS network and never appears on the host's socket table at all.
@@ -506,7 +514,16 @@ Scans developer machines, CI/CD runners, and workstations via osquery. Finds ins
506
514
 
507
515
  After all layers run, the correlator builds a unified agent identity. An agent seen in code (Layer 1), confirmed running in K8s (Layer 3), and observed making network calls (Layer 2) is a single correlated identity — not three separate findings.
508
516
 
509
- Agents present at runtime with **no Layer 1 match** become GHOST agents.
517
+ **CONFIRMED paths** (any of the following is sufficient):
518
+ - Layer 1 (code) + Layer 2 (live network connection)
519
+ - Layer 1 (code) + Layer 3 (K8s / eBPF runtime event)
520
+ - Layer 1 (code) + Layer 4 (osquery endpoint) + Layer 2 (network)
521
+ - Layer 1 (code) + Layer 5 (CloudTrail Bedrock invocation or SSE proxy event)
522
+ - **Layer 2 alone** — when process introspection identifies a framework from the running process entry script, a live connection is sufficient for CONFIRMED without a prior static scan
523
+
524
+ Agents present at runtime with **no Layer 1 match** and no framework identified by process introspection become GHOST agents.
525
+
526
+ The `entry_script` field carries the resolved Python entry-script path from Layer 2 process introspection. When both a static Layer 1 finding and a Layer 2 runtime finding exist for the same file, the platform merges them into a single agent record rather than treating them as two separate orphans.
510
527
 
511
528
  ### SaaS blast radius detection (v2.3.0+)
512
529
 
@@ -670,13 +687,17 @@ When connected to the platform, each scan triggers the **correlation engine** wh
670
687
  - **Cross-machine intelligence** — agent seen on 3 machines and crossed from dev into prod? Automatic risk escalation.
671
688
  - **SaaS blast radius** — platform aggregates confirmed SaaS connections across all scans and computes blast radius score.
672
689
 
673
- **What the scanner sends (v2.8.1+):**
690
+ **What the scanner sends (v2.9.0+):**
674
691
 
675
- The upload payload now includes full Layer 2 and Layer 5 telemetry, not just Layer 1 code findings:
692
+ The upload payload includes full Layer 2 and Layer 5 telemetry, not just Layer 1 code findings:
676
693
 
677
694
  | Field | Source | Contents |
678
695
  |---|---|---|
679
696
  | `agents[]` | All layers | Per-agent metadata including classification, risk, SaaS connections |
697
+ | `agents[].detected_hosts` | Layer 2 ForwardDNSCache | DNS-correlated LLM/provider hostnames (e.g. `bedrock-runtime.us-east-1.amazonaws.com`) recovered before psutil classification — reliable even as provider IPs rotate |
698
+ | `agents[].evidence` | Correlator | Ordered confirmation-source tags: `layer1_code_scan`, `layer2_network`, `layer2_process_introspection`, `layer2_dns_host`, `layer3_k8s_runtime`, `layer4_endpoint`, `layer5_cloudtrail`, `layer5_sse_proxy` |
699
+ | `agents[].entry_script` | Layer 2 process introspection | Absolute path to the Python entry script resolved from the OS process cmdline — used to reconcile L1 `code_file` identity with the L2 runtime finding so the same agent is not reported as two orphans |
700
+ | `agents[].framework_confidence` | Layer 2 process introspection | `"high"` = framework signal found in the entry script directly · `"medium"` = found in a sibling project file · `null` = not detected |
680
701
  | `agents[].metadata.bedrock_invocations` | Layer 5 CloudTrail | Number of Bedrock API calls attributed to this agent |
681
702
  | `agents[].metadata.models_called` | Layer 5 CloudTrail | Model IDs invoked (e.g. `anthropic.claude-3-5-sonnet-20241022-v2:0`) |
682
703
  | `agents[].metadata.iam_users` | Layer 5 CloudTrail | IAM principals observed making calls |
@@ -904,12 +925,12 @@ Full Kubernetes setup: `install.sh` handles Helm, runtime monitoring setup, and
904
925
 
905
926
  ## DefendAI platform
906
927
 
907
- AgentDiscover Scanner is the **discovery layer** of the DefendAI platform.
928
+ agentscanner is the **discovery layer** of the DefendAI platform.
908
929
 
909
930
  | Component | Status | Description |
910
931
  | ------------------------- | -------------- | --------------------------------------------------------------------- |
911
- | **AgentDiscover Scanner** | ✅ Open Source (v2.8.1) | Discover and classify AI agents across your environment |
912
- | **defendai-agent** | 🧪 Beta | MITM proxy for real-time AI traffic inspection and policy enforcement |
932
+ | **agentscanner** | ✅ Open Source (v2.9.0) | Discover and classify AI agents across your environment |
933
+ | **defendai-sensor** | 🧪 Beta | MITM proxy for real-time AI traffic inspection and policy enforcement |
913
934
  | **Correlation Engine** | ✅ Available | Cross-machine identity resolution and behavioral drift detection |
914
935
  | **Policy Engine** | 🚧 Coming Soon | Define and enforce agent behavior rules |
915
936
  | **DefendAI Platform** | 💼 Enterprise | Full lifecycle governance for autonomous AI |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentdiscover"
7
- version = "2.9.0"
7
+ version = "2.9.2"
8
8
  description = "Discover every AI agent in your infrastructure. 5-layer detection: static analysis, network monitoring, eBPF/Kubernetes runtime, endpoint, and cloud audit (CloudTrail). Company-level correlation. AIBOM export. MCP server detection."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -123,13 +123,18 @@ def load_platform_known_apps(
123
123
  except Exception:
124
124
  pass
125
125
  else:
126
- # Urllib fallback when SDK not installed
126
+ # Urllib fallback when SDK not installed.
127
+ # Use the same auth scheme as /scanner/ingest: Authorization: Bearer {api_key}
128
+ # X-DefendAI-Tenant-Token is included only when a tenant token is available.
127
129
  try:
128
130
  import urllib.request
129
131
 
132
+ _headers: dict = {"Authorization": f"Bearer {api_key}"}
133
+ if tenant_token:
134
+ _headers["X-DefendAI-Tenant-Token"] = tenant_token
130
135
  req = urllib.request.Request(
131
136
  f"{wawsdb_url.rstrip('/')}/scanner/config/known-apps",
132
- headers={"X-API-Key": api_key},
137
+ headers=_headers,
133
138
  )
134
139
  with urllib.request.urlopen(req, timeout=5) as resp:
135
140
  data = json.loads(resp.read().decode())
@@ -215,14 +215,17 @@ def load_credentials() -> Optional[Dict[str, str]]:
215
215
  tenant_token = data.get("tenant_token") or data.get("tenant-token")
216
216
  wawsdb_url = data.get("wawsdb_url") or data.get("wawsdb-url")
217
217
 
218
- if not (api_key and tenant_token and wawsdb_url):
218
+ # Only api_key is required; tenant_token and wawsdb_url are optional.
219
+ # A config with just api_key (and optionally wawsdb_url) is valid.
220
+ if not api_key:
219
221
  return None
220
222
 
221
- return {
222
- "api_key": str(api_key),
223
- "tenant_token": str(tenant_token),
224
- "wawsdb_url": str(wawsdb_url),
225
- }
223
+ result: dict[str, str] = {"api_key": str(api_key)}
224
+ if tenant_token:
225
+ result["tenant_token"] = str(tenant_token)
226
+ if wawsdb_url:
227
+ result["wawsdb_url"] = str(wawsdb_url)
228
+ return result
226
229
  except Exception:
227
230
  # Never propagate errors from credential loading
228
231
  return None
@@ -485,28 +488,39 @@ def upload_scan_results(
485
488
  print(f"⚠️ Platform upload failed: {reason} (scan still succeeded)")
486
489
  return False
487
490
 
488
- # Resolve credentials
491
+ # Resolve credentials.
492
+ #
493
+ # tenant_token is OPTIONAL — the API key alone is sufficient to authenticate.
494
+ # When a tenant_token is also provided it is forwarded as X-DefendAI-Tenant-Token
495
+ # for backwards-compatibility with multi-tenant deployments, but it is never
496
+ # required. This means free-tier users can pass just --api-key and have the
497
+ # upload work without a second credential.
498
+ #
499
+ # Resolution order:
500
+ # 1. CLI arguments (api_key / tenant_token)
501
+ # 2. ~/.defendai/config
502
+ # 3. Built-in sandbox defaults (only when no api_key is found at all)
489
503
  source = "sandbox"
490
504
  resolved_api_key = api_key
491
505
  resolved_tenant_token = tenant_token
492
506
  resolved_wawsdb_url = wawsdb_url or _DEFAULT_WAWSDB_URL
493
507
 
494
- if resolved_api_key and resolved_tenant_token:
508
+ if resolved_api_key:
509
+ # api_key alone is enough; tenant_token is supplemental
495
510
  source = "cli"
496
511
  else:
497
512
  cfg = load_credentials()
498
513
  if cfg:
499
- # Only override from config when CLI values are missing
500
514
  if not resolved_api_key:
501
515
  resolved_api_key = cfg.get("api_key")
502
516
  if not resolved_tenant_token:
503
517
  resolved_tenant_token = cfg.get("tenant_token")
504
518
  resolved_wawsdb_url = cfg.get("wawsdb_url") or resolved_wawsdb_url
505
- if resolved_api_key and resolved_tenant_token:
519
+ if resolved_api_key:
506
520
  source = "config"
507
521
 
508
- if not resolved_api_key or not resolved_tenant_token:
509
- # Fall back to sandbox defaults
522
+ if not resolved_api_key:
523
+ # No api_key found anywhere — fall back to sandbox for preview
510
524
  resolved_api_key = _SANDBOX_API_KEY
511
525
  resolved_tenant_token = _SANDBOX_TENANT_TOKEN
512
526
  resolved_wawsdb_url = resolved_wawsdb_url or _DEFAULT_WAWSDB_URL
@@ -518,7 +532,7 @@ def upload_scan_results(
518
532
  elif source == "config":
519
533
  print("✓ Uploading to DefendAI platform (from ~/.defendai/config)...")
520
534
  else:
521
- print("💡 No credentials provided — uploading to DefendAI sandbox for preview")
535
+ print("💡 No API key provided — uploading to DefendAI sandbox for preview")
522
536
 
523
537
  if mcp_result is None:
524
538
  try:
@@ -622,10 +636,13 @@ def upload_scan_results(
622
636
  else:
623
637
  print("⚠️ Large scan result truncated for upload")
624
638
 
625
- headers = {
626
- "X-DefendAI-Tenant-Token": resolved_tenant_token,
639
+ # X-DefendAI-Tenant-Token is included only when a tenant token is available.
640
+ # The Authorization: Bearer header carries the api_key and is always present.
641
+ headers: dict[str, str] = {
627
642
  "Authorization": f"Bearer {resolved_api_key}",
628
643
  }
644
+ if resolved_tenant_token:
645
+ headers["X-DefendAI-Tenant-Token"] = resolved_tenant_token
629
646
 
630
647
  try:
631
648
  with httpx.Client(timeout=30.0, verify=certifi.where()) as client: # type: ignore[call-arg]
@@ -7,6 +7,10 @@ Tests for platform.py enrichment functions added in v2.8.0 / v2.9.0:
7
7
  - format_agents_for_upload (L5 enrichment + credential_files_found + L2 enrichment)
8
8
  """
9
9
 
10
+ import json
11
+ import os
12
+ import tempfile
13
+
10
14
  import pytest
11
15
 
12
16
  from agent_discover_scanner.platform import (
@@ -15,6 +19,7 @@ from agent_discover_scanner.platform import (
15
19
  build_cloud_audit_summary,
16
20
  build_network_summary,
17
21
  format_agents_for_upload,
22
+ load_credentials,
18
23
  )
19
24
 
20
25
 
@@ -558,3 +563,102 @@ class TestFormatAgentsL2Enrichment:
558
563
  agent = self._fmt(report)[0]
559
564
  assert "evidence" in agent
560
565
  assert isinstance(agent["evidence"], list)
566
+
567
+
568
+ # ── TestLoadCredentials ──────────────────────────────────────────────────────
569
+
570
+ class TestLoadCredentials:
571
+ """
572
+ load_credentials() unit tests.
573
+
574
+ Verifies that:
575
+ - api_key alone (without tenant_token) is sufficient to return credentials
576
+ - tenant_token and wawsdb_url are optional fields
577
+ - Missing api_key always returns None
578
+ - Malformed / missing config file returns None cleanly
579
+ """
580
+
581
+ def _write_config(self, tmpdir: str, data: dict) -> str:
582
+ path = os.path.join(tmpdir, ".defendai", "config")
583
+ os.makedirs(os.path.dirname(path), exist_ok=True)
584
+ with open(path, "w") as f:
585
+ json.dump(data, f)
586
+ return path
587
+
588
+ def _load(self, config_path: str):
589
+ """Patch the expanduser path and call load_credentials()."""
590
+ import agent_discover_scanner.platform as plat
591
+ original = plat.os.path.expanduser
592
+ plat.os.path.expanduser = lambda p: config_path if "config" in p else original(p)
593
+ try:
594
+ return load_credentials()
595
+ finally:
596
+ plat.os.path.expanduser = original
597
+
598
+ def test_api_key_and_all_fields_returns_full_dict(self):
599
+ with tempfile.TemporaryDirectory() as tmp:
600
+ cfg = self._write_config(tmp, {
601
+ "api_key": "key-abc",
602
+ "tenant_token": "tok-xyz",
603
+ "wawsdb_url": "https://custom.example.com",
604
+ })
605
+ result = self._load(cfg)
606
+ assert result is not None
607
+ assert result["api_key"] == "key-abc"
608
+ assert result["tenant_token"] == "tok-xyz"
609
+ assert result["wawsdb_url"] == "https://custom.example.com"
610
+
611
+ def test_api_key_only_returns_credentials(self):
612
+ """tenant_token absence must NOT prevent credential loading."""
613
+ with tempfile.TemporaryDirectory() as tmp:
614
+ cfg = self._write_config(tmp, {"api_key": "key-abc"})
615
+ result = self._load(cfg)
616
+ assert result is not None
617
+ assert result["api_key"] == "key-abc"
618
+ assert "tenant_token" not in result
619
+
620
+ def test_api_key_and_url_no_tenant_token(self):
621
+ with tempfile.TemporaryDirectory() as tmp:
622
+ cfg = self._write_config(tmp, {
623
+ "api_key": "key-abc",
624
+ "wawsdb_url": "https://custom.example.com",
625
+ })
626
+ result = self._load(cfg)
627
+ assert result is not None
628
+ assert result["api_key"] == "key-abc"
629
+ assert result["wawsdb_url"] == "https://custom.example.com"
630
+ assert "tenant_token" not in result
631
+
632
+ def test_missing_api_key_returns_none(self):
633
+ with tempfile.TemporaryDirectory() as tmp:
634
+ cfg = self._write_config(tmp, {
635
+ "tenant_token": "tok-xyz",
636
+ "wawsdb_url": "https://custom.example.com",
637
+ })
638
+ result = self._load(cfg)
639
+ assert result is None
640
+
641
+ def test_empty_config_returns_none(self):
642
+ with tempfile.TemporaryDirectory() as tmp:
643
+ cfg = self._write_config(tmp, {})
644
+ result = self._load(cfg)
645
+ assert result is None
646
+
647
+ def test_api_key_hyphen_variant_accepted(self):
648
+ """Both api_key and api-key spellings should work."""
649
+ with tempfile.TemporaryDirectory() as tmp:
650
+ cfg = self._write_config(tmp, {"api-key": "key-hyphen"})
651
+ result = self._load(cfg)
652
+ assert result is not None
653
+ assert result["api_key"] == "key-hyphen"
654
+
655
+ def test_missing_config_file_returns_none(self):
656
+ """A path that doesn't exist must return None without raising."""
657
+ import agent_discover_scanner.platform as plat
658
+ original = plat.os.path.expanduser
659
+ plat.os.path.expanduser = lambda p: "/nonexistent/path/config"
660
+ try:
661
+ result = load_credentials()
662
+ finally:
663
+ plat.os.path.expanduser = original
664
+ assert result is None
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
6
6
  name = "agentdiscover"
7
- version = "2.8.1"
7
+ version = "2.9.2"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "certifi" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes