Agentic LLM-based source code analyzer (local LLM via ollama or OpenWebUI)
  • Python 99.8%
  • JavaScript 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Philipp A. Baer 7c0d4b5418
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
fix: bump version to 1.0.25 and update some dependencies (#6)
Co-authored-by: Philipp A. Baer <philipp.a.baer@gehealthcare.com>
Reviewed-on: #6
2026-09-18 15:30:18 +00:00
.github/workflows Update actions/checkout action to v7 (#4) 2026-09-16 05:44:20 +00:00
config feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
docs feat: add litellm support (#1) 2026-09-11 10:11:03 +00:00
sbom feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
scripts feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
skills feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
src feat: add litellm support (#1) 2026-09-11 10:11:03 +00:00
tests feat: add litellm support (#1) 2026-09-11 10:11:03 +00:00
.dockerignore feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
.env.dist feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
.gitignore feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
AGENTS.md feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
docker-compose.yml feat: add litellm support (#1) 2026-09-11 10:11:03 +00:00
Dockerfile feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
ecosystem.config.cjs feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
pyproject.toml fix: bump version to 1.0.25 and update some dependencies (#6) 2026-09-18 15:30:18 +00:00
README.md feat: add litellm support (#1) 2026-09-11 10:11:03 +00:00
renovate.json Configure Renovate (#2) 2026-09-13 03:26:49 +00:00
start.sh feat: moved agentyzer from dtvp 2026-09-11 09:54:15 +02:00
uv.lock fix: bump version to 1.0.25 and update some dependencies (#6) 2026-09-18 15:30:18 +00:00

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 agentyzer that talks to the running API.

At startup the service:

  1. Prints the packaged Agentyzer project version and image build number before importing the application.
  2. Loads and hot-reloads config/repos.yaml for component-to-repository mappings.
  3. Validates the prompt bundles used by the LLM-assisted steps.
  4. Constructs the configured LLM backend from environment variables.
  5. Initializes an in-memory job store for async assessments.
  6. 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:

  1. 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.
  2. fetch_advisory: fetch advisory details, affected packages, affected ranges, vulnerable symbols, and summary text.
  3. filter_advisory: determine whether the advisory is relevant to the assessed component before spending time on deeper analysis.
  4. prepare_repo: resolve the configured repository or local focus path and prepare the checkout for scanning.
  5. 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.
  6. 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.
  7. scan_code: search repository and extracted-archive source for imports, symbol usage, and vulnerable API references for the selected package.
  8. llm_analyze_code: send code snippets, archive provenance, and surrounding context to the LLM to estimate reachability.
  9. llm_deep_analyze: perform a deeper LLM pass over the relevant code neighborhood to judge exploitability more carefully.
  10. analyze_versions: compare discovered versions against the advisory's explicit versions and normalized version ranges.
  11. what_if_remediation: compute candidate upgrade or mitigation directions based on the detected version state.
  12. check_transitive_paths: analyze dependency chains and intermediary packages for transitive exposure.
  13. 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:

  1. 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, inclusive last_affected, and exclusive limit boundaries are preserved. GitHub advisory requests use AGENTYZER_GITHUB_TOKEN when configured; lookup failures remain explicit evidence and are not described as a source that merely omitted a description.

  2. 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 name field from dependency evidence.

  3. 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.1 are 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 provide project_versions, Agentyzer treats them as processed project-release candidates, not as vulnerable dependency versions. The deprecated affected_product_versions input alias remains accepted. Agentyzer analyzes the intersection with versions represented by exact or release-style tags/branches such as v1.2.3, 1.2.3, release/1.2.3, or release/1.2; tag matching treats the leading v as optional, and release/* branches from every available remote are eligible. The main or master branch joins the intersection only when configured project-version metadata identifies it as one of the supplied releases. Unmatched project releases remain explicit unknown rows in version_analysis.checked_versions, while verified affected and unaffected project releases are reported separately.

  4. Apply worst-case version matching. Compare every discovered version against the advisory ranges. Ecosystem ranges use their own version semantics: npm SEMVER/ECOSYSTEM ranges support prerelease identifiers such as 21.0.0-next.0 instead 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.

  5. 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.

  6. 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 Affected for the current assessment.
  • If historical versions are affected and the current workspace does not provide affirmative exclusion evidence, the result remains Probably Affected rather than Not Affected.
  1. Apply contradiction safeguards. The final verdict logic cross-checks the LLM verdict against hard evidence. For example, a Not Affected verdict is overridden when reachable code or exploitability evidence contradicts it. Conversely, a historical-only affected result can remain Not Affected only 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.

  2. 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.vulnerability and executive_summary.assessment statements from the final post-audit facts. The assessment uses formal disposition, confidence, exposure, basis, required-action, and audit-assurance language. executive_summary.why adds 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_path is 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_SECONDS thereafter. This pass reloads repos.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_files lists JSON metadata used to identify the project release on main or master. It can be set globally and overridden per component. A string entry reads that file's version field; object entries accept path and a dotted field. When omitted, Agentyzer reads .project.json and version; use an empty list to disable default-branch version matching.
  • The service reloads config/repos.yaml when 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, and llm: 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, and exposure.
  • Advisory filtering output through advisory_relevance.
  • Version matching evidence through version_analysis.
  • Compact executive reporting through executive_summary.vulnerability, executive_summary.assessment, and executive_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 summary and reasoning.
  • Machine-consumable Dependency-Track style fields through analysis, justification, response, cvss_vector, and cvss_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 /jobs only returns jobs known to the current API instance.
  • GET /jobs returns shared configuration and backend metadata once on the response envelope; individual jobs carry job-specific request, progress, log, and LLM metadata.
  • DELETE /jobs/{job_id} cancels a pending or running job, and removes a finished job from memory.
  • POST /jobs/{job_id}/compact extracts 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-up creates 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_JOBS bounds how many pipelines run at once. The default is 1, 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, or cancelled.
  • Timestamps for creation and completion.
  • Progress metrics: completed steps, total steps, percent complete.
  • Current step, agent, and activity labels.
  • Parallel branch visibility through active_agents and step_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:

  1. Point the CLI at a running API with --url when needed.
  2. Submit an assessment.
  3. Inspect live jobs or fetch a completed result.
  4. 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, and cvss_vector.
  • Intermediate artifacts such as advisories, dependency findings, snippets, LLM analysis, transitive analysis, and version inventory.
  • Final output in result.
  • Structured step_reports and append-only evidence for 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:

  1. Uses ghcr.io/astral-sh/uv:python3.14-trixie-slim as the base image.
  2. Installs Git for repository operations and unrar-free for RAR inspection.
  3. Optionally mounts a CA certificate secret at build time and installs it into the system trust store.
  4. Installs locked production dependencies with uv sync --frozen --no-dev.
  5. Copies config/ and src/ into /app.
  6. Sets PYTHONPATH=/app, AGENTYZER_CONFIG_DIR=/app/config, and AGENTYZER_REPOS_DIR=/app/repos.
  7. Exposes port 8000.
  8. 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 8000 to host port 8000.
  • Mounts ./config into /app/config as read-only.
  • Points the containerized service at an Ollama instance by default via OLLAMA_HOST=http://host.docker.internal:11434; set LLM_BACKEND=litellm and the LITELLM_* 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

  1. Start the API.
  2. Confirm health with agentyzer health or GET /health.
  3. Run a synchronous assessment for a single CVE.
  4. Inspect assessment.summary, reasoning, version_analysis, and adjusted_cvss; version_analysis.checked_versions lists workspace, tag, branch, and caller-supplied product-version coverage.

Longer-running async analysis

  1. Submit POST /assess without sync=true.
  2. Poll GET /jobs/{job_id} until status=completed.
  3. Fetch the final result from GET /jobs/{job_id}/result.
  4. Delete the job when it is no longer needed.

Local repository investigation

  1. Prepare a local checkout of the target repository.
  2. Submit the assessment with focus_path or --focus-path.
  3. Optionally add user_guidance or --guidance for environment-specific context.
  4. Enable debug or --debug when 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_path is documented as an absolute path and is the safest way to assess an already checked-out repository.
  • When AGENTYZER_MAX_CONCURRENT_JOBS is raised, configured repositories use isolated worktrees even when jobs share one repository URL. Concurrent use of a caller-supplied focus_path remains 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.yaml in this repository is intentionally empty. Populate it with environment-specific component mappings, and keep credential-bearing variants out of public branches.