- Python 99.8%
- JavaScript 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build and Publish Agentyzer / delete-pr-image (push) Has been skipped
Build and Publish Agentyzer / test (push) Successful in 31s
Build and Publish Agentyzer / create-tag (push) Has been skipped
Build and Publish Agentyzer / build-push-image (push) Successful in 1m50s
Build and Publish Agentyzer / check-version (push) Has been skipped
Co-authored-by: Philipp A. Baer <philipp.a.baer@gehealthcare.com> Reviewed-on: #6 |
||
| .github/workflows | ||
| config | ||
| docs | ||
| sbom | ||
| scripts | ||
| skills | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .env.dist | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| ecosystem.config.cjs | ||
| pyproject.toml | ||
| README.md | ||
| renovate.json | ||
| start.sh | ||
| uv.lock | ||
Agentic Vulnerability Analyzer
Agentyzer is a FastAPI service and companion CLI for determining whether a repository or component is actually affected by a CVE or GHSA. It combines advisory retrieval, dependency and version inspection, source scanning, LLM-assisted reachability analysis, and verdict aggregation into a single assessment pipeline.
This repository is standalone. DTVP consumes Agentyzer through its HTTP integration; the two projects have independent versions, tests, images, and release pipelines.
Executive Summary
Agentyzer answers a narrower and more useful question than "is this dependency listed in an advisory?" The system tries to determine whether the target codebase is affected in practice by combining:
- Advisory data from OSV and GHSA.
- Dependency and lock-file presence checks.
- Version-range matching against the advisory.
- AST and text-based code scanning for vulnerable symbols and imports.
- LLM-assisted reachability and exploitability analysis.
- Final verdicting with CVSS rescoring and Dependency-Track compatible fields.
The service supports synchronous assessments for direct integrations and asynchronous job execution for UI or automation flows that need progress tracking. The CLI is a thin client over the API and can submit, poll, print, and clean up jobs.
Quick Reference
Start the API:
uv run uvicorn src.main:app --host 0.0.0.0 --port 8000
Check service health:
curl http://localhost:8000/health
uv run agentyzer health
Submit a synchronous assessment:
uv run agentyzer assess --component benchmark --vuln CVE-2024-49766 --sync
Submit an async assessment and inspect it later:
curl -X POST http://localhost:8000/assess \
-H "Content-Type: application/json" \
-d '{"component_name":"benchmark","vuln_id":"CVE-2024-49766"}'
uv run agentyzer jobs
uv run agentyzer result <job-id>
uv run agentyzer delete <job-id>
What The System Does
Agentyzer exposes two operator surfaces:
- A FastAPI application that accepts assessment requests and returns either a finished result or an async job handle.
- A CLI named
agentyzerthat talks to the running API.
At startup the service:
- Prints the packaged Agentyzer project version and image build number before importing the application.
- Loads and hot-reloads
config/repos.yamlfor component-to-repository mappings. - Validates the prompt bundles used by the LLM-assisted steps.
- Constructs the configured LLM backend from environment variables.
- Initializes an in-memory job store for async assessments.
- Performs an LLM health check and logs whether model-backed steps are available.
End-To-End Process
Process tree
Assessment request
├── Entry surface
│ ├── CLI: agentyzer assess
│ └── API: POST /assess
├── Execution mode
│ ├── sync=true
│ │ └── Run pipeline inline and return AssessResponse
│ └── sync=false
│ ├── Create in-memory job record
│ ├── Run pipeline in background task
│ ├── Poll GET /jobs/{job_id}
│ ├── Fetch GET /jobs/{job_id}/result
│ └── Cleanup DELETE /jobs/{job_id}
└── Pipeline
├── discover_vuln
├── fetch_advisory
├── filter_advisory
├── prepare_repo
├── inspect_archives
├── scan_dependencies
├── parallel branch A
│ ├── analyze_versions
│ └── what_if_remediation
├── parallel branch B
│ ├── scan_code
│ ├── llm_analyze_code
│ └── llm_deep_analyze
├── check_transitive_paths
└── aggregate_verdict
Architecture diagram
flowchart TD
U[User or automation] --> C[agentyzer CLI]
U --> A[FastAPI service]
C -->|HTTP| A
A --> J[In-memory job manager]
A --> G[Pipeline graph runner]
J --> G
G --> WF[Web fetcher\nOSV and GHSA]
G --> AI[Archive inspector]
G --> DS[Dependency scanner]
G --> CS[Code scanner and AST analyzer]
G --> VA[Version analyzer]
G --> VD[Verdict and CVSS aggregator]
G --> LLM[LLM backend\nOllama, OpenWebUI, or LiteLLM]
AI --> R[(Target repository)]
AI --> DS
AI --> CS
DS --> R
CS --> R
VA --> R
WF --> E[(External advisory sources)]
LLM --> O[(Configured model endpoint)]
VD --> RESP[AssessResponse\nsteps + assessment]
RESP --> A
Pipeline steps in detail
The implemented step order is defined in src/pipeline/graph.py and runs as follows:
discover_vuln: if the caller did not provide a vulnerability ID, query OSV for known vulnerabilities for the component and select the highest-severity candidate.fetch_advisory: fetch advisory details, affected packages, affected ranges, vulnerable symbols, and summary text.filter_advisory: determine whether the advisory is relevant to the assessed component before spending time on deeper analysis.prepare_repo: resolve the configured repository or local focus path and prepare the checkout for scanning.inspect_archives: discover supported archives inside the checkout, safely expand them into an isolated per-run workspace, and expose their manifests and source files to the normal scanners.scan_dependencies: inspect repository and extracted-archive manifests and lock files for every package named by the advisory, then select the package backed by repository evidence.scan_code: search repository and extracted-archive source for imports, symbol usage, and vulnerable API references for the selected package.llm_analyze_code: send code snippets, archive provenance, and surrounding context to the LLM to estimate reachability.llm_deep_analyze: perform a deeper LLM pass over the relevant code neighborhood to judge exploitability more carefully.analyze_versions: compare discovered versions against the advisory's explicit versions and normalized version ranges.what_if_remediation: compute candidate upgrade or mitigation directions based on the detected version state.check_transitive_paths: analyze dependency chains and intermediary packages for transitive exposure.aggregate_verdict: merge all evidence into the final assessment, including summary, reasoning, remediation, audit view, executive summary, and CVSS adjustments.
If filter_advisory concludes the advisory is not relevant, the pipeline can short-circuit directly to verdict aggregation.
Decision algorithm
The pipeline above is the execution order. The actual assessment algorithm is the evidence reduction logic that turns those step outputs into one verdict.
In simplified form, Agentyzer does this:
-
Normalize the advisory. Canonicalize source-sensitive identifiers (including lowercase GHSA payloads for OSV and GitHub), follow CVE aliases before querying NVD, and extract affected packages, affected version ranges, explicit affected versions, vulnerable symbols, and CVSS data from OSV, GHSA, and supplemental sources. Package provenance remains attached to affected ranges, explicit versions, and fixed versions so a multi-package advisory cannot mix one package's constraints into another package's analysis. OSV
fixed, inclusivelast_affected, and exclusivelimitboundaries are preserved. GitHub advisory requests useAGENTYZER_GITHUB_TOKENwhen configured; lookup failures remain explicit evidence and are not described as a source that merely omitted a description. -
Confirm dependency presence. Scan manifests and lock files to determine whether the vulnerable package is present directly, transitively, only via matching SBOM attribution, or not rediscovered locally. The configured repository/component name identifies the assessment target and is never substituted for an unresolved advisory package. Upstream SBOM attribution applies only when the selected component identity matches the advisory package; it is not transferred to another dependency name. npm manifests and package locks are parsed structurally, excluding the root package's own
namefield from dependency evidence. -
Inventory versions across the repo history. Collect the vulnerable package version from the current workspace, lock files, tags, and release branches. Manifest ranges such as
^1.2.3,>=2.0, or~=3.1are declarations, not resolved installed versions. Without a lock-file or artifact version they remain explicit unknown evidence and are not reported as concrete affected versions. When callers provideproject_versions, Agentyzer treats them as processed project-release candidates, not as vulnerable dependency versions. The deprecatedaffected_product_versionsinput alias remains accepted. Agentyzer analyzes the intersection with versions represented by exact or release-style tags/branches such asv1.2.3,1.2.3,release/1.2.3, orrelease/1.2; tag matching treats the leadingvas optional, andrelease/*branches from every available remote are eligible. Themainormasterbranch joins the intersection only when configured project-version metadata identifies it as one of the supplied releases. Unmatched project releases remain explicit unknown rows inversion_analysis.checked_versions, while verified affected and unaffected project releases are reported separately. -
Apply worst-case version matching. Compare every discovered version against the advisory ranges. Ecosystem ranges use their own version semantics: npm
SEMVER/ECOSYSTEMranges support prerelease identifiers such as21.0.0-next.0instead of being forced through the Python/PEP 440 parser. Any range that still cannot be evaluated is treated conservatively as possibly affected rather than silently skipped. If any tracked version is affected, the component is treated as version-affected overall. This is intentionally conservative: a patched workspace does not erase the fact that historical shipped releases were vulnerable.Remediation candidates stay attached to the advisory range that supplied them. When an advisory lists fixes for multiple maintained release lines, Agentyzer recommends only the fix for the line containing the detected version and never describes an older release line as an upgrade.
-
Run code and reachability analysis on the current workspace only. Source scanning, LLM reachability analysis, deep exploitability review, and transitive call-path analysis are all workspace-scoped. The system does not attempt historical code reachability analysis for old tags or release branches. Generic dependency usage from the first reachability pass is not treated as a confirmed vulnerable path when the later full-source review explicitly reports that the vulnerability-specific path is not exploitable.
-
Combine version evidence with workspace reachability. This is the key rule set:
- If the current workspace version is affected and the vulnerable functionality is reachable or plausibly reachable, the result stays in the affected bucket.
- If only historical versions are affected, that still keeps the component in scope by default.
- If only historical versions are affected but the current workspace analysis affirmatively excludes the vulnerable functionality, that workspace reachability evidence can overrule the historical version inclusion and produce
Not Affectedfor the current assessment. - If historical versions are affected and the current workspace does not provide affirmative exclusion evidence, the result remains
Probably Affectedrather thanNot Affected.
-
Apply contradiction safeguards. The final verdict logic cross-checks the LLM verdict against hard evidence. For example, a
Not Affectedverdict is overridden when reachable code or exploitability evidence contradicts it. Conversely, a historical-only affected result can remainNot Affectedonly when the workspace evidence clearly excludes runtime exploitability. Missing affected-package identity or affected version constraints forces an Inconclusive result because dependency, version, and reachability conclusions cannot be tied to the advisory. -
Rescore CVSS and format the final response. Once the final verdict is stable, Agentyzer rescales the advisory CVSS vector to reflect the assessed environment and emits a structured response with the verdict, reasoning, audit view, remediation hints, and Dependency-Track compatible fields. It also deterministically builds
executive_summary.vulnerabilityandexecutive_summary.assessmentstatements from the final post-audit facts. The assessment uses formal disposition, confidence, exposure, basis, required-action, and audit-assurance language.executive_summary.whyadds complete labeled advisory-applicability, dependency, version, release, current-workspace reachability, exploitability, transitive-path, and audit facts without character or item-count truncation.
The practical interpretation is:
- Version analysis answers: did any tracked release ship an affected version?
- Reachability analysis answers: is the affected functionality reachable in the current workspace?
- Final verdict answers: given both of those facts, is the assessed codebase affected now?
That separation is deliberate. Historical version evidence widens the set of potentially affected releases, while workspace reachability evidence is the only basis for clearing the current assessment when the workspace itself is patched.
Repository Layout
.
├── config/
│ ├── repos.yaml
│ └── prompts/
├── docker-compose.yml
├── Dockerfile
├── docs/
├── scripts/
├── skills/
├── pyproject.toml
├── repos/
│ └── ... cached or sample repositories used during analysis
├── src/
│ ├── cli.py
│ ├── http.py
│ ├── main.py
│ ├── agents/
│ ├── llm/
│ └── pipeline/
└── tests/
The OKF project-knowledge bundle lives under docs/; start at
docs/index.md. AGENTS.md and
skills/project-entrypoint/SKILL.md are routing instructions for AI agents.
The main code responsibilities are:
src/main.py: FastAPI application, request and response models, async job orchestration, startup lifecycle.src/cli.py: command-line client for health checks, assessment submission, polling, result rendering, and job cleanup.src/http.py: shared HTTP client helper that trusts the system CA store.src/agents/: dependency scanning, AST analysis, code scanning, web fetch, version analysis, CVSS rescoring, and verdict logic.src/llm/: backend abstraction plus Ollama, OpenWebUI, and LiteLLM clients.src/pipeline/: graph topology, state contract, and step nodes.
Requirements
- Python 3.14+
uv- Git
- One reachable LLM backend:
- Ollama
- OpenWebUI-compatible endpoint
- LiteLLM proxy endpoint
Installation And Local Development
Install dependencies
uv sync
Validate the knowledge bundle and run the complete test suite:
uv run python scripts/validate-okf.py docs
uv run pytest
Configure the LLM backend
Ollama is the default backend:
ollama serve
ollama pull mistral
To use OpenWebUI or a LiteLLM proxy instead, set LLM_BACKEND and the
corresponding environment variables described below. LiteLLM is consumed
through its OpenAI-compatible proxy API; for the default proxy setup:
export LLM_BACKEND=litellm
export LITELLM_HOST=http://localhost:4000
export LITELLM_MODEL=openai/gpt-4o-mini
export LITELLM_API_KEY=sk-your-proxy-key
Run the API server
uv run uvicorn src.main:app --host 0.0.0.0 --port 8000
The API will be available at http://localhost:8000.
Run the CLI against the local server
uv run agentyzer health
uv run agentyzer assess --component benchmark --vuln CVE-2024-49766 --sync
Configuration
Environment variables
| Variable | Default | Purpose |
|---|---|---|
LLM_BACKEND |
ollama |
Selects the LLM backend: ollama, openwebui, or litellm. |
OLLAMA_HOST |
http://localhost:11434 |
Base URL for Ollama. |
OLLAMA_MODEL |
mistral |
Model name used with Ollama. |
OPENWEBUI_HOST |
http://localhost:3000 |
Base URL for OpenWebUI. |
OPENWEBUI_MODEL |
mistral |
Model identifier served by OpenWebUI. |
OPENWEBUI_API_KEY |
empty | Bearer token for OpenWebUI when required. |
OPENWEBUI_TOOL_CALLS |
auto |
Native OpenAI-style research tool calls for OpenWebUI, or off to use text research directives. |
OPENWEBUI_CONTEXT_WINDOW |
0 |
Optional model context window in tokens. When set, Agentyzer pre-trims oversized OpenWebUI prompts before sending them. |
OPENWEBUI_CONTEXT_SAFETY_MARGIN |
256 |
Token margin reserved below the configured or reported context limit. |
OPENWEBUI_CONTEXT_RETRIES |
2 |
Number of retries after OpenWebUI rejects a request for exceeding context length. |
OPENWEBUI_MIN_COMPLETION_TOKENS |
256 |
Minimum completion budget to preserve when truncating input context. |
LITELLM_HOST |
http://localhost:4000 |
Base URL for the LiteLLM proxy. LITELLM_BASE_URL is accepted as an alias. |
LITELLM_MODEL |
mistral |
Model or LiteLLM model alias sent to the proxy. Provider-qualified names such as openai/gpt-4o-mini are supported. |
LITELLM_API_KEY |
empty | Bearer token for the LiteLLM proxy when authentication is enabled. |
LITELLM_API_PREFIX |
/v1 |
API path prefix for LiteLLM's OpenAI-compatible routes. Set it to empty when the host already includes the desired route prefix or the proxy exposes root routes. |
LITELLM_TOOL_CALLS |
auto |
Native OpenAI-style research tool calls for LiteLLM, or off to use text research directives. |
LOG_LEVEL |
INFO |
Standard Python logging level. |
AGENTYZER_CONFIG_DIR |
config |
Alternate config directory containing repos.yaml and prompts. |
AGENTYZER_REPOS_DIR |
repos |
Base directory for cached or reused repository workspaces. |
AGENTYZER_MAX_CONCURRENT_JOBS |
1 |
Maximum number of async or sync assessment pipelines allowed to execute at the same time. Extra async jobs remain pending until a slot opens. |
AGENTYZER_REPO_REFRESH_SECONDS |
900 |
Background clone/fetch interval for every explicit URL-backed component in repos.yaml. The first pass starts with the service; 0 disables periodic refresh and positive values have a 60-second minimum. |
AGENTYZER_GITHUB_TOKEN |
empty | Optional GitHub token for authenticated advisory API requests and higher rate limits. |
AGENTYZER_ARCHIVE_MAX_INPUT_BYTES |
1073741824 |
Maximum compressed or uncompressed input size accepted for one repository archive. |
AGENTYZER_ARCHIVE_MAX_ARCHIVES |
25 |
Maximum number of top-level and nested archives inspected in one analysis. |
AGENTYZER_ARCHIVE_MAX_NESTING |
2 |
Maximum nested-archive depth inspected after the top-level archive. |
AGENTYZER_ARCHIVE_MAX_MEMBERS |
50000 |
Maximum combined member count across repository archives in one analysis. |
AGENTYZER_ARCHIVE_MAX_MEMBER_BYTES |
268435456 |
Maximum extracted size accepted for one archive member. |
AGENTYZER_ARCHIVE_MAX_EXTRACTED_BYTES |
2147483648 |
Maximum combined extracted bytes across repository archives in one analysis. |
AGENTYZER_RESEARCH_CLONE_ENABLED |
true |
Enable bounded LLM-requested local inspection of dependent repositories. |
AGENTYZER_RESEARCH_GIT_HOSTS |
github.com,gitlab.com,bitbucket.org |
Comma-separated public HTTPS Git host allowlist; * permits any public host after address checks. |
AGENTYZER_RESEARCH_MAX_CLONES_PER_ANALYSIS |
3 |
Repository-clone requests allowed across one analysis research loop. |
AGENTYZER_RESEARCH_CLONE_TIMEOUT_SECONDS |
90 |
Per Git clone or inspection command timeout. |
AGENTYZER_RESEARCH_CLONE_MAX_REPOSITORY_MB |
256 |
Maximum disk size accepted for one cached research clone. |
AGENTYZER_RESEARCH_CLONE_MAX_FILE_BYTES |
256000 |
Largest committed text file eligible for excerpts. |
AGENTYZER_RESEARCH_CLONE_CACHE_TTL_SECONDS |
3600 |
Time a matching shallow clone can be reused without network refresh. |
Component registry
config/repos.yaml maps logical component names to repositories. Those names are what the API and CLI accept as component_name or --component values.
Example:
project_version_files:
- path: ".project.json"
field: "version"
components:
benchmark:
url: "https://git.example.com/org/benchmark.git"
clone: true
project_version_files:
- path: ".project.json"
field: "version"
- path: "config/release.json"
field: "project.release"
auth:
type: basic
username: "user"
password: "token"
Notes:
- If
focus_pathis provided, the analyzer can work against an existing local checkout instead of cloning. - Explicit URL-backed components are cloned or fetched in a background pass when the service starts and every
AGENTYZER_REPO_REFRESH_SECONDSthereafter. This pass reloadsrepos.yaml, deduplicates identical repository URLs, and continues refreshing other repositories if one fails. An assessment also performs a locked fetch immediately before resolving its detached worktree, so the scan uses a current immutable commit. Defaults templates cannot be prefetched until a concrete component name is requested. project_version_fileslists JSON metadata used to identify the project release onmainormaster. It can be set globally and overridden per component. A string entry reads that file'sversionfield; object entries acceptpathand a dottedfield. When omitted, Agentyzer reads.project.jsonandversion; use an empty list to disable default-branch version matching.- The service reloads
config/repos.yamlwhen the file modification time changes.
API Capabilities
The API is designed for two integration styles:
- Direct request-response integrations that want a finished assessment immediately.
- Job-oriented integrations that want polling, progress updates, and deferred result retrieval.
OpenAPI and interactive documentation
| Endpoint | Purpose |
|---|---|
GET /openapi.json |
Raw OpenAPI document. |
GET /docs |
Swagger UI. |
GET /redoc |
ReDoc view. |
API endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Liveness check for the API process. |
GET |
/configuration |
Sanitized service configuration and backend information for consumers. |
GET |
/prompts |
Prompt bundle metadata; pass include_values=true to include configured prompt text. |
POST |
/assess |
Submit an assessment request, synchronously or asynchronously. |
POST |
/benchmark/compare |
Probabilistically compare a human assessment artifact with an automated assessment result. |
GET |
/jobs |
List all in-memory async jobs known to the current process. |
GET |
/jobs/{job_id} |
Fetch job status and progress. |
GET |
/jobs/{job_id}/result |
Retrieve the final assessment for a completed job. |
POST |
/jobs/{job_id}/compact |
Build concise structured context from a completed job. |
POST |
/jobs/{job_id}/follow-up |
Start another assessment using the compacted parent job context plus a reviewer question. |
DELETE |
/jobs/{job_id} |
Cancel a pending/running job or remove a completed/failed/cancelled job from memory. |
Assessment request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
component_name |
string | Yes | Logical component name from config/repos.yaml, or an ad-hoc label when focus_path is used. |
vuln_id |
string | No | CVE or GHSA to assess. If omitted, the service attempts discovery. |
cvss_vector |
string | No | Override or supply the base CVSS vector for rescoring. |
focus_path |
string | No | Absolute path to a local checkout that should be analyzed instead of a configured repo. |
dependency_paths |
string[][] |
No | Dependency chains used to bias transitive reachability analysis. |
user_guidance |
string | No | Analyst context passed to every LLM-backed step. |
model |
string | No | Optional per-assessment LLM model override when the configured backend supports it. |
llm_backend |
string | No | Optional caller-supplied backend label for tracking. |
llm_provider |
string | No | Optional caller-supplied provider label for tracking. |
debug |
boolean | No | Include more detailed per-step inputs and traces in the result. |
Benchmark comparisons
POST /benchmark/compare evaluates assessment artifacts only. It does not
clone repositories, inspect dependency files, or rerun source analysis. DTVP
uses it after a normal Agentyzer assessment has produced an automated result.
The request body contains a benchmark object prepared by DTVP with:
- the current human assessment snapshot
- the saved automated assessment summary
- deterministic state, justification, and CVSS deltas
- a fallback 1-5 rating; any letter grade is a derived display alias
Agentyzer asks the configured LLM to judge the free-text reasoning and evidence
semantically, using the deterministic deltas as anchors. The response keeps the
same benchmark shape and adds evaluator metadata, comparison_method, findings,
recommendation, and an optional reasoning summary. If the LLM backend is not
healthy or returns invalid JSON, Agentyzer returns a deterministic fallback
instead of rerunning source analysis.
The benchmark judge prompt is loaded from
config/prompts/benchmark_comparison.yaml, or from a matching override under
AGENTYZER_CONFIG_DIR/prompts. The numeric rating.score is canonical; a
rating.grade value is retained only as a score-derived display alias.
Service configuration and backend information
GET /health, GET /configuration, async submission responses, and job status responses include operational metadata for consumers:
configuration: sanitized service configuration, including service name/version, config paths, repository workspace directory, configured component names/counts, alias names, and enabled interface features.backend: runtime backend details, including LLM provider/client/model/health, repository workspace reuse behavior, and in-memory job-store counts.model,llm_backend,llm_provider, andllm: compatibility fields with the configured or accepted LLM backend metadata.
The configuration payload is intentionally sanitized. It does not return repository credentials, authenticated clone URLs, or raw component auth blocks from repos.yaml.
Assessment response capabilities
AssessResponse contains two top-level sections:
assessment: the final verdict, confidence, exposure, compact executive summary with concrete verdict reasons, reasoning, CVSS output, Dependency-Track compatible fields, and optional researcher, remediation, audit, advisory relevance, and version-analysis views.steps: ordered pipeline step findings with structured findings plus evidence strings.
Important assessment payload capabilities:
- Verdict classification through
affected,verdict,confidence, andexposure. - Advisory filtering output through
advisory_relevance. - Version matching evidence through
version_analysis. - Compact executive reporting through
executive_summary.vulnerability,executive_summary.assessment, andexecutive_summary.why; this is produced after verdict guardrails, uses formal assessment terminology, selects one assessment basis and at most one required action, and then retains every naturally bounded evidence category needed to explain why the result is affected, affirmatively excluded, or still uncertain. Human renderers label the list Decision rationale, keep path claims scoped to the current workspace, and render covered product versions on one comma-separated line. The assessment payload does not character-cut or item-cut those statements. Separately generated follow-up prompt context remains bounded to the model's input budget. - Explicit product-release coverage through
version_analysis.covered_product_versions, containing only repository versions matched from the supplied product-release candidates; dependency/component versions stay in the separately labeled detected and checked-version fields. - Human-targeted reporting through
summaryandreasoning. - Machine-consumable Dependency-Track style fields through
analysis,justification,response,cvss_vector, andcvss_score. - CVSS adjustment explanation through
adjusted_cvss, including comparison traces and reasons.
Async job capabilities
Async jobs are held in memory inside the FastAPI process. That means:
- Job state is not durable across process restarts.
GET /jobsonly returns jobs known to the current API instance.GET /jobsreturns sharedconfigurationandbackendmetadata once on the response envelope; individual jobs carry job-specific request, progress, log, and LLM metadata.DELETE /jobs/{job_id}cancels apendingorrunningjob, and removes a finished job from memory.POST /jobs/{job_id}/compactextracts bounded request, verdict, CVSS, evidence, and step-finding context from a completed job so clients can reuse it without replaying the full result.POST /jobs/{job_id}/follow-upcreates a new async job from that compact context and an analyst question. DTVP uses this path for follow-up vulnerability assessment questions. The rendered compact context is capped before it is embedded into model guidance.AGENTYZER_MAX_CONCURRENT_JOBSbounds how many pipelines run at once. The default is1, matching the usual single-LLM-backend deployment. Configured repositories are isolated per run; raise the limit only when the model backend and host CPU/disk capacity can handle parallel scans.
Job status responses include:
- Lifecycle state:
pending,running,completed,failed, orcancelled. - Timestamps for creation and completion.
- Progress metrics: completed steps, total steps, percent complete.
- Current step, agent, and activity labels.
- Parallel branch visibility through
active_agentsandstep_statuses. - Recent live log entries in
logs, plus request metadata and LLM metadata (model,llm_backend,llm_provider,llm) when known. Model-wait heartbeats update one entry per pipeline step instead of growing the log every 15 seconds. Single-job status responses also include service configuration and backend information.
API usage examples
The commands in this section were smoke-tested against a local instance on 2026-05-02. Health, OpenAPI, async submission, job polling, result retrieval, CLI health, CLI job listing, CLI result retrieval, and CLI deletion all completed successfully.
Health check:
curl http://localhost:8000/health
Service configuration:
curl http://localhost:8000/configuration
Representative service configuration excerpt:
{
"status": "ok",
"model": "mistral",
"llm_provider": "ollama",
"configuration": {
"service_version": "<packaged version>",
"config_dir": "config",
"repos_config_path": "config/repos.yaml",
"repositories": {
"workspace_dir": "repos",
"component_count": 4,
"components": ["benchmark", "web"],
"hot_reload": true
}
},
"backend": {
"llm": {
"provider": "ollama",
"backend": "OllamaClient",
"host": "http://localhost:11434",
"model": "mistral",
"healthy": true
},
"repositories": {
"reuse_strategy": "stable control repository per sanitized URL plus a detached worktree for each analysis"
},
"jobs": {
"job_store": "in_memory",
"known_jobs": 0
}
}
}
Synchronous assessment:
curl -X POST "http://localhost:8000/assess?sync=true" \
-H "Content-Type: application/json" \
-d '{
"component_name": "benchmark",
"vuln_id": "CVE-2024-49766",
"debug": true,
"user_guidance": "Prioritize HTTP-reachable paths"
}'
Asynchronous assessment:
curl -X POST "http://localhost:8000/assess" \
-H "Content-Type: application/json" \
-d '{
"component_name": "benchmark",
"vuln_id": "CVE-2024-49766"
}'
curl http://localhost:8000/jobs/<job-id>
curl http://localhost:8000/jobs/<job-id>/result
curl -X DELETE http://localhost:8000/jobs/<job-id>
Local checkout assessment:
curl -X POST "http://localhost:8000/assess?sync=true" \
-H "Content-Type: application/json" \
-d '{
"component_name": "benchmark",
"vuln_id": "CVE-2024-49766",
"focus_path": "/path/to/local/repo"
}'
Representative async submission response excerpt:
{
"job_id": "9b05cd2dbd0b",
"status": "pending",
"poll_url": "/jobs/9b05cd2dbd0b"
}
Representative completed result excerpt:
{
"assessment": {
"affected": false,
"verdict": "Not Affected",
"confidence": "Low",
"exposure": "transitive",
"executive_summary": {
"vulnerability": "CVE-2024-49766 affects werkzeug. Crafted multipart form data can trigger excessive resource consumption.",
"assessment": "Not Affected (Low confidence; exposure: transitive). Audit: fail/weak. Current exclusion evidence is insufficient."
},
"summary": "AUDIT FAILURE: the downgrade to Not Affected / low-info is not supported by the available evidence. Werkzeug safe_join not safe on Windows",
"analysis": "NOT_AFFECTED",
"justification": "CODE_NOT_REACHABLE",
"cvss_score": 0.0
},
"steps": [
{
"step": "scan_dependencies",
"title": "Dependency Scan",
"status": "found"
},
{
"step": "aggregate_verdict",
"title": "Final Verdict",
"status": "Not Affected"
}
]
}
CLI Capabilities
The CLI is defined in src/cli.py and wraps the API rather than running analysis locally.
The intended CLI operator loop is:
- Point the CLI at a running API with
--urlwhen needed. - Submit an assessment.
- Inspect live jobs or fetch a completed result.
- Delete finished jobs when you no longer need them.
Commands
| Command | Capability |
|---|---|
agentyzer health |
Check whether the API is reachable. |
agentyzer assess |
Submit a vulnerability assessment. |
agentyzer jobs |
List known async jobs. |
agentyzer result <job-id> |
Fetch the result for a completed job. |
agentyzer delete <job-id> |
Cancel a running async job or delete a finished one. |
Global option
| Option | Meaning |
|---|---|
--url |
Base URL of the API server. Defaults to http://localhost:8000. |
assess options
| Option | Meaning |
|---|---|
-c, --component |
Required component name. |
-v, --vuln |
Vulnerability ID such as CVE-2024-49766. |
--cvss-vector |
Explicit CVSS vector to use for rescoring. |
--focus-path |
Path that narrows the assessment to a local checkout. |
--guidance |
Additional analyst context for LLM steps. |
--sync |
Block until the assessment finishes. |
--debug |
Print verbose debug-oriented output and traces. |
CLI examples
uv run agentyzer health
uv run agentyzer assess \
--component benchmark \
--vuln CVE-2024-49766 \
--sync
uv run agentyzer assess \
--component benchmark \
--vuln CVE-2024-49766 \
--guidance "Treat worker-triggered paths as in scope" \
--debug
uv run agentyzer jobs
uv run agentyzer result <job-id>
uv run agentyzer delete <job-id>
The CLI formatter prints:
- Verdict, confidence, and exposure.
- Advisory relevance and version analysis summaries.
- CVSS original versus adjusted score plus reasons.
- Researcher, remediation, and audit views when present.
- Pipeline step evidence, including node input summaries when debug data is available.
Representative CLI result excerpt:
✓ Not Affected (confidence: Low, exposure: transitive)
Advisory filter: relevant (rules)
Version analysis: 3.1.6 (lock file, not affected)
CVSS 4.0: 6.3 → 0.0
Researcher view:
summary: Research conclusion: Not Affected.
Audit view:
status: fail
consistency: mixed
Pipeline And Analysis Design
The pipeline state contract lives in src/pipeline/state.py and carries:
- Inputs such as
vuln_id,component_name,dependency_paths,user_guidance, andcvss_vector. - Intermediate artifacts such as advisories, dependency findings, snippets, LLM analysis, transitive analysis, and version inventory.
- Final output in
result. - Structured
step_reportsand append-onlyevidencefor auditing.
The graph wiring in src/pipeline/graph.py also records step metadata such as title, agent name, and current activity. Those labels are surfaced in async job progress responses. LLM-bound stages emit model-wait heartbeat progress while the backend is waiting for OpenWebUI, Ollama, or LiteLLM, so API clients can distinguish slow model generation from a stalled job. Each stage retains only its latest heartbeat log entry while its current activity continues to update. Persisted llm_conversation turns include request/response timestamps and directional token usage when the provider reports it; Ollama also retains its native evaluation-duration metrics. With OpenWebUI or LiteLLM tool calls enabled, research-capable LLM calls advertise bounded OpenAI-style tools (search_web, fetch_url, fetch_package, fetch_source, clone_repository); Agentyzer executes them locally through allowlisted handlers, records assistant tool calls plus returned tool messages in llm_conversation, and falls back to text FETCH_* and CLONE_REPOSITORY directives when native tool calls are unavailable. The OpenWebUI and LiteLLM backends retry one transient remote stream disconnect before reporting the model call as unavailable.
clone_repository fills the gap between a dependency-chain name and the
implementation evidence needed to judge reachability. The model supplies a
public HTTPS repository URL, a narrow search focus, and optionally a branch or
tag. Agentyzer validates the host and its resolved addresses, rejects embedded
credentials and Git ref expressions, performs a shallow filtered clone without
a worktree, enumerates and searches the complete eligible committed source and
manifest tree, and feeds matching blobs through the same language-aware
import/call-site analyzers and structural extraction as the primary checkout.
It returns bounded parser findings, matching excerpts, and commit provenance;
hooks, build scripts, submodules, package managers, and repository code are
never run. Returned content is wrapped as untrusted external evidence. Source
comments, documentation, strings, tests, filenames, and prompt-like content can
never become analyst guidance or override the trusted task, tool policy, or
response contract. Research
clones live under AGENTYZER_REPOS_DIR/research, are locked per cache path, and
are reused within the configured TTL. Private dependent repositories are not
available to the model-facing tool; configured primary repositories continue
to use the separate authenticated repos.yaml workflow.
The archive_inspector is a dedicated primary-repository tool that runs after
checkout preparation. It discovers tar archives (plain, gzip, bzip2, xz, or
zstd), ZIP-compatible packages (including JAR, WAR, EAR, wheel, NuGet, APK,
and AAR), 7z files, and RAR files inside the checkout. Supported members are
expanded beneath a unique __agentyzer_archives__/<analysis-id> directory so
the existing dependency, version, AST, and code scanners see the contents and
retain archive provenance. Nested archives share the same analysis limits.
Archive contents are never executed. The extractor rejects traversal paths,
links, special filesystem entries, encrypted members, duplicate files, and
configured input, member, expansion, nesting, or count limits. A malformed or
unsupported archive is recorded as partial evidence while other archives
continue. The generated directory is removed with the per-run worktree; for a
caller-supplied focus_path, Agentyzer removes only its own analysis directory.
All LLM prompts are managed as YAML bundles in config/prompts/. Prompt bundles use compact analysis_protocol sections instead of bundled few-shot example transcripts. The protocol tells the model to keep analysis private, apply security researcher/remediator/auditor/ticket-author lenses internally, and emit only structured evidence fields such as call paths, dependency chains, exclusions, remediation, and validation notes. Response contracts define exact field order, allowed values, evidence labels, and disallow markdown, JSON, preambles, conclusions, or extra fields. Legacy custom prompt bundles that still provide few_shot are accepted as a compatibility alias for analysis_protocol.
OpenWebUI context limits are handled in two ways. If OpenWebUI rejects a request
with a context-length error, Agentyzer parses the reported model limit and input
or request token count, then retries with a lower completion budget or a
compacted prompt. The reported limit is retained for later calls, including the
request (…) exceeds the available context size (…) response emitted by some
OpenWebUI model backends.
For models with a known window, set OPENWEBUI_CONTEXT_WINDOW to enable
preflight prompt compaction before the request is sent. The estimate is
conservative for code, reserves the requested completion budget, and generated
AST evidence is independently capped with an explicit omission marker. Full AST
scan counts remain in pipeline evidence. For example, a model with a
131072-token context can use OPENWEBUI_CONTEXT_WINDOW=131072.
Parallel Project Runs And Workspace Reuse
Multiple async jobs can be submitted to the FastAPI process. Accepted jobs are kept in the in-memory job store; at most AGENTYZER_MAX_CONCURRENT_JOBS pipelines run at the same time, and the rest stay pending until an execution slot opens. The default is 1 because DTVP's packaged deployment usually points Agentyzer at one LLM backend. Increase the value only when the model backend, CPU/disk resources, and repository workspace strategy can support parallel scans.
Inside one running pipeline, Agentyzer still exposes parallel branch visibility through progress.active_agents and progress.step_statuses. After archive inspection, the dependency scan selects the affected advisory package found in the repository; the graph then fans out into version and code/LLM branches using that same package. Filesystem-heavy dependency, AST, usage, and structure scans run off the async event loop so both branches make progress concurrently. Usage hits are reused for snippet collection instead of walking the source tree a second time.
Repository data is reused across runs without sharing a mutable scan directory.
src/agents/dependency_scanner.py maps each repository URL to a stable control
repository under AGENTYZER_REPOS_DIR (default: repos) using the repo name
plus a SHA-256 hash of the sanitized URL. Initial clones are built in a
temporary directory and atomically moved into place. Existing caches are
fetched without resetting their working trees. A background task performs that
clone/fetch pass at service startup and at the configured refresh interval,
even when no assessment reaches repository preparation. Logs identify refresh
cycle start/completion and each component's cache and resolved commit.
Repository preparation takes a filesystem-backed, per-repository advisory
lock before cloning or fetching, resolving the remote default branch to a
commit, and registering a detached git worktree. The lock coordinates API
processes or containers that use the same repository volume, provided the
shared filesystem supports advisory file locks. Each pipeline scans only its
own worktree under .worktrees/<repository-key>/<analysis-id>, so a later
fetch cannot change files being analyzed by an earlier run. Git objects remain
shared through the control repository instead of being cloned for every
component.
Active worktrees hold OS-backed lease files. Normal completion, errors, and cancellation remove the worktree in the pipeline's cleanup path. If a process crashes, the OS releases its lease; the next preparation of that repository reclaims the abandoned checkout and prunes stale Git worktree metadata.
Raising AGENTYZER_MAX_CONCURRENT_JOBS is therefore repository-safe for
configured repositories that share either identical or different URLs. The
remaining shared bottlenecks are CPU and disk IO, plus configured LLM backend
throughput; the global execution limit remains process-local and there is not
yet a separate LLM-stage rate limiter. User-supplied focus_path checkouts are
not managed by this cache and remain the caller's concurrency responsibility.
Docker Setup
Dockerfile behavior
The top-level Dockerfile:
- Uses
ghcr.io/astral-sh/uv:python3.14-trixie-slimas the base image. - Installs Git for repository operations and
unrar-freefor RAR inspection. - Optionally mounts a CA certificate secret at build time and installs it into the system trust store.
- Installs locked production dependencies with
uv sync --frozen --no-dev. - Copies
config/andsrc/into/app. - Sets
PYTHONPATH=/app,AGENTYZER_CONFIG_DIR=/app/config, andAGENTYZER_REPOS_DIR=/app/repos. - Exposes port
8000. - Starts the service with
uv run --no-sync uvicorn src.main:app --host 0.0.0.0 --port 8000.
Build example:
docker build -t agentyzer .
docker run --rm -p 8000:8000 agentyzer
Docker Compose behavior
docker-compose.yml defines one service named analyzer that:
- Builds from the local Dockerfile.
- Publishes container port
8000to host port8000. - Mounts
./configinto/app/configas read-only. - Points the containerized service at an Ollama instance by default via
OLLAMA_HOST=http://host.docker.internal:11434; setLLM_BACKEND=litellmand theLITELLM_*variables to use a LiteLLM proxy instead. - Defaults to one assessment execution slot through
AGENTYZER_MAX_CONCURRENT_JOBS=1.
Run it with:
docker compose up --build
If host.docker.internal is not available on your Linux setup, adjust OLLAMA_HOST to a reachable address for your host or model container. For a LiteLLM deployment, set LITELLM_HOST to a URL reachable from the container and provide LITELLM_API_KEY when required.
CI/CD Pipeline
.github/workflows/build-publish.yml validates the OKF bundle, runs the Python
tests, builds the container, and publishes independent dev, PR, latest,
and versioned image tags. The workflow reads the package version from this
repository's pyproject.toml; it does not depend on DTVP's checkout or release
version. It omits the GitHub-only permissions field, which Forgejo ignores.
Configure the runner registry variables and push token expected by the workflow
before enabling publication; if those credentials are replaced with Forgejo's
capability-based workflow authorization, grant the required capabilities
through an Authorized Integration.
Testing
Run the current automated test suite with:
uv run pytest
Useful variants:
uv run pytest -v
uv run pytest tests/test_cli.py -v
uv run pytest --junitxml=test-reports/results.xml
Typical Operator Flows
Quick triage
- Start the API.
- Confirm health with
agentyzer healthorGET /health. - Run a synchronous assessment for a single CVE.
- Inspect
assessment.summary,reasoning,version_analysis, andadjusted_cvss;version_analysis.checked_versionslists workspace, tag, branch, and caller-supplied product-version coverage.
Longer-running async analysis
- Submit
POST /assesswithoutsync=true. - Poll
GET /jobs/{job_id}untilstatus=completed. - Fetch the final result from
GET /jobs/{job_id}/result. - Delete the job when it is no longer needed.
Local repository investigation
- Prepare a local checkout of the target repository.
- Submit the assessment with
focus_pathor--focus-path. - Optionally add
user_guidanceor--guidancefor environment-specific context. - Enable
debugor--debugwhen you need deeper step-level evidence.
Notes And Constraints
- Async jobs are process-local and not persisted.
- LLM-backed stages depend on backend reachability; startup logs warn when the backend is unavailable.
- The CLI does not perform scanning itself; it only calls the API.
focus_pathis documented as an absolute path and is the safest way to assess an already checked-out repository.- When
AGENTYZER_MAX_CONCURRENT_JOBSis raised, configured repositories use isolated worktrees even when jobs share one repository URL. Concurrent use of a caller-suppliedfocus_pathremains caller-managed. - The service trusts the system CA store for outbound HTTP calls and can also consume injected CA certificates at image-build time.
- The shipped
config/repos.yamlin this repository is intentionally empty. Populate it with environment-specific component mappings, and keep credential-bearing variants out of public branches.