DEV Community

jaryn
jaryn

Posted on

Triage 487 Dependency Alerts With a Free Model and a Free Server

Wednesday 14:20, and the dependency scanner flags 487 alerts across 31 services. Two engineers are already fighting an incident, and a compliance review lands Friday. The team's first instinct is a commercial triage product with a per-seat license and a data-processing addendum. A more auditable option is a free-tier AI workspace: MonkeyCode offers free models with a 10,000,000-token monthly allowance and a free hosted server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow below treats that free tier as a disposable triage engine, not as a production security control.

The bottleneck is the decision, not the scan

A scanner produces alerts; a human produces decisions. The bottleneck is the decision, not the scan. A free model can draft the decision, and a free server can run the draft pipeline without touching a production VM. The point is to convert 487 alerts into a ranked list of 12 that need human eyes.

Three rules keep the pipeline honest. First, never send source code, only package names, versions, and CVE IDs. Second, never send the full SBOM, only the diff since the last scan. Third, always log the model's confidence per alert so a wrong verdict stays traceable.

Pipeline shape

The pipeline has five stages and one deliberate gap:

scanner JSON -> normalize -> free model (triage) -> ranked CSV -> human review
                    ^
              free server (MonkeyCode)
Enter fullscreen mode Exit fullscreen mode

The deliberate gap sits between the model and the human. The model never writes to the ticket system; it writes a CSV that a human imports. That gap costs ten minutes and prevents an automated bad verdict from becoming an automated bad ticket.

The triage script

Python, no framework, stdlib plus urllib. The script reads a JSON array of alerts, builds a prompt per alert, and writes a ranked CSV. It fails open to "escalate" because a model error should cost attention, not silence.

# triage.py
import csv, json, os, sys
from urllib import request

MODEL_URL = os.environ["MODEL_URL"]  # MonkeyCode free model endpoint
ALERTS = json.load(open(sys.argv[1]))

PROMPT_TEMPLATE = """You are a dependency triage assistant. Classify this alert:
Package: {package} {version}
CVE: {cve}
CVSS: {cvss}
Fix available: {fix}
Return JSON: {{"verdict": "review|accept|escalate", "reason": "one sentence"}}"""

def call_model(alert):
    payload = json.dumps({
        "messages": [{"role": "user", "content": PROMPT_TEMPLATE.format(**alert)}],
        "temperature": 0,
    }).encode()
    req = request.Request(MODEL_URL, data=payload, headers={"content-type": "application/json"})
    with request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())["choices"][0]["message"]["content"]

rows = []
for a in ALERTS:
    try:
        verdict = json.loads(call_model(a))
        rows.append({**a, "verdict": verdict["verdict"], "reason": verdict["reason"]})
    except Exception as e:
        rows.append({**a, "verdict": "escalate", "reason": f"model error: {e}"})

with open("triage.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
    w.writeheader()
    w.writerows(rows)
Enter fullscreen mode Exit fullscreen mode

The temperature is pinned to zero because a triage verdict should be deterministic. The timeout is pinned to thirty seconds because a hung model should not hang the pipeline. The exception handler is the most important line: any failure becomes "escalate", never "accept".

Token budget math

The free model allowance is 10,000,000 tokens as of 2026-08-28. A triage call with a 200-token prompt and a 100-token response costs about 300 tokens per alert. 487 alerts consume roughly 146,000 tokens, about 1.5 percent of the monthly allowance. The budget holds even with a weekly full re-scan:

Workload Alerts Tokens % of 10M
Daily diff triage 50 15K 0.15%
Weekly full re-scan 500 150K 1.5%
Incident retro (paste-free) 20 6K 0.06%

The arithmetic matters because a free tier is only free until the quota runs out. When the allowance resets, the pipeline should re-run the weekly scan automatically. When it is exhausted, the script should fail closed to "escalate" rather than silently skipping alerts.

Free server isolation

The free server option runs the cron job. Treat it as untrusted infrastructure: no production credentials, no SSH keys, outbound network restricted to the model endpoint, and scanner output copied in via a one-way sync. The server is disposable; the CSV is the artifact.

Deploying the pipeline on the free server takes five steps. First, create a directory for the scanner output and the CSV. Second, set MODEL_URL and a scoped API key in the server environment. Third, install the script and the test suite. Fourth, add a cron entry that runs the test suite before the triage at 06:00 UTC. Fifth, configure a one-way sync that pulls the CSV to the internal network.

The failing test

A triage pipeline without a regression test is a suggestion. The test suite needs a positive fixture that must escalate and a negative fixture that must not. The fixtures below use real CVEs with known characteristics: Log4Shell is a remote code execution with a public PoC, while the moment case is a higher-CVSS but lower-exploitability finding.

# test_triage.py
import json
from triage import call_model

FIXTURES = [
    {"package": "log4j", "version": "2.14.1", "cve": "CVE-2021-44228",
     "cvss": "10.0", "fix": "yes", "expect": "escalate"},
    {"package": "moment", "version": "2.29.1", "cve": "CVE-2022-31129",
     "cvss": "7.8", "fix": "yes", "expect": "review"},
]

for fx in FIXTURES:
    verdict = json.loads(call_model(fx))
    assert verdict["verdict"] == fx["expect"], \
        f"{fx['cve']}: got {verdict['verdict']}, want {fx['expect']}"
Enter fullscreen mode Exit fullscreen mode

The expected values encode the team's policy, not an objective truth; a team that escalates every ReDoS can flip the second fixture. The assertion is the gate. If the free model changes behavior or the endpoint drifts, the test fails before the pipeline touches a real alert. That is the difference between a demo and a control.

Prevent, detect, recover

Phase Control
Prevent no source code in prompts; token budget cap; server with no prod credentials
Detect confidence log per alert; CSV diff vs last run; alert if model error rate > 5%
Recover rotate the model API key; re-run triage on the CSV; escalate everything the model touched if a leak is suspected

The recover column assumes the CSV survives. Store it outside the free server, because a disposable server can vanish with the only copy of the verdicts.

Limitations and who should not use this

A free model is not a vulnerability scanner; it cannot see the codebase, and it will hallucinate CVSS scores if the input lacks them. The pipeline inherits every weakness of the scanner that feeds it. Never use this for PCI, HIPAA, or customer-PII workloads, and never use it for alerts where a wrong "accept" means a breach. If the team cannot read the model's JSON output and spot a bad verdict, the pipeline is a liability, not a tool.

The approach also assumes the scanner output is structured. A PDF report or a proprietary format requires a normalization step that this script does not include. Teams with fewer than fifty alerts a week should skip the pipeline entirely and review the list by hand; the setup cost outweighs the saved attention.

The boundary question

The threshold between "review" and "escalate" can live in the prompt or in the CSV post-processor. My take: keep the prompt dumb and enforce the threshold in code, because a prompt is a string and a test is a gate. A useful next step is to point the script at MonkeyCode's free model endpoint, run the two fixtures above, and see whether the verdicts hold; the whole experiment costs a few thousand tokens and one coffee.

Top comments (0)