aictem 0.1.0__py3-none-any.whl
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.
- aictem/__init__.py +3 -0
- aictem/__main__.py +106 -0
- aictem/catalog.py +313 -0
- aictem/detectors/__init__.py +50 -0
- aictem/detectors/code.py +528 -0
- aictem/detectors/deps.py +334 -0
- aictem/detectors/infra.py +206 -0
- aictem/detectors/mcp.py +189 -0
- aictem/detectors/models.py +122 -0
- aictem/detectors/secrets.py +49 -0
- aictem/detectors/synth.py +155 -0
- aictem/gitinfo.py +102 -0
- aictem/model.py +139 -0
- aictem/osv.py +68 -0
- aictem/output.py +124 -0
- aictem/scan.py +100 -0
- aictem/upload.py +48 -0
- aictem/walker.py +65 -0
- aictem-0.1.0.dist-info/METADATA +136 -0
- aictem-0.1.0.dist-info/RECORD +23 -0
- aictem-0.1.0.dist-info/WHEEL +4 -0
- aictem-0.1.0.dist-info/entry_points.txt +2 -0
- aictem-0.1.0.dist-info/licenses/LICENSE +21 -0
aictem/__init__.py
ADDED
aictem/__main__.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""aictem command line.
|
|
2
|
+
|
|
3
|
+
aictem discover [PATH] inventory a repository: packages, containers, cloud, CI and AI assets, with findings
|
|
4
|
+
aictem discover . --format json -o r.json
|
|
5
|
+
aictem discover . --upload send the inventory to the CTEM platform (CTEM_URL, CTEM_CI_TOKEN)
|
|
6
|
+
aictem discover . --vulns also look up known vulnerabilities in pinned packages (OSV)
|
|
7
|
+
aictem discover . --fail-on high exit 1 if a finding at or above the severity is present
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from aictem import __version__, output, scan
|
|
18
|
+
from aictem.model import SEVERITY_RANK
|
|
19
|
+
|
|
20
|
+
EXIT_FINDINGS, EXIT_UPLOAD_GATE, EXIT_ERROR = 1, 3, 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(prog="aictem", description="Discover repository and AI assets and send them to the CTEM platform.")
|
|
25
|
+
parser.add_argument("--version", action="version", version=f"aictem {__version__}")
|
|
26
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
27
|
+
d = sub.add_parser("discover", help="inventory a directory tree", description="Inventory a repository: dependencies, container images, cloud "
|
|
28
|
+
"resources, CI pipelines, secrets, and AI assets (providers, frameworks, agents, RAG, MCP servers, models).")
|
|
29
|
+
d.add_argument("path", nargs="?", default=".", help="directory to scan (default: current directory)")
|
|
30
|
+
d.add_argument("-f", "--format", choices=("table", "json", "sarif"), default="table", help="output format (json is the upload format)")
|
|
31
|
+
d.add_argument("-o", "--output", help="write the report to a file instead of standard output")
|
|
32
|
+
d.add_argument("--approved-providers", help="comma-separated AI provider keys; others are reported as shadow AI (for example openai,anthropic)")
|
|
33
|
+
d.add_argument("--no-secrets", action="store_true", help="skip the committed-credential scan")
|
|
34
|
+
d.add_argument("--vulns", action="store_true", help="look up known vulnerabilities in pinned packages at osv.dev (sends names and versions)")
|
|
35
|
+
d.add_argument("--exclude", action="append", default=[], metavar="GLOB", help="skip paths matching a glob (repeatable; also read from .aictemignore)")
|
|
36
|
+
d.add_argument("--max-files", type=int, default=50_000, help="stop after this many files (default 50000)")
|
|
37
|
+
d.add_argument("--fail-on", choices=("none", "low", "medium", "high", "critical"), default="none",
|
|
38
|
+
help="exit with status 1 when a finding at or above this severity is present")
|
|
39
|
+
d.add_argument("--all", action="store_true", help="table format: list every asset and finding")
|
|
40
|
+
up = d.add_argument_group("upload to the CTEM platform")
|
|
41
|
+
up.add_argument("--upload", action="store_true", help="send the report to the platform's CI ingestion endpoint")
|
|
42
|
+
up.add_argument("--url", default=os.environ.get("CTEM_URL"), help="platform address (default: $CTEM_URL)")
|
|
43
|
+
up.add_argument("--token-env", default="CTEM_CI_TOKEN", help="environment variable holding the pipeline token (default: CTEM_CI_TOKEN)")
|
|
44
|
+
up.add_argument("--gate", choices=("none", "low", "medium", "high", "critical"), default="none",
|
|
45
|
+
help="server-side gate: fail (exit 3) when open findings at or above this severity remain after the upload")
|
|
46
|
+
return parser
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(argv: list[str] | None = None) -> int:
|
|
50
|
+
args = _parser().parse_args(argv)
|
|
51
|
+
approved = {p.strip().lower() for p in (args.approved_providers or "").split(",") if p.strip()} or None
|
|
52
|
+
report = scan.run(Path(args.path), approved_providers=approved, secrets_enabled=not args.no_secrets, max_files=args.max_files,
|
|
53
|
+
excludes=args.exclude)
|
|
54
|
+
if args.vulns:
|
|
55
|
+
from aictem import osv
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
added = osv.enrich(report)
|
|
59
|
+
print(f"aictem: {added} known vulnerabilities in {report['summary']['vulnerabilities_checked']} pinned packages", file=sys.stderr)
|
|
60
|
+
except OSError as exc:
|
|
61
|
+
print(f"aictem: vulnerability lookup failed: {exc}", file=sys.stderr)
|
|
62
|
+
|
|
63
|
+
if args.format == "table" and not args.output:
|
|
64
|
+
output.table(report, show_all=args.all)
|
|
65
|
+
else:
|
|
66
|
+
rendered = output.to_sarif(report) if args.format == "sarif" else output.to_json(report) if args.format == "json" else None
|
|
67
|
+
if rendered is None: # table to a file
|
|
68
|
+
with open(args.output, "w", encoding="utf-8") as fh:
|
|
69
|
+
output.table(report, fh, show_all=True)
|
|
70
|
+
elif args.output:
|
|
71
|
+
Path(args.output).write_text(rendered, encoding="utf-8")
|
|
72
|
+
else:
|
|
73
|
+
sys.stdout.write(rendered)
|
|
74
|
+
if args.output:
|
|
75
|
+
s = report["summary"]
|
|
76
|
+
print(f"aictem: wrote {args.output}: {len(report['assets'])} assets ({s['ai_assets']} AI), {len(report['findings'])} findings", file=sys.stderr)
|
|
77
|
+
|
|
78
|
+
status = 0
|
|
79
|
+
if args.upload:
|
|
80
|
+
from aictem import upload
|
|
81
|
+
|
|
82
|
+
token = os.environ.get(args.token_env)
|
|
83
|
+
if not args.url or not token:
|
|
84
|
+
print(f"aictem: --upload needs --url (or CTEM_URL) and a pipeline token in ${args.token_env}", file=sys.stderr)
|
|
85
|
+
return EXIT_ERROR
|
|
86
|
+
code, body = upload.send(report, args.url, token, fail_on=args.gate)
|
|
87
|
+
if code >= 400 and code != 412:
|
|
88
|
+
print(f"aictem: upload failed (HTTP {code}): {(body.get('error') or {}).get('message') or body}", file=sys.stderr)
|
|
89
|
+
return EXIT_ERROR
|
|
90
|
+
gate = body.get("gate") or {}
|
|
91
|
+
print(f"aictem: uploaded ({body.get('status')}): {body.get('findings_new', 0)} new, {body.get('findings_seen', 0)} seen findings"
|
|
92
|
+
+ (f"; gate {'passed' if gate.get('passed') else 'FAILED'} at {gate.get('threshold')}" if gate and gate.get("threshold") != "none" else ""),
|
|
93
|
+
file=sys.stderr)
|
|
94
|
+
if code == 412:
|
|
95
|
+
status = EXIT_UPLOAD_GATE
|
|
96
|
+
|
|
97
|
+
if args.fail_on != "none":
|
|
98
|
+
worst = output.worst(report)
|
|
99
|
+
if worst and SEVERITY_RANK[worst] >= SEVERITY_RANK[args.fail_on]:
|
|
100
|
+
print(f"aictem: findings at or above {args.fail_on} present (worst: {worst})", file=sys.stderr)
|
|
101
|
+
status = status or EXIT_FINDINGS
|
|
102
|
+
return status
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
sys.exit(main())
|
aictem/catalog.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""What the detectors look for. Keys match the CTEM platform's AI signatures so uploads merge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Tech:
|
|
11
|
+
key: str
|
|
12
|
+
name: str
|
|
13
|
+
category: str # provider | framework | vector_db | serving | mcp | registry
|
|
14
|
+
pypi: tuple[str, ...] = ()
|
|
15
|
+
npm: tuple[str, ...] = ()
|
|
16
|
+
imports: tuple[str, ...] = () # Python module roots and JS/TS package specifiers
|
|
17
|
+
hosts: tuple[str, ...] = () # substrings of API hostnames or ports
|
|
18
|
+
env: tuple[str, ...] = ()
|
|
19
|
+
go: tuple[str, ...] = () # Go module paths
|
|
20
|
+
images: tuple[str, ...] = () # container image name prefixes
|
|
21
|
+
agentic: bool = False
|
|
22
|
+
rag: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
T = Tech
|
|
26
|
+
TECHS: tuple[Tech, ...] = (
|
|
27
|
+
# Hosted model providers
|
|
28
|
+
T("openai", "OpenAI", "provider", ("openai",), ("openai",), ("openai",), ("api.openai.com",), ("OPENAI_API_KEY", "OPENAI_BASE_URL"),
|
|
29
|
+
("github.com/openai/openai-go", "github.com/sashabaranov/go-openai")),
|
|
30
|
+
T("anthropic", "Anthropic", "provider", ("anthropic",), ("@anthropic-ai/sdk",), ("anthropic", "@anthropic-ai/sdk"), ("api.anthropic.com",),
|
|
31
|
+
("ANTHROPIC_API_KEY",), ("github.com/anthropics/anthropic-sdk-go",)),
|
|
32
|
+
T("google_gemini", "Google Gemini", "provider", ("google-generativeai", "google-genai"), ("@google/generative-ai", "@google/genai"),
|
|
33
|
+
("google.generativeai", "google.genai", "@google/generative-ai", "@google/genai"), ("generativelanguage.googleapis.com",),
|
|
34
|
+
("GEMINI_API_KEY", "GOOGLE_GENAI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"), ("google.golang.org/genai", "github.com/google/generative-ai-go")),
|
|
35
|
+
T("vertex_ai", "Google Vertex AI", "provider", ("google-cloud-aiplatform", "vertexai"), ("@google-cloud/vertexai",),
|
|
36
|
+
("vertexai", "google.cloud.aiplatform", "@google-cloud/vertexai"), ("aiplatform.googleapis.com",)),
|
|
37
|
+
T("aws_bedrock", "AWS Bedrock", "provider", ("langchain-aws",), ("@aws-sdk/client-bedrock-runtime", "@aws-sdk/client-bedrock-agent-runtime"),
|
|
38
|
+
("@aws-sdk/client-bedrock-runtime", "@aws-sdk/client-bedrock-agent-runtime"), ("bedrock-runtime.", "bedrock-agent-runtime."),
|
|
39
|
+
("AWS_BEDROCK_MODEL_ID", "BEDROCK_MODEL_ID")),
|
|
40
|
+
T("azure_openai", "Azure OpenAI / AI Foundry", "provider", ("azure-ai-inference", "azure-ai-projects", "azure-ai-agents"),
|
|
41
|
+
("@azure/openai", "@azure-rest/ai-inference"), ("azure.ai.inference", "azure.ai.projects", "azure.ai.agents", "@azure/openai"),
|
|
42
|
+
(".openai.azure.com", ".services.ai.azure.com", ".inference.ai.azure.com"), ("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT")),
|
|
43
|
+
T("cohere", "Cohere", "provider", ("cohere",), ("cohere-ai",), ("cohere", "cohere-ai"), ("api.cohere.com", "api.cohere.ai"), ("COHERE_API_KEY", "CO_API_KEY")),
|
|
44
|
+
T("mistral", "Mistral AI", "provider", ("mistralai",), ("@mistralai/mistralai",), ("mistralai", "@mistralai/mistralai"), ("api.mistral.ai",), ("MISTRAL_API_KEY",)),
|
|
45
|
+
T("groq", "Groq", "provider", ("groq",), ("groq-sdk",), ("groq", "groq-sdk"), ("api.groq.com",), ("GROQ_API_KEY",)),
|
|
46
|
+
T("together", "Together AI", "provider", ("together",), ("together-ai",), ("together", "together-ai"), ("api.together.xyz", "api.together.ai"), ("TOGETHER_API_KEY",)),
|
|
47
|
+
T("fireworks", "Fireworks AI", "provider", ("fireworks-ai",), ("@fireworks-ai/sdk",), ("fireworks",), ("api.fireworks.ai",), ("FIREWORKS_API_KEY",)),
|
|
48
|
+
T("deepseek", "DeepSeek", "provider", (), (), (), ("api.deepseek.com",), ("DEEPSEEK_API_KEY",)),
|
|
49
|
+
T("xai", "xAI", "provider", ("xai-sdk",), ("@ai-sdk/xai",), ("xai_sdk",), ("api.x.ai",), ("XAI_API_KEY",)),
|
|
50
|
+
T("openrouter", "OpenRouter", "provider", (), ("@openrouter/ai-sdk-provider",), (), ("openrouter.ai",), ("OPENROUTER_API_KEY",)),
|
|
51
|
+
T("perplexity", "Perplexity", "provider", (), (), (), ("api.perplexity.ai",), ("PERPLEXITY_API_KEY", "PPLX_API_KEY")),
|
|
52
|
+
T("replicate", "Replicate", "provider", ("replicate",), ("replicate",), ("replicate",), ("api.replicate.com",), ("REPLICATE_API_TOKEN",)),
|
|
53
|
+
T("voyage", "Voyage AI", "provider", ("voyageai",), ("voyageai",), ("voyageai",), ("api.voyageai.com",), ("VOYAGE_API_KEY",)),
|
|
54
|
+
T("nvidia_nim", "NVIDIA NIM", "serving", (), (), (), ("integrate.api.nvidia.com", "ai.api.nvidia.com"), ("NVIDIA_API_KEY", "NGC_API_KEY"),
|
|
55
|
+
images=("nvcr.io/nim/",)),
|
|
56
|
+
# Self-hosted serving
|
|
57
|
+
T("ollama", "Ollama", "serving", ("ollama", "langchain-ollama"), ("ollama", "ollama-ai-provider"), ("ollama",), (":11434",), ("OLLAMA_HOST",),
|
|
58
|
+
("github.com/ollama/ollama",), ("ollama/ollama",)),
|
|
59
|
+
T("vllm", "vLLM", "serving", ("vllm",), (), ("vllm",), (), (), images=("vllm/vllm-openai",)),
|
|
60
|
+
T("tgi", "Hugging Face Text Generation Inference", "serving", ("text-generation",), (), ("text_generation",), (), (),
|
|
61
|
+
images=("ghcr.io/huggingface/text-generation-inference",)),
|
|
62
|
+
T("llama_cpp", "llama.cpp", "serving", ("llama-cpp-python",), ("node-llama-cpp",), ("llama_cpp", "node-llama-cpp"), (), (),
|
|
63
|
+
images=("ghcr.io/ggerganov/llama.cpp", "ghcr.io/ggml-org/llama.cpp")),
|
|
64
|
+
T("localai", "LocalAI", "serving", (), (), (), (), (), images=("localai/localai", "quay.io/go-skynet/local-ai")),
|
|
65
|
+
T("open_webui", "Open WebUI", "serving", ("open-webui",), (), (), (), (), images=("ghcr.io/open-webui/open-webui",)),
|
|
66
|
+
# Gateways and frameworks
|
|
67
|
+
T("litellm", "LiteLLM gateway", "framework", ("litellm",), (), ("litellm",), (), ("LITELLM_MASTER_KEY",), images=("ghcr.io/berriai/litellm",)),
|
|
68
|
+
T("langchain", "LangChain", "framework",
|
|
69
|
+
("langchain", "langchain-core", "langchain-community", "langgraph", "langchain-openai", "langchain-anthropic", "langchain-google-genai"),
|
|
70
|
+
("langchain", "@langchain/core", "@langchain/langgraph", "@langchain/openai", "@langchain/anthropic", "@langchain/community"),
|
|
71
|
+
("langchain", "langchain_core", "langchain_community", "langgraph", "langchain_openai", "langchain_anthropic",
|
|
72
|
+
"@langchain/core", "@langchain/langgraph", "@langchain/openai", "@langchain/anthropic", "@langchain/community"),
|
|
73
|
+
(), ("LANGCHAIN_API_KEY", "LANGSMITH_API_KEY"), ("github.com/tmc/langchaingo",), agentic=True),
|
|
74
|
+
T("claude_agent_sdk", "Claude Agent SDK", "framework", ("claude-agent-sdk", "claude-code-sdk"), ("@anthropic-ai/claude-agent-sdk", "@anthropic-ai/claude-code"),
|
|
75
|
+
("claude_agent_sdk", "claude_code_sdk", "@anthropic-ai/claude-agent-sdk", "@anthropic-ai/claude-code"), agentic=True),
|
|
76
|
+
T("llamaindex", "LlamaIndex", "framework", ("llama-index", "llama-index-core"), ("llamaindex",), ("llama_index", "llamaindex"), (),
|
|
77
|
+
("LLAMA_CLOUD_API_KEY",), rag=True),
|
|
78
|
+
T("haystack", "Haystack", "framework", ("haystack-ai", "farm-haystack"), (), ("haystack",), (), (), rag=True),
|
|
79
|
+
T("semantic_kernel", "Semantic Kernel", "framework", ("semantic-kernel",), ("semantic-kernel",), ("semantic_kernel",), agentic=True),
|
|
80
|
+
T("autogen", "AutoGen", "framework", ("pyautogen", "autogen", "autogen-agentchat", "autogen-core", "ag2"), (),
|
|
81
|
+
("autogen", "autogen_agentchat", "autogen_core"), agentic=True),
|
|
82
|
+
T("deepagents", "LangChain Deep Agents", "framework", ("deepagents", "managed-deepagents"), ("deepagents",),
|
|
83
|
+
("deepagents", "managed_deepagents"), agentic=True),
|
|
84
|
+
T("crewai", "CrewAI", "framework", ("crewai", "crewai-tools"), (), ("crewai", "crewai_tools"), agentic=True),
|
|
85
|
+
T("openai_agents", "OpenAI Agents SDK", "framework", ("openai-agents",), ("@openai/agents",), ("@openai/agents",), agentic=True),
|
|
86
|
+
T("pydantic_ai", "Pydantic AI", "framework", ("pydantic-ai", "pydantic-ai-slim"), (), ("pydantic_ai",), agentic=True),
|
|
87
|
+
T("smolagents", "smolagents", "framework", ("smolagents",), (), ("smolagents",), agentic=True),
|
|
88
|
+
T("google_adk", "Google Agent Development Kit", "framework", ("google-adk",), ("@google/adk",), ("google.adk", "@google/adk"), agentic=True),
|
|
89
|
+
T("strands", "Strands Agents", "framework", ("strands-agents",), (), ("strands",), agentic=True),
|
|
90
|
+
T("dspy", "DSPy", "framework", ("dspy", "dspy-ai"), (), ("dspy",)),
|
|
91
|
+
T("vercel_ai", "Vercel AI SDK", "framework", (), ("ai", "@ai-sdk/openai", "@ai-sdk/anthropic", "@ai-sdk/google"),
|
|
92
|
+
("@ai-sdk/openai", "@ai-sdk/anthropic", "@ai-sdk/google", "@ai-sdk/react"), agentic=True),
|
|
93
|
+
T("mastra", "Mastra", "framework", (), ("@mastra/core",), ("@mastra/core",), agentic=True),
|
|
94
|
+
T("mcp", "Model Context Protocol SDK", "mcp", ("mcp", "fastmcp", "langchain-mcp-adapters"), ("@modelcontextprotocol/sdk", "@langchain/mcp-adapters"),
|
|
95
|
+
("mcp", "fastmcp", "@modelcontextprotocol/sdk", "langchain_mcp_adapters", "@langchain/mcp-adapters"),
|
|
96
|
+
go=("github.com/mark3labs/mcp-go", "github.com/modelcontextprotocol/go-sdk")),
|
|
97
|
+
# Vector stores
|
|
98
|
+
T("pinecone", "Pinecone", "vector_db", ("pinecone", "pinecone-client"), ("@pinecone-database/pinecone",), ("pinecone", "@pinecone-database/pinecone"),
|
|
99
|
+
(".pinecone.io",), ("PINECONE_API_KEY",)),
|
|
100
|
+
T("chroma", "Chroma", "vector_db", ("chromadb", "langchain-chroma"), ("chromadb",), ("chromadb",), (":8000/api/v1",), ("CHROMA_HOST",),
|
|
101
|
+
images=("chromadb/chroma",)),
|
|
102
|
+
T("qdrant", "Qdrant", "vector_db", ("qdrant-client", "langchain-qdrant"), ("@qdrant/js-client-rest",), ("qdrant_client", "@qdrant/js-client-rest"),
|
|
103
|
+
(":6333", ".qdrant.io"), ("QDRANT_API_KEY", "QDRANT_URL"), images=("qdrant/qdrant",)),
|
|
104
|
+
T("weaviate", "Weaviate", "vector_db", ("weaviate-client",), ("weaviate-client", "weaviate-ts-client"), ("weaviate",),
|
|
105
|
+
(".weaviate.network", ".weaviate.cloud"), ("WEAVIATE_API_KEY", "WEAVIATE_URL"), images=("semitechnologies/weaviate", "cr.weaviate.io/")),
|
|
106
|
+
T("milvus", "Milvus", "vector_db", ("pymilvus",), ("@zilliz/milvus2-sdk-node",), ("pymilvus",), (":19530",), ("MILVUS_URI",), images=("milvusdb/milvus",)),
|
|
107
|
+
T("pgvector", "pgvector", "vector_db", ("pgvector",), ("pgvector",), ("pgvector",), images=("pgvector/pgvector",)),
|
|
108
|
+
T("faiss", "FAISS", "vector_db", ("faiss-cpu", "faiss-gpu"), (), ("faiss",)),
|
|
109
|
+
T("lancedb", "LanceDB", "vector_db", ("lancedb",), ("@lancedb/lancedb",), ("lancedb", "@lancedb/lancedb")),
|
|
110
|
+
# Model hubs and registries
|
|
111
|
+
T("huggingface", "Hugging Face", "registry", ("huggingface-hub", "transformers", "diffusers", "sentence-transformers"),
|
|
112
|
+
("@huggingface/inference", "@huggingface/hub", "@huggingface/transformers"),
|
|
113
|
+
("huggingface_hub", "transformers", "diffusers", "sentence_transformers", "@huggingface/inference", "@huggingface/transformers"),
|
|
114
|
+
("huggingface.co", "api-inference.huggingface.co"), ("HF_TOKEN", "HUGGINGFACEHUB_API_TOKEN", "HUGGING_FACE_HUB_TOKEN")),
|
|
115
|
+
T("mlflow", "MLflow", "registry", ("mlflow",), (), ("mlflow",), (), ("MLFLOW_TRACKING_URI",), images=("ghcr.io/mlflow/mlflow",)),
|
|
116
|
+
)
|
|
117
|
+
BY_KEY = {t.key: t for t in TECHS}
|
|
118
|
+
# Framework integration packages that also mean the named provider is used (langchain-anthropic -> Anthropic).
|
|
119
|
+
INTEGRATIONS = {
|
|
120
|
+
"claude-agent-sdk": "anthropic", "claude_agent_sdk": "anthropic", "@anthropic-ai/claude-agent-sdk": "anthropic",
|
|
121
|
+
"langchain-openai": "openai", "langchain-anthropic": "anthropic", "langchain-google-genai": "google_gemini",
|
|
122
|
+
"langchain-google-vertexai": "vertex_ai", "langchain-aws": "aws_bedrock", "langchain-mistralai": "mistral", "langchain-groq": "groq",
|
|
123
|
+
"langchain-cohere": "cohere", "langchain-ollama": "ollama", "langchain-together": "together", "langchain-fireworks": "fireworks",
|
|
124
|
+
"langchain-xai": "xai", "langchain-deepseek": "deepseek", "langchain-pinecone": "pinecone", "langchain-chroma": "chroma",
|
|
125
|
+
"langchain-qdrant": "qdrant", "langchain-weaviate": "weaviate", "langchain-milvus": "milvus", "langchain-postgres": "pgvector",
|
|
126
|
+
"llama-index-llms-openai": "openai", "llama-index-llms-anthropic": "anthropic", "llama-index-llms-ollama": "ollama",
|
|
127
|
+
"llama-index-embeddings-openai": "openai", "llama-index-vector-stores-chroma": "chroma", "llama-index-vector-stores-qdrant": "qdrant",
|
|
128
|
+
"llama-index-vector-stores-pinecone": "pinecone", "llama-index-vector-stores-postgres": "pgvector",
|
|
129
|
+
"@langchain/openai": "openai", "@langchain/anthropic": "anthropic", "@langchain/google-genai": "google_gemini", "@langchain/ollama": "ollama",
|
|
130
|
+
"@ai-sdk/openai": "openai", "@ai-sdk/anthropic": "anthropic", "@ai-sdk/google": "google_gemini", "@ai-sdk/mistral": "mistral",
|
|
131
|
+
"@ai-sdk/amazon-bedrock": "aws_bedrock", "@ai-sdk/azure": "azure_openai", "@ai-sdk/groq": "groq",
|
|
132
|
+
"langchain_openai": "openai", "langchain_anthropic": "anthropic", "langchain_google_genai": "google_gemini", "langchain_aws": "aws_bedrock",
|
|
133
|
+
"langchain_mistralai": "mistral", "langchain_groq": "groq", "langchain_ollama": "ollama", "langchain_chroma": "chroma",
|
|
134
|
+
"langchain_qdrant": "qdrant", "langchain_pinecone": "pinecone",
|
|
135
|
+
}
|
|
136
|
+
# "provider:model" identifiers used by init_chat_model and similar helpers.
|
|
137
|
+
MODEL_PREFIXES = {"openai": "openai", "anthropic": "anthropic", "google_genai": "google_gemini", "google_vertexai": "vertex_ai",
|
|
138
|
+
"google-vertex": "vertex_ai", "bedrock": "aws_bedrock", "bedrock_converse": "aws_bedrock", "mistralai": "mistral",
|
|
139
|
+
"groq": "groq", "ollama": "ollama", "azure_openai": "azure_openai", "cohere": "cohere", "fireworks": "fireworks",
|
|
140
|
+
"together": "together", "xai": "xai", "deepseek": "deepseek", "huggingface": "huggingface"}
|
|
141
|
+
PREFIXED_MODEL = re.compile(r"""["'](openai|anthropic|google_genai|google_vertexai|google-vertex|bedrock_converse|bedrock|mistralai|groq|ollama|"""
|
|
142
|
+
r"""azure_openai|cohere|fireworks|together|xai|deepseek|huggingface):(?=[\w.\-:]*[A-Za-z])([A-Za-z0-9][\w.\-:]{1,80})["']""") # not host:port
|
|
143
|
+
PYPI_INDEX = {p.lower(): t for t in TECHS for p in t.pypi}
|
|
144
|
+
NPM_INDEX = {p.lower(): t for t in TECHS for p in t.npm}
|
|
145
|
+
GO_INDEX = {p: t for t in TECHS for p in t.go}
|
|
146
|
+
IMPORT_INDEX = {i: t for t in TECHS for i in t.imports}
|
|
147
|
+
ENV_INDEX = {e: t for t in TECHS for e in t.env}
|
|
148
|
+
IMAGE_INDEX = {i: t for t in TECHS for i in t.images}
|
|
149
|
+
ASSET_TYPE = {"provider": "ai_provider", "serving": "ai_provider", "framework": "ai_framework", "mcp": "ai_framework",
|
|
150
|
+
"vector_db": "vector_database", "registry": "model_registry"}
|
|
151
|
+
|
|
152
|
+
# --- Secrets ---------------------------------------------------------------------------------------
|
|
153
|
+
# (rule, provider key or "", label, severity, pattern). Group 1 is a non-secret prefix kept in evidence.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class SecretPattern:
|
|
158
|
+
rule: str
|
|
159
|
+
provider: str
|
|
160
|
+
label: str
|
|
161
|
+
severity: str
|
|
162
|
+
pattern: re.Pattern = field(compare=False)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
S = SecretPattern
|
|
166
|
+
SECRET_PATTERNS: tuple[SecretPattern, ...] = (
|
|
167
|
+
S("anthropic_api_key", "anthropic", "Anthropic API key", "critical", re.compile(r"\b(sk-ant-(?:api|admin)\d{2}-)[A-Za-z0-9_\-]{60,}")),
|
|
168
|
+
S("openai_project_key", "openai", "OpenAI API key", "critical", re.compile(r"\b(sk-(?:proj|svcacct|admin)-)[A-Za-z0-9_\-]{40,}")),
|
|
169
|
+
S("openai_api_key", "openai", "OpenAI API key (legacy format)", "critical", re.compile(r"\b(sk-)[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}")),
|
|
170
|
+
S("huggingface_token", "huggingface", "Hugging Face token", "critical", re.compile(r"\b(hf_)[A-Za-z0-9]{34,}")),
|
|
171
|
+
# Generic Google key (Maps, Firebase web config, Gemini, ...): not evidence of an AI provider, and often public by design.
|
|
172
|
+
S("google_api_key", "", "Google API key", "medium", re.compile(r"\b(AIza)[0-9A-Za-z_\-]{35}\b")),
|
|
173
|
+
S("nvidia_api_key", "nvidia_nim", "NVIDIA API key", "critical", re.compile(r"\b(nvapi-)[A-Za-z0-9_\-]{50,}")),
|
|
174
|
+
S("pinecone_api_key", "pinecone", "Pinecone API key", "critical", re.compile(r"\b(pcsk_)[A-Za-z0-9_]{40,}")),
|
|
175
|
+
S("replicate_token", "replicate", "Replicate API token", "critical", re.compile(r"\b(r8_)[A-Za-z0-9]{37}\b")),
|
|
176
|
+
S("groq_api_key", "groq", "Groq API key", "critical", re.compile(r"\b(gsk_)[A-Za-z0-9]{48,}")),
|
|
177
|
+
S("xai_api_key", "xai", "xAI API key", "critical", re.compile(r"\b(xai-)[A-Za-z0-9]{70,}")),
|
|
178
|
+
S("aws_access_key", "", "AWS access key ID", "high", re.compile(r"\b((?:AKIA|ASIA))[0-9A-Z]{16}\b")),
|
|
179
|
+
S("github_token", "", "GitHub token", "critical", re.compile(r"\b(gh[pousr]_)[A-Za-z0-9]{36,}\b|\b(github_pat_)[A-Za-z0-9_]{60,}")),
|
|
180
|
+
S("gitlab_token", "", "GitLab token", "critical", re.compile(r"\b(glpat-)[A-Za-z0-9_\-]{20,}")),
|
|
181
|
+
S("slack_token", "", "Slack token", "high", re.compile(r"\b(xox[baprs]-)[A-Za-z0-9\-]{10,}")),
|
|
182
|
+
S("stripe_live_key", "", "Stripe live secret key", "critical", re.compile(r"\b((?:sk|rk)_live_)[A-Za-z0-9]{20,}")),
|
|
183
|
+
S("npm_token", "", "npm access token", "high", re.compile(r"\b(npm_)[A-Za-z0-9]{36}\b")),
|
|
184
|
+
S("sendgrid_key", "", "SendGrid API key", "high", re.compile(r"\b(SG\.)[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}\b")),
|
|
185
|
+
S("private_key", "", "Private key", "critical", re.compile(r"(-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----)")),
|
|
186
|
+
)
|
|
187
|
+
# Values that are clearly placeholders, not credentials.
|
|
188
|
+
PLACEHOLDER = re.compile(r"(x{6,}|0{8,}|your[_-]?(api[_-]?)?key|example|placeholder|changeme|dummy|<[^>]+>|\.\.\.)", re.IGNORECASE)
|
|
189
|
+
|
|
190
|
+
# --- Model artifacts --------------------------------------------------------------------------------
|
|
191
|
+
MODEL_EXTENSIONS = {
|
|
192
|
+
".pkl": "pickle", ".pickle": "pickle", ".joblib": "pickle", ".pt": "pytorch", ".pth": "pytorch", ".ckpt": "pytorch",
|
|
193
|
+
".bin": "binary", ".h5": "hdf5", ".keras": "keras", ".onnx": "onnx", ".safetensors": "safetensors", ".gguf": "gguf",
|
|
194
|
+
".tflite": "tflite", ".mlmodel": "coreml",
|
|
195
|
+
}
|
|
196
|
+
MODEL_FILE_NAMES = {"saved_model.pb": "tensorflow"} # other .pb files are usually protobuf descriptors, not models
|
|
197
|
+
CODE_EXECUTION_FORMATS = {"pickle", "pytorch"}
|
|
198
|
+
MIN_MODEL_BYTES = 32 # smaller files with these extensions are not models
|
|
199
|
+
|
|
200
|
+
# --- MCP ---------------------------------------------------------------------------------------------
|
|
201
|
+
MCP_CONFIG_NAMES = {".mcp.json", "mcp.json", "claude_desktop_config.json", "mcp_config.json", "mcp-config.json", "mcp_settings.json",
|
|
202
|
+
"cline_mcp_settings.json"}
|
|
203
|
+
MCP_CONFIG_PARENTS = {".cursor", ".vscode", ".claude", ".windsurf", ".gemini", ".roo", ".kiro", ".amazonq", ".codex", ".continue"}
|
|
204
|
+
|
|
205
|
+
# --- Agent tools --------------------------------------------------------------------------------------
|
|
206
|
+
CAPABILITY_MARKERS: dict[str, re.Pattern] = {
|
|
207
|
+
"shell": re.compile(r"\b(shell|bash|exec(ute)?[_ ]?(command|cmd)|run[_ ]?(command|cmd|script)|terminal|subprocess)\b", re.IGNORECASE),
|
|
208
|
+
"code_execution": re.compile(r"\b(eval|run[_ ]?(python|code|js)|code[_ ]?interpreter|execute[_ ]?code|python[_ ]?repl)\b", re.IGNORECASE),
|
|
209
|
+
"filesystem_write": re.compile(r"\b(write|delete|remove|move|rename|create)[_ ]?(file|dir|directory|folder)|edit[_ ]?file\b", re.IGNORECASE),
|
|
210
|
+
"database_write": re.compile(r"\b(execute[_ ]?sql|run[_ ]?query|insert[_ ]?(row|record)|update[_ ]?(row|record|table)|drop[_ ]?table)\b", re.IGNORECASE),
|
|
211
|
+
"email_send": re.compile(r"\bsend[_ ]?(e?mail|message|sms|slack)\b", re.IGNORECASE),
|
|
212
|
+
"payment": re.compile(r"\b(payments?|refunds?|refund[_ ]?\w+|pay[_ ]?(invoice|bill|out|vendor)|transfer[_ ]?funds|charge[_ ]?(card|customer)|"
|
|
213
|
+
r"purchase|issue[_ ]?invoice|process[_ ]?payment)\b", re.IGNORECASE),
|
|
214
|
+
"deploy": re.compile(r"\b(deploy|release|kubectl|terraform[_ ]?apply|merge[_ ]?(pull|pr))\b", re.IGNORECASE),
|
|
215
|
+
"iam_admin": re.compile(r"\b(create|delete|grant|revoke|attach)[_ ]?(user|role|policy|permission|access[_ ]?key)\b", re.IGNORECASE),
|
|
216
|
+
"delete": re.compile(r"\b(delete|destroy|purge|wipe)[_ ]?\w*", re.IGNORECASE),
|
|
217
|
+
"external_http": re.compile(r"\b(fetch[_ ]?(url|page|web)|http[_ ]?request|curl|browse|web[_ ]?(search|fetch)|download)\b", re.IGNORECASE),
|
|
218
|
+
}
|
|
219
|
+
HIGH_IMPACT = {"shell", "code_execution", "filesystem_write", "database_write", "email_send", "payment", "deploy", "iam_admin", "delete",
|
|
220
|
+
"external_http"}
|
|
221
|
+
|
|
222
|
+
POISONING_INDICATORS: tuple[tuple[str, str, re.Pattern], ...] = (
|
|
223
|
+
("instruction_override", "Text instructs the model to ignore or override other instructions",
|
|
224
|
+
re.compile(r"(ignore|disregard|forget|override)\s+(all\s+|any\s+)?(previous|prior|above|other|earlier|system)\s+(instructions?|rules?|prompts?)", re.IGNORECASE)),
|
|
225
|
+
("concealment", "Text instructs the model to hide its behaviour from the user",
|
|
226
|
+
re.compile(r"(do\s+not|don'?t|never)\s+(tell|inform|mention|reveal|show|notify)\s+(this\s+)?(to\s+)?(the\s+)?(user|human)", re.IGNORECASE)),
|
|
227
|
+
("imperative_block", "Description contains a directive block addressed to the model",
|
|
228
|
+
re.compile(r"<\s*(important|system|instructions?|secret|admin)\s*>|\[\s*(system|important)\s*\]", re.IGNORECASE)),
|
|
229
|
+
("sensitive_file_access", "Description directs access to credential or key material",
|
|
230
|
+
re.compile(r"(~|\$HOME|/home/[^/\s]+|/root)/\.(ssh|aws|gnupg|kube)|id_rsa|/etc/(passwd|shadow)|credentials\.json", re.IGNORECASE)),
|
|
231
|
+
("exfiltration", "Description directs data to an external destination or hidden parameter",
|
|
232
|
+
re.compile(r"(send|post|upload|forward|exfiltrate)\s+[^.]{0,60}to\s+https?://", re.IGNORECASE)),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# --- Infrastructure as code -------------------------------------------------------------------------
|
|
236
|
+
# resource type prefix -> (asset type, AI technology key or None)
|
|
237
|
+
# resource type -> (asset type, AI technology key or None). Types match exactly; an entry ending in "_" (Terraform) or "::"
|
|
238
|
+
# (CloudFormation) is a family prefix, used for AI services whose every resource is worth listing.
|
|
239
|
+
TERRAFORM_TYPES: tuple[tuple[str, str, str | None], ...] = (
|
|
240
|
+
("aws_sagemaker_endpoint", "llm_endpoint", None), ("aws_sagemaker_model", "ai_model", None),
|
|
241
|
+
("aws_bedrockagent_agent", "ai_agent", "aws_bedrock"), ("aws_bedrockagent_knowledge_base", "rag_application", "aws_bedrock"),
|
|
242
|
+
("aws_bedrock_", "cloud_resource", "aws_bedrock"), ("aws_bedrockagent_", "cloud_resource", "aws_bedrock"),
|
|
243
|
+
("google_vertex_ai_endpoint", "llm_endpoint", "vertex_ai"), ("google_vertex_ai_index", "vector_database", "vertex_ai"),
|
|
244
|
+
("google_vertex_ai_", "cloud_resource", "vertex_ai"),
|
|
245
|
+
("azurerm_cognitive_deployment", "llm_endpoint", "azure_openai"), ("azurerm_cognitive_account", "ai_provider", "azure_openai"),
|
|
246
|
+
("azurerm_machine_learning_workspace", "model_registry", None), ("azurerm_search_service", "vector_database", None),
|
|
247
|
+
("aws_s3_bucket", "storage", None), ("google_storage_bucket", "storage", None), ("azurerm_storage_account", "storage", None),
|
|
248
|
+
("aws_db_instance", "database", None), ("aws_rds_cluster", "database", None), ("aws_dynamodb_table", "database", None),
|
|
249
|
+
("google_sql_database_instance", "database", None), ("azurerm_postgresql_server", "database", None),
|
|
250
|
+
("azurerm_postgresql_flexible_server", "database", None), ("azurerm_mssql_server", "database", None), ("azurerm_mssql_database", "database", None),
|
|
251
|
+
("azurerm_cosmosdb_account", "database", None), ("aws_opensearch_domain", "database", None), ("aws_elasticache_cluster", "database", None),
|
|
252
|
+
("aws_elasticache_replication_group", "database", None), ("aws_redshift_cluster", "database", None),
|
|
253
|
+
("aws_lb", "load_balancer", None), ("aws_alb", "load_balancer", None), ("aws_elb", "load_balancer", None),
|
|
254
|
+
("google_compute_forwarding_rule", "load_balancer", None), ("azurerm_lb", "load_balancer", None), ("azurerm_application_gateway", "load_balancer", None),
|
|
255
|
+
("aws_iam_role", "identity", None), ("aws_iam_user", "identity", None), ("google_service_account", "identity", None),
|
|
256
|
+
("azurerm_user_assigned_identity", "identity", None),
|
|
257
|
+
("aws_instance", "host", None), ("google_compute_instance", "host", None), ("azurerm_linux_virtual_machine", "host", None),
|
|
258
|
+
("azurerm_windows_virtual_machine", "host", None),
|
|
259
|
+
("aws_eks_cluster", "cloud_resource", None), ("google_container_cluster", "cloud_resource", None), ("azurerm_kubernetes_cluster", "cloud_resource", None),
|
|
260
|
+
("aws_lambda_function", "cloud_resource", None), ("google_cloudfunctions_function", "cloud_resource", None),
|
|
261
|
+
("google_cloudfunctions2_function", "cloud_resource", None), ("google_cloud_run_service", "cloud_resource", None),
|
|
262
|
+
("google_cloud_run_v2_service", "cloud_resource", None), ("aws_ecs_service", "cloud_resource", None),
|
|
263
|
+
("aws_api_gateway_rest_api", "api", None), ("aws_apigatewayv2_api", "api", None),
|
|
264
|
+
)
|
|
265
|
+
CFN_TYPES: tuple[tuple[str, str, str | None], ...] = (
|
|
266
|
+
("AWS::SageMaker::Endpoint", "llm_endpoint", None), ("AWS::SageMaker::Model", "ai_model", None),
|
|
267
|
+
("AWS::Bedrock::Agent", "ai_agent", "aws_bedrock"), ("AWS::Bedrock::KnowledgeBase", "rag_application", "aws_bedrock"),
|
|
268
|
+
("AWS::Bedrock::", "cloud_resource", "aws_bedrock"), ("AWS::S3::Bucket", "storage", None), ("AWS::RDS::DBInstance", "database", None),
|
|
269
|
+
("AWS::RDS::DBCluster", "database", None), ("AWS::DynamoDB::Table", "database", None),
|
|
270
|
+
("AWS::ElasticLoadBalancingV2::LoadBalancer", "load_balancer", None), ("AWS::IAM::Role", "identity", None), ("AWS::IAM::User", "identity", None),
|
|
271
|
+
("AWS::EC2::Instance", "host", None), ("AWS::EKS::Cluster", "cloud_resource", None), ("AWS::Lambda::Function", "cloud_resource", None),
|
|
272
|
+
("AWS::Serverless::Function", "cloud_resource", None), ("AWS::ECS::Service", "cloud_resource", None), ("AWS::ApiGateway::RestApi", "api", None),
|
|
273
|
+
("AWS::ApiGatewayV2::Api", "api", None), ("AWS::OpenSearchService::Domain", "database", None),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def resource_type(kind: str, table) -> tuple[str, str | None] | None:
|
|
278
|
+
"""(asset type, technology) for an IaC resource type: exact entries first, then family prefixes."""
|
|
279
|
+
for name, asset_type, tech in table:
|
|
280
|
+
if kind == name:
|
|
281
|
+
return asset_type, tech
|
|
282
|
+
for name, asset_type, tech in table:
|
|
283
|
+
if name.endswith(("_", "::")) and kind.startswith(name):
|
|
284
|
+
return asset_type, tech
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# Hosts whose URLs in code are documentation or package indexes, not services the code calls.
|
|
289
|
+
NON_SERVICE_DOMAINS = (
|
|
290
|
+
"github.com", "githubusercontent.com", "gitlab.com", "stackoverflow.com", "wikipedia.org", "example.com", "example.org", "example.net",
|
|
291
|
+
"schema.org", "w3.org", "json-schema.org", "npmjs.com", "npmjs.org", "pypi.org", "pythonhosted.org", "mozilla.org", "python.org",
|
|
292
|
+
"shields.io", "creativecommons.org", "opensource.org", "unpkg.com", "jsdelivr.net", "twitter.com", "x.com", "youtube.com", "medium.com",
|
|
293
|
+
"spdx.org", "readthedocs.io", "readthedocs.org", "apache.org", "gnu.org", "microsoft.com", "google.com", "apple.com",
|
|
294
|
+
)
|
|
295
|
+
NON_SERVICE_LABELS = ("docs", "doc", "schemas", "fonts", "img", "images", "cdn", "static", "assets", "www")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def is_reference_host(host: str) -> bool:
|
|
299
|
+
"""True for documentation, package-index and asset hosts: URLs to them are references, not services the code calls."""
|
|
300
|
+
host = host.lower().split(":")[0]
|
|
301
|
+
if any(host == d or host.endswith("." + d) for d in NON_SERVICE_DOMAINS):
|
|
302
|
+
return True
|
|
303
|
+
return host.split(".")[0] in NON_SERVICE_LABELS
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# URL path or port markers of a self-hosted inference API.
|
|
307
|
+
INFERENCE_MARKERS = re.compile(r"(:11434\b|/v1/chat/completions|/v1/completions|/v1/embeddings|/api/generate|/api/chat\b|/generate_stream|/v1/models)")
|
|
308
|
+
MODEL_NAME = re.compile(
|
|
309
|
+
r"""(?:model(?:_name|_id|Name|Id)?\s*[=:]\s*|"model"\s*:\s*)["']((?:gpt-|o[134]-|o[134]\b|claude-|gemini-|mistral-|mixtral|llama|codellama|"""
|
|
310
|
+
r"""qwen|deepseek|phi-|command-r|grok-|text-embedding-|embed-|anthropic\.|amazon\.|meta\.)[\w.:\-/]*)["']""", re.IGNORECASE)
|
|
311
|
+
HF_MODEL_REF = re.compile(
|
|
312
|
+
r"""(?:from_pretrained|hf_hub_download|snapshot_download|pipeline|SentenceTransformer|AutoModel\w*\.from_pretrained)\s*\(\s*"""
|
|
313
|
+
r"""(?:[^)]*?(?:repo_id|model)\s*=\s*)?["']([A-Za-z0-9][\w.\-]*/[\w.\-]+)["']""")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Detectors read files and record assets, relationships, findings and AI usage signals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from aictem.catalog import BY_KEY, Tech
|
|
9
|
+
from aictem.model import Asset, Inventory, Location
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Signal:
|
|
14
|
+
"""One observed use of an AI technology."""
|
|
15
|
+
|
|
16
|
+
tech: Tech
|
|
17
|
+
method: str # dependency | import | config | endpoint | secret | container | iac
|
|
18
|
+
location: Location
|
|
19
|
+
package: str | None = None
|
|
20
|
+
version: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Context:
|
|
25
|
+
root: Path
|
|
26
|
+
inventory: Inventory
|
|
27
|
+
repo: Asset
|
|
28
|
+
repo_info: dict
|
|
29
|
+
tracked: set[str] | None = None # git-tracked paths, when git is available
|
|
30
|
+
signals: list[Signal] = field(default_factory=list)
|
|
31
|
+
models_referenced: dict[str, set[str]] = field(default_factory=dict) # tech key -> model names
|
|
32
|
+
agent_files: dict[str, dict] = field(default_factory=dict) # rel path -> {frameworks, tools, flags, name, identifiers}
|
|
33
|
+
tool_files: dict[str, list[dict]] = field(default_factory=dict) # tools defined outside any agent definition
|
|
34
|
+
mcp_tool_files: dict[str, list[dict]] = field(default_factory=dict) # tool modules of MCP servers
|
|
35
|
+
rag_markers: dict[str, list[str]] = field(default_factory=dict) # rel path -> markers
|
|
36
|
+
endpoints: dict[str, Location] = field(default_factory=dict) # origin -> first location (self-hosted inference)
|
|
37
|
+
ci_systems: set[str] = field(default_factory=set)
|
|
38
|
+
languages: dict[str, int] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
def signal(self, key: str, method: str, file: str, line: int | None = None, package: str | None = None, version: str | None = None) -> None:
|
|
41
|
+
tech = BY_KEY.get(key)
|
|
42
|
+
if tech is not None:
|
|
43
|
+
self.signals.append(Signal(tech, method, Location(file, line, method), package, version))
|
|
44
|
+
|
|
45
|
+
def is_tracked(self, rel: str) -> bool | None:
|
|
46
|
+
return None if self.tracked is None else rel in self.tracked
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def line_of(text: str, index: int) -> int:
|
|
50
|
+
return text.count("\n", 0, index) + 1
|