https://vines.rosebird.orgAuth: Bearer token in
Authorization header: Authorization: Bearer <YOUR_TOKEN>Generate your token in the Vines portal → Profile > API Tokens.
Contents
Quickstart
Start a scan
curl -sS -X POST https://vines.rosebird.org/api/v1/scans \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"target":"https://example.com","scan_type":"both"}'
# → {"status":"success","scan_id":"...","target":"https://example.com"}
Python
import requests
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
h = {"Authorization": f"Bearer {TOKEN}"}
payload = {"target": "https://example.com", "scan_type": "both"}
r = requests.post(f"{BASE}/api/v1/scans", json=payload, headers=h)
print(r.json()) # {"status":"success","scan_id":"...","target":"https://example.com"}
Wait for completion
SCAN="<scan_id_from_previous_step>"
while true; do
curl -sS "https://vines.rosebird.org/api/v1/scan/$SCAN/status" \
-H "Authorization: Bearer <YOUR_TOKEN>" | jq .
sleep 3
done
Python
import time, requests
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
SCAN = "<scan_id_from_previous_step>"
h = {"Authorization": f"Bearer {TOKEN}"}
while True:
r = requests.get(f"{BASE}/api/v1/scan/{SCAN}/status", headers=h)
print(r.json())
time.sleep(3)
Fetch report (JSON)
curl -sS "https://vines.rosebird.org/api/v1/report/$SCAN/json" \
-H "Authorization: Bearer <YOUR_TOKEN>" -o report.json
Python
import requests, json
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
SCAN = "<scan_id_from_previous_step>"
h = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get(f"{BASE}/api/v1/report/{SCAN}/json", headers=h)
open("report.json","w").write(json.dumps(r.json(), indent=2))
Fetch report (PDF)
curl -L "https://vines.rosebird.org/api/v1/report/$SCAN/pdf" \
-H "Authorization: Bearer <YOUR_TOKEN>" -o report.pdf
Python
import requests
BASE = "https://vines.rosebird.org"
TOKEN = "YOUR_TOKEN"
SCAN = "<scan_id_from_previous_step>"
h = {"Authorization": f"Bearer {TOKEN}"}
r = requests.get(f"{BASE}/api/v1/report/{SCAN}/pdf", headers=h)
open("report.pdf","wb").write(r.content)
Authentication
All API calls require a Bearer token:
Authorization: Bearer <YOUR_TOKEN>
Get your token in Profile → API Tokens. Store it in CI/CD secret managers (GitHub Secrets, GitLab Variables, Jenkins Credentials, Azure Library, CircleCI Contexts, etc.).
Endpoints
/api/v1/scans
Description: Start a scan. Respects plan & capacity limits.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
target | string | yes | URL or host (e.g., https://example.com) |
scan_type | string | no | nmap | zap | both (default) |
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
curl -sS -X POST "$BASE/api/v1/scans" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"https://example.com","scan_type":"both"}'
sample response
{"status":"success","scan_id":"9106bad7-fda6-464e-980f-3d7154409a69","target":"https://example.com"}
/api/v1/reports
?limit=1..100 (default 25)
Description: List your scans (id, target, date, status, counts, ports).
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
curl -sS "$BASE/api/v1/reports?limit=10" \
-H "Authorization: Bearer $TOKEN" | jq .
sample response (trimmed)
[
{
"scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
"target": "https://niles.rosebird.org",
"scan_date": "2025-08-26T05:46:40Z",
"status": "completed",
"vuln_count": 9,
"open_ports": 4,
"max_risk_score": 5.0
}
]
/api/v1/scan/<scan_id>/status
Description: Poll current scan status.
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS "$BASE/api/v1/scan/$SCAN_ID/status" \
-H "Authorization: Bearer $TOKEN" | jq .
sample response
{"status":"running","message":"Web scan (quick)…","progress":55,"target":"https://example.com"}
/api/v1/scans/active
Description: Snapshot of your initializing/running scans.
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
curl -sS "$BASE/api/v1/scans/active" -H "Authorization: Bearer $TOKEN" | jq .
sample response
[]
/api/v1/scan/<scan_id>/abort
Description: Abort a running scan (idempotent).
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS -X POST "$BASE/api/v1/scan/$SCAN_ID/abort" \
-H "Authorization: Bearer $TOKEN" | jq .
sample response
{"ok":true}
/api/v1/report/<scan_id>/json
Description: Full report JSON (scan_info, open_ports, vulnerabilities, risk_scores).
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS "$BASE/api/v1/report/$SCAN_ID/json" \
-H "Authorization: Bearer $TOKEN" | jq .
sample response (trimmed)
{
"scan_info": {
"scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
"target": "https://niles.rosebird.org",
"scan_date": "2025-08-26 05:46:40",
"status": "completed"
},
"open_ports": [
{"port":80,"protocol":"tcp","service":"http","product":"nginx","version":"1.23.x"},
{"port":443,"protocol":"tcp","service":"https","product":"nginx","version":"1.23.x"}
],
"vulnerabilities": [
{
"source":"zap",
"name":"Content Security Policy (CSP) Header Not Set",
"severity":"medium",
"cvss_score":5.0,
"details":{"cwe_id":"CWE-693","url":"https://niles.rosebird.org/","instances":[{"url":"https://niles.rosebird.org/"}]}
}
],
"risk_scores": { "max":5.0, "overall_score":3.26,
"summary":{"critical":0,"high":0,"medium":2,"low":3,"info":4} }
}
/api/v1/report/<scan_id>/pdf
Description: PDF report stream.
curl
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sSL "$BASE/api/v1/report/$SCAN_ID/pdf" \
-H "Authorization: Bearer $TOKEN" -o report.pdf
headers
Content-Type: application/pdf
Content-Disposition: inline; filename="Vulnerability_Vines_YYYY-MM-DD.pdf"
KPIs & CVSS
/api/v1/scan/<scan_id>/kpi
Description: KPIs for a single scan (same JSON shape as /kpi/latest), filtered by scan_id.
curl examples
# Env-based (recommended)
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/scan/$SCAN_ID/kpi" | jq .
# One-liner (shows HTTP code and avoids jq errors on HTML)
curl -sS -w "\nHTTP %{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/scan/$SCAN_ID/kpi" | (jq . 2>/dev/null || cat)
sample response (trimmed)
{
"cvss": { "max": 5.0, "mean": 2.56 },
"risk_scores": { "max": 5.0, "overall_score": 3.26,
"summary": { "critical": 0, "high": 0, "medium": 2, "low": 3, "info": 4 } },
"scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
"target": "https://niles.rosebird.org",
"scan_date": "2025-08-26 05:46:40",
"totals": { "vulnerabilities": 9, "open_ports": 4 }
}
/api/v1/kpi/latest
?limit=1..50 (default 1)
Description: Latest N scans’ KPIs for the authenticated user.
curl examples
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
# Latest scan KPI
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/kpi/latest" | jq .
# Latest 10 scan KPIs
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/kpi/latest?limit=10" | jq .
KPI + Vulnerability Titles
/api/v1/scan/<scan_id>/kpi+titles
Description: Returns KPIs (counts, CVSS, risk summary) for a single scan, plus a lightweight list of vulnerabilities
with title and severity. No sensitive details or evidence are included.
curl example
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN_ID="9106bad7-fda6-464e-980f-3d7154409a69"
curl -sS -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/scan/$SCAN_ID/kpi+titles" | jq .
Sample Response
{
"cvss": { "max": 5.0, "mean": 2.56 },
"risk_scores": {
"max": 5.0,
"overall_score": 3.26,
"summary": { "critical": 0, "high": 0, "info": 4, "low": 3, "medium": 2 }
},
"scan_date": "2025-08-26 05:46:40",
"scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
"target": "https://niles.rosebird.org",
"totals": {
"critical": 0,
"high": 0,
"info": 4,
"low": 3,
"medium": 2,
"open_ports": 4,
"vulnerabilities": 9
},
"vulnerabilities": [
{ "severity": "medium", "title": "Content Security Policy (CSP) Header Not Set" },
{ "severity": "medium", "title": "Missing Anti-clickjacking Header" },
{ "severity": "low", "title": "Strict-Transport-Security Header Not Set" },
{ "severity": "low", "title": "Timestamp Disclosure - Unix" },
{ "severity": "low", "title": "X-Content-Type-Options Header Missing" },
{ "severity": "info", "title": "Information Disclosure - Suspicious Comments" },
{ "severity": "info", "title": "Modern Web Application" },
{ "severity": "info", "title": "Re-examine Cache-control Directives" },
{ "severity": "info", "title": "User Agent Fuzzer" }
]
}
Json Sample Responses
Report JSON (trimmed)
Click to expand JSON
{
"scan_info": {
"scan_id": "9106bad7-fda6-464e-980f-3d7154409a69",
"target": "https://niles.rosebird.org",
"scan_date": "2025-08-26 05:46:40",
"has_zap": true,
"is_nmap_only": false
},
"open_ports": [
{"port":80,"protocol":"tcp","service":"http","product":"nginx","version":"1.23.x"},
{"port":443,"protocol":"tcp","service":"https","product":"nginx","version":"1.23.x"}
],
"vulnerabilities": [
{
"source":"zap",
"name":"Content Security Policy (CSP) Header Not Set",
"severity":"medium",
"cvss_score":5.0,
"details":{
"cwe_id":"CWE-693",
"url":"https://niles.rosebird.org/",
"instances":[{"url":"https://niles.rosebird.org/"}]
}
}
],
"risk_scores": {
"max":5.0,
"overall_score":3.26,
"summary":{"critical":0,"high":0,"medium":2,"low":3,"info":4}
}
}
Report PDF
Headers: Content-Type: application/pdf, Content-Disposition: inline; filename="Vulnerability_Vines_YYYY-MM-DD.pdf"
curl
curl -L "https://vines.rosebird.org/api/v1/report/$SCAN/pdf" \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-o report.pdf
Python
import requests
h = {"Authorization": "Bearer YOUR_TOKEN"}
r = requests.get(f"https://vines.rosebird.org/api/v1/report/{SCAN}/pdf", headers=h)
with open("report.pdf","wb") as f: f.write(r.content)
Node
import fs from 'node:fs';
const res = await fetch(`${BASE}/api/v1/report/${SCAN}/pdf`, {
headers: { Authorization: `Bearer ${TOKEN}` }
});
const buf = Buffer.from(await res.arrayBuffer());
fs.writeFileSync('report.pdf', buf);
CI/CD Gates
/api/v1/scan/<scan_id>/gate
Description: Returns pass/fail (ok) and reasons, based on query thresholds.
Typical policies
| Environment | Query |
|---|---|
| Prod strict | ?max_critical=0&max_high=0&max_cvss=8.9 |
| Staging | ?max_critical=0&max_high=2&max_cvss=9.4 |
| Hardened surface | ?max_open_ports=0 |
/api/v1/kpi/latest/gate
Description: Gate on the latest scan for your token’s user. Same query params.
CI/CD Integration Examples
Store your token in secrets. Each example exits non-zero when the gate fails.
Generic bash
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN/gate?max_critical=0&max_cvss=8.9" \
-H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq .
if [ "$(echo "$RESP" | jq -r '.ok')" != "true" ]; then
echo "Gate failed: $(echo "$RESP" | jq -r '.reasons | join(\"; \")')" >&2
exit 1
fi
GitHub Actions
- name: Gate latest scan
env: { BASE: https://vines.rosebird.org, VINES_TOKEN: ${{ secrets.VINES_TOKEN }}, SCAN_ID: ${{ vars.VINES_SCAN_ID }} }
run: |
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
echo "$RESP" | jq .
test "$(echo "$RESP" | jq -r '.ok')" = "true"
GitLab CI
vines_gate:
image: alpine:latest
stage: test
before_script: [ 'apk add --no-cache curl jq' ]
script:
- RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
- echo "$RESP" | jq .
- test "$(echo "$RESP" | jq -r '.ok')" = "true"
Jenkins Full CI/CD
sh '''
set -e
# Deps: curl, jq
# Env you can set at the job or pipeline level:
# BASE (default: https://vines.rosebird.org)
# TARGET (e.g., https://example.com)
# VINES_TOKEN (store as Jenkins Secret Text and inject to env)
# MAX_CVSS (default: 8.9)
# MAX_CRITICAL (default: 0)
: "${BASE:=https://vines.rosebird.org}"
: "${TARGET:?Set TARGET (e.g., https://example.com)}"
: "${VINES_TOKEN:?Set VINES_TOKEN in Jenkins credentials}"
MAX_CVSS="${MAX_CVSS:-8.9}"
MAX_CRITICAL="${MAX_CRITICAL:-0}"
echo "== Start scan → $TARGET"
RESP=$(curl -sS -X POST "$BASE/api/v1/scans" \
-H "Authorization: Bearer $VINES_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"target\":\"$TARGET\",\"scan_type\":\"both\"}")
echo "$RESP" | jq .
SCAN_ID=$(echo "$RESP" | jq -r .scan_id)
test -n "$SCAN_ID" || { echo "Failed to obtain scan_id"; exit 1; }
echo "scan_id=$SCAN_ID"
echo "== Poll status"
while true; do
SRESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/status" \
-H "Authorization: Bearer $VINES_TOKEN")
STATUS=$(echo "$SRESP" | jq -r .status)
PROG=$(echo "$SRESP" | jq -r '.progress // "n/a"')
echo "status=$STATUS progress=$PROG"
case "$STATUS" in
completed|failed|aborted) break ;;
*) sleep 3 ;;
esac
done
[ "$STATUS" = "completed" ] || { echo "Scan did not complete successfully: $STATUS"; exit 1; }
echo "== KPI (informational)"
KPI=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/kpi" \
-H "Authorization: Bearer $VINES_TOKEN")
echo "$KPI" | jq .
echo "== Gate (policy)"
GATE=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=${MAX_CRITICAL}&max_cvss=${MAX_CVSS}" \
-H "Authorization: Bearer $VINES_TOKEN")
echo "$GATE" | jq .
test "$(echo "$GATE" | jq -r .ok)" = "true" || { echo "Gate failed"; exit 1; }
echo "== Download PDF"
curl -sSL "$BASE/api/v1/report/$SCAN_ID/pdf" \
-H "Authorization: Bearer $VINES_TOKEN" -o report.pdf
test -s report.pdf && echo "Saved report.pdf"
'''
Azure Pipelines
- task: Bash@3
env: { BASE: https://vines.rosebird.org, VINES_TOKEN: $(VINES_TOKEN), SCAN_ID: $(SCAN_ID) }
inputs:
targetType: 'inline'
script: |
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
echo "$RESP" | jq .
if [ "$(echo "$RESP" | jq -r '.ok')" != "true" ]; then
echo "##vso[task.logissue type=error]$(echo "$RESP" | jq -r '.reasons | join(\"; \")')"
exit 1
fi
Jenkins Full CI/CD
sh '''
# ... your Jenkins shell script ...
'''
CircleCI
version: 2.1
jobs:
vines_gate:
docker: [{ image: cimg/base:stable }]
steps:
- checkout
- run: sudo apt-get update && sudo apt-get install -y jq
- run: |
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
echo "$RESP" | jq .
test "$(echo "$RESP" | jq -r '.ok')" = "true"
workflows:
gate:
jobs: [vines_gate]
Google Cloud Build
steps:
- name: gcr.io/cloud-builders/curl
entrypoint: 'bash'
args:
- -c
- |
apk add --no-cache jq || true
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
echo "$RESP" | jq .
test "$(echo "$RESP" | jq -r '.ok')" = "true"
Bitbucket Pipelines
pipelines:
default:
- step:
image: alpine:latest
script:
- apk add --no-cache curl jq
- RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN_ID/gate?max_critical=0&max_cvss=8.9" -H "Authorization: Bearer $VINES_TOKEN")
- echo "$RESP" | jq .
- test "$(echo "$RESP" | jq -r '.ok')" = "true"
SDK Snippets
Python
import os
import requests
BASE = "https://vines.rosebird.org"
TOKEN = os.environ["VINES_TOKEN"]
H = {"Authorization": f"Bearer {TOKEN}"}
def start_scan(target, scan_type="both"):
r = requests.post(f"{BASE}/api/v1/scans",
json={"target": target, "scan_type": scan_type},
headers=H)
r.raise_for_status()
return r.json()
def status(scan_id):
r = requests.get(f"{BASE}/api/v1/scan/{scan_id}/status", headers=H)
r.raise_for_status()
return r.json()
def report_json(scan_id):
r = requests.get(f"{BASE}/api/v1/report/{scan_id}/json", headers=H)
r.raise_for_status()
return r.json()
def report_pdf(scan_id, path="report.pdf"):
r = requests.get(f"{BASE}/api/v1/report/{scan_id}/pdf", headers=H)
r.raise_for_status()
with open(path, "wb") as f:
f.write(r.content)
def gate(scan_id, **policy):
q = "&".join(f"{k}={v}" for k, v in policy.items())
r = requests.get(f"{BASE}/api/v1/scan/{scan_id}/gate?{q}", headers=H)
r.raise_for_status()
return r.json()
Node (ESM)
import { writeFileSync } from "node:fs";
const BASE = "https://vines.rosebird.org";
const TOKEN = process.env.VINES_TOKEN;
const H = {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
};
export async function startScan(target, scan_type = "both") {
const res = await fetch(`${BASE}/api/v1/scans`, {
method: "POST",
headers: H,
body: JSON.stringify({ target, scan_type }),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function status(id) {
const res = await fetch(`${BASE}/api/v1/scan/${id}/status`, {
headers: { Authorization: H.Authorization },
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function reportJson(id) {
const res = await fetch(`${BASE}/api/v1/report/${id}/json`, {
headers: { Authorization: H.Authorization },
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function reportPdf(id, path = "report.pdf") {
const res = await fetch(`${BASE}/api/v1/report/${id}/pdf`, {
headers: { Authorization: H.Authorization },
});
if (!res.ok) throw new Error(await res.text());
const buf = Buffer.from(await res.arrayBuffer());
writeFileSync(path, buf);
}
export async function gate(id, policy = {}) {
const q = new URLSearchParams(policy).toString();
const res = await fetch(`${BASE}/api/v1/scan/${id}/gate?${q}`, {
headers: { Authorization: H.Authorization },
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
OpenAPI Mini-Spec (importable)
Show YAML
openapi: 3.0.0
info: { title: Vulnerability Vines AI API, version: "1.0" }
servers: [ { url: https://vines.rosebird.org } ]
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
security: [ { bearerAuth: [] } ]
paths:
/api/v1/scans:
post:
summary: Start a scan
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
target: { type: string }
scan_type: { type: string, enum: [nmap, zap, both] }
required: [target]
responses:
"200": { description: OK }
/api/v1/scan/{scan_id}/status:
get:
summary: Scan status
parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
responses: { "200": { description: OK } }
/api/v1/scan/{scan_id}/abort:
post:
summary: Abort scan
parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
responses: { "200": { description: OK } }
/api/v1/report/{scan_id}/json:
get:
summary: Report JSON
parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
responses: { "200": { description: OK } }
/api/v1/report/{scan_id}/pdf:
get:
summary: Report PDF
parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
responses: { "200": { description: PDF stream } }
/api/v1/kpi/latest:
get:
summary: Latest KPIs
parameters: [ { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 1 } } ]
responses: { "200": { description: OK } }
/api/v1/scan/{scan_id}/kpi:
get:
summary: Single-scan KPI
parameters: [ { name: scan_id, in: path, required: true, schema: { type: string } } ]
responses: { "200": { description: OK } }
/api/v1/scan/{scan_id}/gate:
get:
summary: Gate (pass/fail)
parameters:
- { name: scan_id, in: path, required: true, schema: { type: string } }
- { name: max_cvss, in: query, schema: { type: number } }
- { name: max_critical, in: query, schema: { type: integer } }
- { name: max_high, in: query, schema: { type: integer } }
- { name: max_medium, in: query, schema: { type: integer } }
- { name: max_low, in: query, schema: { type: integer } }
- { name: max_info, in: query, schema: { type: integer } }
- { name: max_total, in: query, schema: { type: integer } }
- { name: max_open_ports, in: query, schema: { type: integer } }
responses: { "200": { description: OK } }
Tip: copy the YAML into Postman/Insomnia to generate a workspace quickly.
Rate Limits & Security
- Capacity: If you see
429 {"error":"capacity"}, back off and retry later. - Plan limits:
plan_limit_active(concurrency) andplan_limit_scans(retention) mirror the web app. - Token rotation: rotate tokens periodically; revoke on compromise; never commit tokens.
- Retries: use exponential backoff (e.g., 1s, 2s, 4s…) on 429/5xx; avoid hot loops.
- Versioning: all routes are under
/api/v1. Breaking changes will appear under a new version.
Errors
| HTTP | Body | Meaning |
|---|---|---|
| 401 | {"error":"missing_token"|"invalid_token"} | Token header missing/invalid |
| 403 | {"error":"subscription_required"} | Subscription inactive/expired |
| 403 | {"error":"plan_limit_active"|"plan_limit_scans"} | Plan constraints |
| 404 | {"error":"not_found"|"not_owner"} | Not found / no ownership |
| 429 | {"error":"capacity"} | Global capacity full |
Policy Gates & CI Fail Rules
The /api/v1/scan/{scan_id}/gate endpoint enforces severity and CVSS thresholds.
It returns {"ok":true|false,"reasons":[...]} based on query parameters. CI pipelines
should call this endpoint to fail builds if limits are exceeded.
Supported parameters
max_cvss(float 0.0–10.0)max_critical,max_high,max_medium,max_low,max_info(integers)max_total(all severities combined)max_open_ports(network exposures)
Interpretation: max_high=5 allows up to 5 High; fails on 6+. max_critical=0 disallows any Critical.
Common policies
- Fail if any Critical:
?max_critical=0 - Fail if High > 5:
?max_high=5 - Strict Prod:
?max_critical=0&max_high=0&max_cvss=8.9 - Relaxed Staging:
?max_critical=0&max_high=2&max_cvss=9.4 - Fail if more than 20 issues:
?max_total=20 - Zero open ports:
?max_open_ports=0
Curl example
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
SCAN="$SCAN_ID"
# Example: fail if Critical>0, High>5, or CVSS>8.9
Q="max_critical=0&max_high=5&max_cvss=8.9"
RESP=$(curl -sS "$BASE/api/v1/scan/$SCAN/gate?$Q" -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq .
test "$(echo "$RESP" | jq -r .ok)" = "true"
Jenkins KPI Checks
sh '''
set -e
: "${BASE:=https://vines.rosebird.org}"
: "${VINES_TOKEN:?Missing token}"
: "${MAX_CRITICAL:=0}"
: "${MAX_HIGH:=5}"
: "${MAX_CVSS:=8.9}"
SID=$(cat .scan.id)
Q="max_critical=${MAX_CRITICAL}&max_high=${MAX_HIGH}&max_cvss=${MAX_CVSS}"
echo "== Gate with $Q"
RESP=$(curl -sS "$BASE/api/v1/scan/$SID/gate?$Q" -H "Authorization: Bearer $VINES_TOKEN")
echo "$RESP" | jq .
if [ "$(echo "$RESP" | jq -r .ok)" != "true" ]; then
echo "Gate failed: $(echo "$RESP" | jq -r '.reasons | join("; ")')"
exit 1
fi
'''
GitHub Actions step
- name: Gate policy
env:
BASE: https://vines.rosebird.org
VINES_TOKEN: ${{ secrets.VINES_TOKEN }}
MAX_CRITICAL: 0
MAX_HIGH: 5
MAX_CVSS: 8.9
run: |
SID=$(cat .scan.id)
Q="max_critical=${MAX_CRITICAL}&max_high=${MAX_HIGH}&max_cvss=${MAX_CVSS}"
R=$(curl -sS "$BASE/api/v1/scan/$SID/gate?$Q" -H "Authorization: Bearer $VINES_TOKEN")
echo "$R" | jq .
test "$(echo "$R" | jq -r .ok)" = "true"
GitLab CI fragment
SID=$(cat .scan.id)
Q="max_critical=0&max_high=5&max_cvss=8.9"
R=$(curl -sS "$BASE/api/v1/scan/$SID/gate?$Q" -H "Authorization: Bearer $VINES_TOKEN")
echo "$R" | jq .
test "$(echo "$R" | jq -r .ok)" = "true"
Tips
- Keep prod strict (no Critical/High, cap CVSS 8.9); allow relaxed gates in staging.
- Always echo
reasonsfor developer feedback. - Set thresholds via environment vars so each branch/CI job can differ.
- Use
Idempotency-Keyon POST /scans in CI retries to avoid duplicate scans.
Best Practices
- Keep tokens in CI/CD secrets; never echo them in logs.
- Use
/api/v1/kpi/latest?limit=1to discover the latestscan_idautomatically. - Gate prod releases with strict policies (
max_critical=0&max_high=0&max_cvss=8.9). - Add a nightly job that exports
/kpi/latest?limit=10to your SIEM/Data Lake. - Handle
429with exponential backoff; show a friendly message in developer tooling.
Pagination & Filtering
Endpoints that return lists (e.g., /api/v1/reports, /api/v1/kpi/latest) support cursor-based pagination.
Query params
| Param | Type | Default | Description |
|---|---|---|---|
limit | integer (1–100) | 25 | Max items to return. |
cursor | string | — | Opaque token from the previous response’s next_cursor. |
target | string | — | Filter reports whose target contains this substring. |
status | enum | — | initializing|running|completed|aborted|failed. |
since | ISO-8601 | — | Only items with scan_date ≥ this timestamp (UTC). |
until | ISO-8601 | — | Only items with scan_date ≤ this timestamp (UTC). |
sort | string | scan_date | Field to sort by. |
order | enum | desc | asc|desc. |
Example
curl -sS "$BASE/api/v1/reports?limit=10&status=completed&since=2025-08-01T00:00:00Z" \
-H "Authorization: Bearer $VINES_TOKEN"
Response shape
{
"items": [ /* ... */ ],
"next_cursor": "eyJvZmZzZXQiOjEwfQ" // present only if more data is available
}
Rate Limits
All responses include standard rate-limit headers. Plans may vary.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Max requests permitted in the current window. |
X-RateLimit-Remaining | Remaining requests in the current window. |
X-RateLimit-Reset | Unix epoch seconds when the window resets. |
Retry-After | Seconds to wait before retrying (only on 429). |
Exponential backoff (bash)
for d in 1 2 4 8 16; do
code=$(curl -s -w "%{http_code}" -o .out "$URL" -H "Authorization: Bearer $VINES_TOKEN" | tail -c 3)
if [ "$code" -lt 500 ] && [ "$code" != "429" ]; then break; fi
sleep "$d"
done
cat .out
Errors — Canonical Schema
We always return JSON on errors with a stable shape:
{
"error": "plan_limit_active",
"message": "Concurrent scan limit reached",
"request_id": "req_9zFQ",
"hint": "Wait for active scans to finish or upgrade plan."
}
Common error codes
| error | HTTP | Meaning |
|---|---|---|
missing_token / invalid_token | 401 | Bearer token missing/invalid/expired. |
subscription_required | 403 | Subscription inactive/expired. |
plan_limit_active | 403 | Concurrent scan limit reached. |
plan_limit_scans | 403 | Retention exceeded for your plan. |
not_found | 404 | Resource not found. |
not_owner | 404 | Resource exists but not owned by token. |
capacity | 429 | Global capacity saturated; back off and retry. |
Idempotency
Recommended for POST /api/v1/scans. Send an Idempotency-Key header (e.g., a UUID). Retries with the same key will not create duplicate scans.
curl -sS -X POST "$BASE/api/v1/scans" \
-H "Authorization: Bearer $VINES_TOKEN" \
-H "Idempotency-Key: 9106bad7-fda6-464e-980f-3d7154409a69" \
-H "Content-Type: application/json" \
-d '{"target":"https://example.com","scan_type":"both"}'
Stable Headers You Can Rely On
| Header | Endpoints | Notes |
|---|---|---|
X-Request-Id | All | Include this in support tickets. |
ETag | /api/v1/report/{id}/json | Use for cache validation (If-None-Match). |
Content-Disposition | /api/v1/report/{id}/pdf | inline; filename="Vulnerability_Vines_YYYY-MM-DD.pdf" |
Field Conventions
- Timestamps: ISO-8601 UTC (e.g.,
2025-08-26T05:46:40Z). - IDs: UUID v4 strings unless specified.
- Enums: documented per endpoint (e.g.,
status,scan_type). - Scores: CVSS is a number 0.0–10.0 (double).
Versioning, Deprecations & Changelog
Policy
- All current routes live under
/api/v1. - Breaking changes will ship under a new base path (
/api/v2). - Deprecated fields/routes return
Deprecation: trueandSunset:headers with at least 90 days’ notice.
Changelog
Recent changes
2025-08-26
- Added: KPIs endpoints and /kpi/latest
- Added: Gate endpoints for CI/CD policies
- Improved: Report JSON includes risk_scores.max and overall_score
- Docs: Pagination, idempotency, rate-limit headers, canonical error schema
Data Retention & Export
- Retention: Reports and KPIs are retained per plan limits; expired artifacts may be pruned.
- Guarantee:
/api/v1/report/{id}/jsonremains fetchable while within your plan’s retention window. - Bulk export: Use
/reports?since=...&until=...and/kpi/latest?limit=Non a nightly job to your SIEM/Data Lake.
Export example
curl -sS "$BASE/api/v1/reports?limit=100&since=2025-08-01T00:00:00Z" \
-H "Authorization: Bearer $VINES_TOKEN" | jq -r '.items[] | @json' >> reports.ndjson
Security & Acceptable Use
- Only scan assets you own or are explicitly authorized to test.
- Tokens are personal; rotate immediately if compromised.
- Respect robots/target rules where applicable; we may throttle/deny abusive patterns.
- Contact us for prg/security questions at
[email protected].
Support Checklist
- Include X-Request-Id, endpoint, and full HTTP status.
- Attach request timestamp (UTC) and the minimal repro curl.
- For auth issues, confirm token freshness and scope (if applicable).
- For rate limits, share the limit headers and your backoff config.
Common Recipes
Start → Poll → Gate → Fetch PDF
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
# 1) Start
SCAN=$(curl -sS -X POST "$BASE/api/v1/scans" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"target":"https://example.com","scan_type":"both"}' | jq -r '.scan_id')
# 2) Poll
while true; do
s=$(curl -sS "$BASE/api/v1/scan/$SCAN/status" -H "Authorization: Bearer $TOKEN" | jq -r '.status')
echo "status: $s"; [ "$s" = "completed" -o "$s" = "failed" -o "$s" = "aborted" ] && break
sleep 3
done
# 3) Gate
curl -sS "$BASE/api/v1/scan/$SCAN/gate?max_critical=0&max_cvss=8.9" \
-H "Authorization: Bearer $TOKEN" | tee gate.json | jq .
# 4) PDF (if ok)
jq -e '.ok == true' gate.json >/dev/null && \
curl -sSL "$BASE/api/v1/report/$SCAN/pdf" -H "Authorization: Bearer $TOKEN" -o report.pdf
Latest completed scan for a target (bash)
SCAN=$(curl -sS "$BASE/api/v1/reports?limit=1&target=niles.rosebird.org&status=completed&order=desc" \
-H "Authorization: Bearer $TOKEN" | jq -r '.items[0].scan_id')
Node: fail CI if CVSS > 8.9
const url = `${BASE}/api/v1/scan/${SCAN_ID}/gate?max_critical=0&max_cvss=8.9`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` }});
const reqId = res.headers.get('x-request-id');
const body = await res.json();
if (!body.ok) { throw new Error(`Gate failed (${reqId}): ${body.reasons?.join('; ')}`); }
OpenAPI Addendum
YAML updates (pagination, errors, idempotency)
paths:
/api/v1/reports:
get:
summary: List reports
parameters:
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
- { name: cursor, in: query, schema: { type: string } }
- { name: target, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string, enum: [initializing, running, completed, aborted, failed] } }
- { name: since, in: query, schema: { type: string, format: date-time } }
- { name: until, in: query, schema: { type: string, format: date-time } }
- { name: sort, in: query, schema: { type: string, default: scan_date } }
- { name: order, in: query, schema: { type: string, enum: [asc, desc], default: desc } }
responses:
"200":
description: OK
headers:
X-RateLimit-Limit: { schema: { type: integer } }
X-RateLimit-Remaining: { schema: { type: integer } }
X-RateLimit-Reset: { schema: { type: integer } }
content:
application/json:
schema:
type: object
properties:
items: { type: array, items: { type: object } }
next_cursor: { type: string }
/api/v1/scans:
post:
summary: Start a scan
parameters:
- name: Idempotency-Key
in: header
required: false
schema: { type: string, description: "Client-provided unique key to dedupe retries" }
responses:
"200": { description: OK }
components:
schemas:
Error:
type: object
properties:
error: { type: string }
message: { type: string }
request_id: { type: string }
hint: { type: string }
Scanning Private/Internal Apps
If your application is inside a private network (NAT/VPN/firewall), the Vines AI scanners must be able to reach it over the Internet to perform HTTP/HTTPS DAST (and any host/port checks you allow). Below are safe, reversible ways to provide reachability for a limited time.
Key options
- Temporary tunnel (recommended for staging): Use
ngrokto expose only the app’s HTTP(S) port with a public URL. - Cloudflare DNS-only (no proxy): Point a subdomain to your origin with DNS only and allowlist Vines AI IPs.
- Cloudflare tunnel + bypass rules: If you must keep Cloudflare, create “skip” rules for Vines AI IPs and disable Bot/WAF just for the scanning path or subdomain.
Tip: For the most thorough results, use a staging environment that mirrors prod, and run scans during a known window (e.g., off-peak).
Option A — Expose a Temporary URL with ngrok
1) Install & authenticate
# macOS (brew) / Linux (apt) / Windows (choco) — pick one
brew install ngrok
# or: curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc
# && echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
# && sudo apt update && sudo apt install ngrok
# Add your ngrok token (from dashboard)
ngrok config add-authtoken <NGROK_AUTH_TOKEN>
2) Start a tunnel
Expose your local app to the Internet. Use a reserved domain for a stable URL (recommended), otherwise an ephemeral URL is created each run.
HTTP app on port 8080
# Ephemeral URL
ngrok http 8080
# Reserved domain (requires ngrok reserved domain)
ngrok http --domain=api-yourapp.ngrok.app 8080
HTTPS app on 8443 (backend TLS)
ngrok http https://localhost:8443
# With reserved domain
ngrok http --domain=secure-yourapp.ngrok.app https://localhost:8443
Forward the original Host header (helps apps that key off host)
ngrok http --host-header=rewrite 8080
Disable the ngrok web inspector (slightly harder to enumerate)
ngrok http 8080 --inspect=false
3) Config file (repeatable)
Create ~/.config/ngrok/ngrok.yml (or ngrok.yml) and run ngrok start --all.
# ~/.config/ngrok/ngrok.yml
authtoken: <NGROK_AUTH_TOKEN>
version: 2
tunnels:
app:
addr: 8080
proto: http
host_header: rewrite
inspect: false
# Optional reserved domain for stability:
# domain: api-yourapp.ngrok.app
app-https:
addr: https://localhost:8443
proto: http
inspect: false
4) Feed the URL to Vines AI
BASE="https://vines.rosebird.org"
TOKEN="$VINES_TOKEN"
TARGET="https://api-yourapp.ngrok.app" # from ngrok
curl -sS -X POST "$BASE/api/v1/scans" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"target\":\"$TARGET\",\"scan_type\":\"both\"}" | jq .
Heads‑up: Port scans will reflect the public edge (ngrok endpoint), not your internal LAN. Web vulnerabilities (ZAP) are still valid against your app.
Programmatic helpers
Python (start, read URL, scan)
import subprocess, json, time, requests, os
BASE = "https://vines.rosebird.org"
TOKEN = os.environ["VINES_TOKEN"]
H = {"Authorization": f"Bearer {TOKEN}"}
# start ngrok in background
proc = subprocess.Popen(["ngrok","http","--inspect=false","8080"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
time.sleep(2) # give ngrok a moment
# fetch public URL from local API
j = requests.get("http://127.0.0.1:4040/api/tunnels").json()
TARGET = [t["public_url"] for t in j["tunnels"] if t["proto"].startswith("http")][0]
# start scan
r = requests.post(f"{BASE}/api/v1/scans", json={"target": TARGET, "scan_type": "both"}, headers=H)
print(r.json())
Node (start, read URL, scan)
import { spawn } from "node:child_process";
const BASE = "https://vines.rosebird.org";
const TOKEN = process.env.VINES_TOKEN;
const ng = spawn("ngrok", ["http","--inspect=false","8080"], { stdio: "ignore", detached: true });
const url = "http://127.0.0.1:4040/api/tunnels";
const tunnels = await (await fetch(url)).json();
const TARGET = tunnels.tunnels.find(t => t.proto.startsWith("http")).public_url;
const res = await fetch(`${BASE}/api/v1/scans`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ target: TARGET, scan_type: "both" }),
});
console.log(await res.json());
Firewall & WAF Allowlisting
To reduce false negatives and avoid throttling, allowlist the scanner egress IPs and relax WAF rules just for the scan window/subdomain.
Checklist
- Allow inbound 80/443 to the exposed endpoint (ngrok handles this if used).
- Allowlist Vines AI scanner IP(s) on your firewall/WAF. (Contact support for the current list.)
- Temporarily relax/bypass rate‑limit, bot management, and aggressive WAF signatures for the scan host.
- Disable geo/IP reputation blocking for the allowlisted scanner IPs.
- Ensure outbound egress can reach
vines.rosebird.orgfor APIs if you trigger scans from inside.
Authentication & Session Strategy
Authentication gates can hide routes from the scanner. Use one of these patterns:
- IP‑based bypass (safest scope): For the scan subdomain, if client IP ∈ {Vines AI} then skip auth middleware and CSRF checks; leave auth on for everyone else.
- Dedicated test user: Create a non‑privileged account without MFA/SSO prompts and share a long‑lived token via the
Authorizationheader for the scan window. - Staging mirror without auth: Duplicate prod config but disable auth only in staging and only during the scan window.
Example: NGINX allowlist to skip auth (pseudocode)
map $remote_addr $skip_auth {
default 0;
# Vines AI scanner egress IPs (sample placeholders)
203.0.113.10 1;
203.0.113.11 1;
}
server {
server_name scan.staging.example.com;
location / {
if ($skip_auth) { set $auth_bypass 1; }
# your app reads X-Bypass-Auth to skip middleware
proxy_set_header X-Bypass-Auth $auth_bypass;
proxy_pass http://app:8080;
}
}
Example: Express middleware
const SCANNER_IPS = new Set(["203.0.113.10","203.0.113.11"]);
app.use((req,res,next) => {
if (SCANNER_IPS.has(req.ip) || req.get("X-Bypass-Auth") === "1") return next(); // bypass
return requireAuth(req,res,next);
});
Avoid disabling protections globally. Limit by IP + subdomain + timeframe and revert after the scan.
Cloudflare — Make Origin Reachable
Option 1 — DNS Only (no proxy)
In DNS, set your scan subdomain’s record to DNS Only (gray cloud). This exposes the origin IP directly. Combine with firewall allowlisting of Vines AI IPs.
- Turn off “Proxied” (orange cloud) → becomes gray cloud “DNS only”.
- Ensure port 80/443 open to the Internet for the origin (or to your forward proxy/ELB).
- Revert to orange cloud after the scan.
Option 2 — Keep proxy but bypass security for Vines AI
Create a WAF Custom Rule: if ip.src in {Vines IPs} then Skip WAF, Bot Management, Rate Limiting, and Managed Challenge for the scan subdomain or path.
Option 3 — Cloudflare Tunnel
If you use cloudflared tunnels, keep the tunnel but add custom rules to Skip WAF/Bot/RL for scanner IPs or create a separate scan‑only subdomain routed to the same origin.
Disable Bot Fight & UAM (scoped)
- Turn off “Under Attack Mode” for the scan subdomain.
- Disable “Super Bot Fight Mode” (or set to “Allow Verified Bots + Skip for scanners”).
- Set Security Level “Essentially Off” for the scan subdomain rule only.
Cloudflare caching can mask dynamic responses. Add a Page Rule or Cache Rule to Bypass cache for the scan host.
Throttling, Windows & Safety
- Choose a window: Run scans in an agreed maintenance window to avoid alert fatigue.
- Rate limits: Temporarily increase or disable rate limits for the scan host/IPs.
- DoS protections: Ensure WAF anomaly scoring doesn’t auto‑block during crawling.
- Robots: Allow scanning crawler in
/robots.txtfor the scan host; or temporarily serveDisallow:only for production, not staging. - Data safety: Use a staging DB without real PII/data mutations. If scanning prod, restrict dangerous verbs (DELETE/PUT) behind feature flags.
- TLS: Support modern ciphers; present complete chain; avoid certificate pinning during scans.
Quick Allowlist Snippets
NGINX (allowlist scanner IPs)
# Only allow Vines AI for this server; others get 403
allow 203.0.113.10; # placeholder
allow 203.0.113.11; # placeholder
deny all;
Apache
Require ip 203.0.113.10
Require ip 203.0.113.11
# Otherwise:
Require all denied
Replace placeholder IPs with the official Vines AI egress IP list you receive from support.
Optional — Authenticated Routes
If you want to scan authenticated areas:
- Create a test user with broad read‑only access (no 2FA/MFA/SSO prompts).
- Expose a login endpoint that accepts basic creds or a static token during the scan window.
- Whitelist the scanner IPs from brute‑force/lockout policies.
Header example
curl -sS "$TARGET/protected" \
-H "Authorization: Bearer <TEST_STATIC_TOKEN>"
DevSecOps Pre‑Scan Checklist
- ✅ Reachability confirmed (ngrok URL or DNS-only host resolves publicly).
- ✅ Firewall/WAF allowlisting applied to scanner IPs; rate‑limits relaxed for scan host.
- ✅ Auth plan chosen (IP bypass, test user, or staging without auth).
- ✅ Cache disabled and CDN/WAF “skip” rules in place for scan host.
- ✅ Database/test data prepared; destructive ops gated.
- ✅ Maintenance window agreed; monitoring teams notified.
- ✅ Post‑scan revert plan documented (undo bypasses, re‑enable protections).
Quick Tools
Curl env bootstrap
export BASE="https://vines.rosebird.org"
export VINES_TOKEN="<your_token>"
alias vines-get='curl -sS -H "Authorization: Bearer $VINES_TOKEN"'
vines-get "$BASE/api/v1/reports?limit=5" | jq .
Tiny Makefile (start→poll→pdf)
BASE ?= https://vines.rosebird.org
TOKEN ?= $(VINES_TOKEN)
TARGET ?= https://example.com
SCAN_FILE := .scan.id
start:
\t@curl -sS -X POST "$(BASE)/api/v1/scans" \\
\t -H "Authorization: Bearer $(TOKEN)" -H "Content-Type: application/json" \\
\t -d '{"target":"$(TARGET)","scan_type":"both"}' | jq -r .scan_id > $(SCAN_FILE) && \\
\techo "scan=$$(cat $(SCAN_FILE))"
poll:
\t@sid=$$(cat $(SCAN_FILE)); \\
\twhile true; do \\
\t st=$$(curl -sS "$(BASE)/api/v1/scan/$$sid/status" -H "Authorization: Bearer $(TOKEN)" | jq -r .status); \\
\t echo status: $$st; \\
\t [ "$$st" = "completed" -o "$$st" = "failed" -o "$$st" = "aborted" ] && break; \\
\t sleep 3; \\
\tdone
pdf:
\t@sid=$$(cat $(SCAN_FILE)); \\
\tcurl -sSL "$(BASE)/api/v1/report/$$sid/pdf" -H "Authorization: Bearer $(TOKEN)" -o report.pdf && \\
\techo "saved report.pdf"
Troubleshooting (2-minute fixes)
| Symptom | Likely cause | Fix |
|---|---|---|
401 invalid_token | Wrong/expired token | Rotate in Profile → API Tokens; re-export VINES_TOKEN. |
404 not_owner | Using someone else’s scan_id | List yours via /api/v1/reports and retry. |
429 capacity | Busy scanners / plan limit | Backoff (1,2,4,8s) or try later; reduce concurrency. |
| Polling never completes | Target unreachable behind firewall | Use ngrok/cloud DNS-only; allowlist scanner IPs; see “Internal Apps”. |
| All JSON empty | Blocked by WAF/bot rules | Add bypass for scan subdomain + scanner IPs; disable CF proxy for the scan. |
| PDF downloads but blank | Cache/CDN serving error page | Bypass cache for scan host; retry directly to origin. |
Minimal Policies (what small teams actually use)
- Prod gate:
?max_critical=0&max_high=0&max_cvss=8.9 - Staging gate:
?max_critical=0&max_high=2&max_cvss=9.4 - Quick smoke:
?max_cvss=7.0(let everything else pass)
Keep it simple: one strict prod gate, one relaxed staging gate.
Lightweight Support
- Email:
[email protected](includeX-Request-Idand your curl). - Status/maintenance window: status page
- Version pinning: lock to
/api/v1; breaking changes will use/api/v2.
Quick Tools & Scripts (Small-Team Friendly)
Postman
Import our mini collection and environment, hit “Send.”
- Postman Collection (JSON)
- Postman Environment (JSON)
- Insomnia Workspace (YAML)
- Jenkinsfile (Declarative Pipeline)
- Vines.sh (Linux Bash helper + CI generators)
- GitHub Actions Workflow (YAML)
- GitLab CI Pipeline (YAML)
- Makefile (Local CLI helper)
- Dockerfile (Containerized scanner wrapper)
- Vines CLI (Linux Bash helper)
Hoppscotch (Web)
Use the online client; import OpenAPI or run raw requests.
{
"openapi": "3.0.0",
"info": { "title": "Vines API", "version": "1.0" },
"servers": [{ "url": "https://vines.rosebird.org" }],
"paths": { "/api/v1/reports": { "get": { "summary": "List reports" } } }
}
In Hoppscotch: Collections → Import → OpenAPI JSON (above).
Bruno
Simple, local-first API client. Import a tiny collection.
{
"version": "1",
"type": "collection",
"name": "Vines API",
"slug": "vines-api",
"requests": [
{
"name": "List Reports",
"method": "GET",
"url": "https://vines.rosebird.org/api/v1/reports?limit=5",
"headers": [{ "name": "Authorization", "value": "Bearer {{VINES_TOKEN}}" }]
}
],
"envs": [{ "name": "default", "variables": { "VINES_TOKEN": "" } }]
}
Thunder Client (VS Code)
Import a JSON collection into Thunder Client.
{
"client": "Thunder Client",
"collectionName": "Vines API",
"dateExported": "2025-08-26",
"requests": [
{
"name": "Gate Latest",
"method": "GET",
"url": "https://vines.rosebird.org/api/v1/kpi/latest?limit=1",
"headers": [{ "name": "Authorization", "value": "Bearer {{VINES_TOKEN}}" }]
}
],
"env": [{ "name": "default", "key": "VINES_TOKEN", "value": "" }]
}
RapidAPI Client (VS Code)
Import OpenAPI (same as Hoppscotch) → set VINES_TOKEN global var → run.
openapi: 3.0.0
info: { title: Vines API, version: "1.0" }
servers: [ { url: https://vines.rosebird.org } ]
paths:
/api/v1/scan/{scan_id}/status:
get:
parameters:
- { name: scan_id, in: path, required: true, schema: { type: string } }
REST Client (VS Code .http)
Save as vines.http, click “Send Request.”
### Set vars
@BASE = https://vines.rosebird.org
@TOKEN = {{VINES_TOKEN}}
### List reports
GET {{BASE}}/api/v1/reports?limit=5
Authorization: Bearer {{TOKEN}}
### Gate latest
GET {{BASE}}/api/v1/kpi/latest?limit=1
Authorization: Bearer {{TOKEN}}
HTTPie (CLI)
Human-friendly curl for quick calls.
http GET https://vines.rosebird.org/api/v1/reports \
Authorization:"Bearer $VINES_TOKEN"
http POST https://vines.rosebird.org/api/v1/scans \
Authorization:"Bearer $VINES_TOKEN" \
Content-Type:application/json \
target=https://example.com scan_type=both
Bash Helpers
Add these to ~/.bashrc/~/.zshrc for 1-liners.
export BASE="https://vines.rosebird.org"
export VINES_TOKEN="<token>"
vget(){ curl -sS "$1" -H "Authorization: Bearer $VINES_TOKEN"; }
vpost(){ curl -sS -X POST "$1" -H "Authorization: Bearer $VINES_TOKEN" -H "Content-Type: application/json" -d "$2"; }
# examples
vget "$BASE/api/v1/reports?limit=5" | jq .
vpost "$BASE/api/v1/scans" '{"target":"https://example.com","scan_type":"both"}' | jq .
PowerShell
Great for Windows users; uses Invoke-RestMethod.
$BASE = "https://vines.rosebird.org"
$TOKEN = $env:VINES_TOKEN
$H = @{ Authorization = "Bearer $TOKEN" }
# List reports
Invoke-RestMethod -Uri "$BASE/api/v1/reports?limit=5" -Headers $H -Method GET
# Start scan
$body = @{ target = "https://example.com"; scan_type = "both" } | ConvertTo-Json
Invoke-RestMethod -Uri "$BASE/api/v1/scans" -Headers ($H + @{ "Content-Type"="application/json" }) -Method POST -Body $body
Try It — Live
Supply your Base URL and token, pick an endpoint, then hit Run. For endpoints with {id}, fill scan_id.
Output will appear here…