Drive Django Clinic from your own code
Everything the web page does is available over HTTP: paste a Django project in, get the same
structured security audit back. The natural use is a CI job that re-audits whenever
settings.py changes, or a script that runs the same twelve-control check across every
service in a fleet and fails the build when one of them drifts into exposed.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses
the same envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
Send your app slug as X-App-Slug: django-clinic and your token as
Authorization: Bearer … on every call.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. Check the X-App-Slug header. |
not_found | 404 | Unknown job id, unknown collection, or the app slug does not exist. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
validation_error | 422 | The input object is missing a required field — files is the usual one — or a field is the wrong type. |
rate_limited | 429 | Too many requests. Back off and retry; do not tight-loop. |
internal | 5xx | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. A tiny client
One helper that adds the headers, unwraps data and raises on error.
# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="django-clinic"
TOKEN="YOUR_TOKEN" # from https://django-clinic.skillsafe.ai/tokens.html
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG"
fi
}
import json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "django-clinic"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://django-clinic.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "django-clinic";
const TOKEN = "YOUR_TOKEN"; // from https://django-clinic.skillsafe.ai/tokens.html
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "django-clinic"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://django-clinic.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class Clinic {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "django-clinic";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":...,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "django-clinic"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://django-clinic.skillsafe.ai/tokens.html
def call(path, body = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "django-clinic";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN, "X-App-Slug: " . SLUG];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class Clinic
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "django-clinic";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
req.Headers.Add("X-App-Slug", Slug);
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
2. Get a token
The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. You never need to open the developer console.
A guest token can call /me and /estimate. Running an
audit is metered, so it needs a personal token from signing in.
# A guest token is enough for /me and /estimate. Running an audit is metered and
# needs a personal token: open the token page and press "Sign in".
#
# https://django-clinic.skillsafe.ai/tokens.html
#
# That page also gives you a ready-made shell export:
# export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead:
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" -H "X-App-Slug: django-clinic"
# Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
# The token page exists so you never have to dig a token out of the browser
# yourself; it also prints the `export SKILLSAFE_TOKEN=...` line.
#
# A guest token, which can call /me and /estimate but cannot run:
guest = call("guest")
TOKEN = guest["token"]
// Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered audit.
const guest = await call("guest");
// Use guest.token as the bearer for subsequent calls.
// Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
// Or mint a guest token, which can call /me and /estimate but cannot run:
raw, err := call("guest", map[string]any{})
if err != nil {
panic(err)
}
var guest struct {
Token string `json:"token"`
}
_ = json.Unmarshal(raw, &guest)
// Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered audit.
String guest = call("guest", "{}");
System.out.println(guest);
# Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
# A guest token can call /me and /estimate but cannot run a metered audit.
guest = call("guest", {})
puts guest["token"]
<?php
// Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered audit.
$guest = call("guest", []);
echo $guest["token"];
// Open https://django-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered audit.
var guest = await Clinic.Call("guest", new { });
Console.WriteLine(guest.GetProperty("token").GetString());
3. Check the session and the balance
GET /me tells you whether the token is a guest or a person, and what the balance is.
Compare it against min_credits from the next step before you run, so a shortfall
surfaces as your own clear message rather than a 402.
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await Clinic.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
4. Price the run — free
The input object is exactly what the app's own form submits:
| field | type | meaning |
|---|---|---|
files | string, required | The pasted project files — settings.py, urls.py, views, models, forms, serializers, middleware, templates, requirements.txt. Put a # file: settings.py marker line above each one so they can be told apart. This is the audit's only evidence. A file whose middle has been removed should say so with a [... clipped ...] comment. |
posture | string | The deployment posture to audit against: production, staging, internal or unknown. It changes priority and framing, not what counts as a finding — DEBUG = True on internal is still a finding. unknown is audited against production, and the audit says so in assumptions. |
focus | string | general, settings, authentication, injection, data-exposure or deployment. Emphasis, not exclusivity: a high-severity finding from another area is never suppressed. |
context | string, optional | Free-form notes: who the users are, whether the app is internet-facing, what sits behind a VPN, what a previous audit found, the deadline. |
prescan_facts | object | {resources: [{id,label}], flags: [{id,label}]} — what a deterministic client-side scan established: settings keys, middleware entries, installed apps, views, models, serializers, routes and dependencies in resources; checks that fired in flags. Every flags id must come back in coverage_check, which is how you hold the model to the facts. |
retry_note | string, optional | Send only on a retry, when a previous reply failed to parse or came back truncated. The instruction is obeyed exactly. |
/estimate creates no job and charges nothing. It returns the model
binding — model, model_alias, markup_bps — and the
reservation: hold_credits is what gets held, min_credits is the balance
you must clear to start, and sponsor_enabled says whether the app is covering the run.
The actual charge is normally far lower than the hold, because the hold prices the full output cap.
INPUT='{"files": "# file: config/settings.py\nimport os\n\nDEBUG = True\nSECRET_KEY = \"django-insecure-REDACTED\"\nALLOWED_HOSTS = [\"*\"]\n\nMIDDLEWARE = [\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.security.SecurityMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n]\n\n# file: billing/views.py\ndef invoice_detail(request, pk):\n return render(request, \"invoice.html\", {\"invoice\": Invoice.objects.get(pk=pk)})\n\n# file: requirements.txt\nDjango==3.2.18", "posture": "production", "focus": "general", "context": "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.", "prescan_facts": {"resources": [{"id": "res:setting/DEBUG", "label": "Setting DEBUG = True"}, {"id": "res:dependency/Django", "label": "Dependency Django==3.2.18"}], "flags": [{"id": "settings:debug-true", "label": "DEBUG = True in config/settings.py"}, {"id": "settings:allowed-hosts-wildcard", "label": "ALLOWED_HOSTS = [\"*\"]"}]}}'
call estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":1890,"min_credits":280,"sponsor_enabled":false}}
#
# estimate is FREE. It creates no job and charges nothing. hold_credits is what
# gets RESERVED; the charge afterwards is normally much lower.
INPUT = {
"files": "# file: config/settings.py\nimport os\n\nDEBUG = True\nSECRET_KEY = \"django-insecure-REDACTED\"\nALLOWED_HOSTS = [\"*\"]\n\nMIDDLEWARE = [\n \"django.contrib.sessions.middleware.SessionMiddleware\",\n \"django.middleware.security.SecurityMiddleware\",\n \"django.middleware.csrf.CsrfViewMiddleware\",\n]\n\n# file: billing/views.py\ndef invoice_detail(request, pk):\n return render(request, \"invoice.html\", {\"invoice\": Invoice.objects.get(pk=pk)})\n\n# file: requirements.txt\nDjango==3.2.18",
"posture": "production",
"focus": "general",
"context": "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
"prescan_facts": {
"resources": [
{"id": "res:setting/DEBUG", "label": "Setting DEBUG = True"},
{"id": "res:dependency/Django", "label": "Dependency Django==3.2.18"}
],
"flags": [
{"id": "settings:debug-true", "label": "DEBUG = True in config/settings.py"},
{"id": "settings:allowed-hosts-wildcard", "label": "ALLOWED_HOSTS = [\"*\"]"}
]
}
}
est = call("estimate", INPUT)
print(est["model"], est["model_alias"], est["markup_bps"])
print(est["hold_credits"], est["min_credits"], est["sponsor_enabled"])
# estimate is free: no job is created and nothing is charged.
const INPUT = {
files:
'# file: config/settings.py\nimport os\n\nDEBUG = True\nSECRET_KEY = "django-insecure-REDACTED"\nALLOWED_HOSTS = ["*"]\n\n# file: requirements.txt\nDjango==3.2.18',
posture: "production",
focus: "general",
context: "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
prescan_facts: {
resources: [{ id: "res:setting/DEBUG", label: "Setting DEBUG = True" }],
flags: [
{ id: "settings:debug-true", label: "DEBUG = True in config/settings.py" },
{ id: "settings:allowed-hosts-wildcard", label: 'ALLOWED_HOSTS = ["*"]' },
],
},
};
const est = await call("estimate", INPUT);
console.log(est.model, est.model_alias, est.markup_bps);
console.log(est.hold_credits, est.min_credits, est.sponsor_enabled);
// estimate is free: no job is created and nothing is charged.
input := map[string]any{
"files": "# file: config/settings.py\nDEBUG = True\nALLOWED_HOSTS = [\"*\"]\n\n# file: requirements.txt\nDjango==3.2.18",
"posture": "production",
"focus": "general",
"context": "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
"prescan_facts": map[string]any{
"resources": []any{map[string]string{"id": "res:setting/DEBUG", "label": "Setting DEBUG = True"}},
"flags": []any{
map[string]string{"id": "settings:debug-true", "label": "DEBUG = True in config/settings.py"},
map[string]string{"id": "settings:allowed-hosts-wildcard", "label": "ALLOWED_HOSTS wildcard"},
},
},
}
raw, err := call("estimate", input)
if err != nil {
panic(err)
}
fmt.Println(string(raw)) // estimate is free - no job, no charge
String input = """
{
"files": "# file: config/settings.py\\nDEBUG = True\\nALLOWED_HOSTS = [\\"*\\"]\\n\\n# file: requirements.txt\\nDjango==3.2.18",
"posture": "production",
"focus": "general",
"context": "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
"prescan_facts": {
"resources": [
{ "id": "res:setting/DEBUG", "label": "Setting DEBUG = True" }
],
"flags": [
{ "id": "settings:debug-true", "label": "DEBUG = True in config/settings.py" },
{ "id": "settings:allowed-hosts-wildcard", "label": "ALLOWED_HOSTS wildcard" }
]
}
}
""";
System.out.println(call("estimate", input));
// estimate is free: no job is created and nothing is charged.
// The data object carries model, model_alias, markup_bps, hold_credits,
// min_credits and sponsor_enabled.
input = {
"files" => "# file: config/settings.py\nDEBUG = True\nALLOWED_HOSTS = [\"*\"]\n\n# file: requirements.txt\nDjango==3.2.18",
"posture" => "production",
"focus" => "general",
"context" => "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
"prescan_facts" => {
"resources" => [{ "id" => "res:setting/DEBUG", "label" => "Setting DEBUG = True" }],
"flags" => [
{ "id" => "settings:debug-true", "label" => "DEBUG = True in config/settings.py" },
{ "id" => "settings:allowed-hosts-wildcard", "label" => "ALLOWED_HOSTS wildcard" }
]
}
}
est = call("estimate", input)
puts "#{est['model']} hold=#{est['hold_credits']} min=#{est['min_credits']}"
# estimate is free: no job is created and nothing is charged.
<?php
$input = [
"files" => "# file: config/settings.py\nDEBUG = True\nALLOWED_HOSTS = [\"*\"]\n\n# file: requirements.txt\nDjango==3.2.18",
"posture" => "production",
"focus" => "general",
"context" => "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
"prescan_facts" => [
"resources" => [["id" => "res:setting/DEBUG", "label" => "Setting DEBUG = True"]],
"flags" => [
["id" => "settings:debug-true", "label" => "DEBUG = True in config/settings.py"],
["id" => "settings:allowed-hosts-wildcard", "label" => "ALLOWED_HOSTS wildcard"],
],
],
];
$est = call("estimate", $input);
echo $est["model"], " ", $est["hold_credits"], " ", $est["min_credits"], PHP_EOL;
// estimate is free: no job is created and nothing is charged.
var input = new
{
files = "# file: config/settings.py\nDEBUG = True\nALLOWED_HOSTS = [\"*\"]\n\n# file: requirements.txt\nDjango==3.2.18",
posture = "production",
focus = "general",
context = "Internet-facing billing app, 6 engineers, audit ahead of a SOC 2 review.",
prescan_facts = new
{
resources = new[] { new { id = "res:setting/DEBUG", label = "Setting DEBUG = True" } },
flags = new[]
{
new { id = "settings:debug-true", label = "DEBUG = True in config/settings.py" },
new { id = "settings:allowed-hosts-wildcard", label = "ALLOWED_HOSTS wildcard" }
}
}
};
var est = await Clinic.Call("estimate", input);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
Console.WriteLine(est.GetProperty("sponsor_enabled").GetBoolean());
// estimate is free: no job is created and nothing is charged.
5. Run it, then poll
POST /run returns a job_id; poll GET jobs/{job_id} until
status is succeeded or failed. The audit JSON is the string
at data.output.output.
Always send an Idempotency-Key. Derive it from the input, as the web
app does (django-clinic:<hash>:a<attempt>). A retried request carrying the
same key returns the same job instead of billing a second run — which is what makes a CI retry
safe. Replaying a key with a different body is a 409 conflict, so bump the
attempt suffix whenever the input actually changed.
# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
KEY="django-clinic:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until the job reaches a terminal status.
while :; do
OUT=$(call "jobs/$JOB")
STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
sleep 2
done
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time
# Always send an Idempotency-Key derived from the input: a retried request with
# the same key returns the SAME job instead of billing a second run.
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"django-clinic:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] == "succeeded":
break
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
time.sleep(2)
audit = json.loads(job["output"]["output"])
print(audit["posture"], audit["django_version"], len(audit["findings"]), "findings")
import { createHash } from "node:crypto";
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `django-clinic:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(INPUT),
}).then((r) => r.json());
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const audit = JSON.parse(job.output.output);
console.log(audit.posture, audit.settings_module, audit.findings.length, "findings");
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("django-clinic:%x:a1", sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
fmt.Println(job.Output.Output) // the audit JSON, as a string
break
}
if job.Status == "failed" {
panic("run failed")
}
time.Sleep(2 * time.Second)
}
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
var digest = java.security.MessageDigest.getInstance("SHA-256")
.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
var key = "django-clinic:" + java.util.HexFormat.of().formatHex(digest).substring(0, 16) + ":a1";
var start = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
String started = HTTP.send(start, HttpResponse.BodyHandlers.ofString()).body();
// Parse job_id out of `started`, then poll GET jobs/{job_id} every two seconds
// until status is "succeeded" or "failed"; the audit JSON is data.output.output.
System.out.println(started);
require "digest"
# Always send an Idempotency-Key derived from the input: a retried request with
# the same key returns the SAME job instead of billing a second run.
digest = Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]
key = "django-clinic:#{digest}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(input)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("jobs/#{job_id}")
break puts(job["output"]["output"]) if job["status"] == "succeeded"
raise "run failed" if job["status"] == "failed"
sleep 2
end
<?php
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
$digest = substr(hash("sha256", json_encode($input)), 0, 16);
$key = "django-clinic:{$digest}:a1";
$ch = curl_init(BASE . "/run");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"X-App-Slug: " . SLUG,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
while (true) {
$job = call("jobs/" . $jobId);
if ($job["status"] === "succeeded") { echo $job["output"]["output"]; break; }
if ($job["status"] === "failed") { throw new RuntimeException("run failed"); }
sleep(2);
}
using System.Security.Cryptography;
using System.Text;
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
var json = JsonSerializer.Serialize(input);
var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json)))[..16].ToLowerInvariant();
var key = $"django-clinic:{digest}:a1";
var run = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
run.Headers.Add("Authorization", "Bearer YOUR_TOKEN");
run.Headers.Add("X-App-Slug", "django-clinic");
run.Headers.Add("Idempotency-Key", key);
run.Content = JsonContent.Create(input);
// POST it, read data.job_id, then poll GET jobs/{job_id} every two seconds until
// status is "succeeded" or "failed"; the audit JSON is data.output.output.
6. Or stream it
POST /run-stream is the same call over server-sent events. The web app uses it to
advance a staged progress display as sections arrive, and to keep whatever parsed if the stream
dies mid-flight. The final done event carries charged_credits and the
truncated flag.
# Server-sent events. Each `delta` carries a chunk of the JSON audit; the final
# `done` event carries the status, charged_credits and the truncated flag.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"audit_name\":\"billing"}
# event: delta {"text":" — production security audit\","}
# event: done {"status":"succeeded","charged_credits":438,"truncated":false}
# Server-sent events: the audit arrives in chunks, so a UI can show progress.
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
req.add_header("Accept", "text/event-stream")
raw = ""
done = {}
event = None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
audit = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])
print(audit["posture"], len(audit["findings"]), "findings", "truncated" if done.get("truncated") else "")
// Server-sent events: the audit arrives in chunks, so a UI can show progress.
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
"Content-Type": "application/json",
"Idempotency-Key": key,
Accept: "text/event-stream",
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let raw = "";
let done = {};
let event = null;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") {
raw += JSON.parse(line.slice(6)).text ?? "";
} else if (line.startsWith("data: ") && event === "done") {
done = JSON.parse(line.slice(6));
}
}
}
const audit = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
console.log(audit.posture, audit.findings.length, "findings", done.charged_credits);
// Server-sent events: the audit arrives in chunks, so a UI can show progress.
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()
var raw strings.Builder
var event string
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct {
Text string `json:"text"`
}
_ = json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println(strings.TrimPrefix(line, "data: ")) // status, charged_credits, truncated
}
}
fmt.Println(raw.String())
// Server-sent events: the audit arrives in chunks, so a UI can show progress.
var stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
StringBuilder raw = new StringBuilder();
String[] event = { null };
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event: ")) event[0] = line.substring(7);
else if (line.startsWith("data: ") && "delta".equals(event[0])) {
raw.append(line.substring(6)); // each data line is {"text":"..."} - decode and append .text
}
});
System.out.println(raw);
# Server-sent events: the audit arrives in chunks, so a UI can show progress.
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req["Accept"] = "text/event-stream"
req.body = JSON.generate(input)
raw = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta"
raw << (JSON.parse(line[6..])["text"] || "")
end
end
end
end
end
audit = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
puts "#{audit['posture']} #{audit['findings'].length} findings"
<?php
// Server-sent events: the audit arrives in chunks, so a UI can show progress.
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"X-App-Slug: " . SLUG,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
"Accept: text/event-stream",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ") && $event === "delta") {
$raw .= json_decode(substr($line, 6), true)["text"] ?? "";
}
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$audit = json_decode(substr($raw, strpos($raw, "{")), true);
echo $audit["posture"], PHP_EOL;
// Server-sent events: the audit arrives in chunks, so a UI can show progress.
var stream = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
stream.Headers.Add("Authorization", "Bearer YOUR_TOKEN");
stream.Headers.Add("X-App-Slug", "django-clinic");
stream.Headers.Add("Idempotency-Key", key);
stream.Headers.Add("Accept", "text/event-stream");
stream.Content = JsonContent.Create(input);
using var res = await Http.SendAsync(stream, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null;
while (await reader.ReadLineAsync() is { } line)
{
if (line.StartsWith("event: ")) evt = line[7..];
else if (line.StartsWith("data: ") && evt == "delta")
{
var d = JsonSerializer.Deserialize<JsonElement>(line[6..]);
if (d.TryGetProperty("text", out var t)) raw.Append(t.GetString());
}
}
Console.WriteLine(raw.ToString());
7. Parse the audit and check the reconciliation
Two invariants are worth enforcing on your side, because the app enforces them too: every
focus_areas[].finding_ids entry must name a real finding id, and every
prescan_facts.flags id must appear exactly once in
coverage_check. A flag missing from the reconciliation means the model quietly skipped
a fact you established — treat that as a failed run, not a passing one, and retry with a
retry_note naming the missing ids.
The truncated flag on the done event (and on the finished job) means the
reply hit the output cap. What you hold is a prefix, not an audit: retry with a
retry_note asking for fewer, denser findings rather than trying to repair the JSON.
# The audit JSON is a string inside the envelope, so unwrap it twice.
AUDIT=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])')
printf '%s' "$AUDIT" | python3 -c '
import sys, json
a = json.load(sys.stdin)
print(a["posture"], "|", a["verdict"])
print(a["django_version"], a["deploy_target"], a["settings_module"])
for c in a["controls"]:
print(f" {c[\"status\"]:8} {c[\"control\"]}")
for f in a["findings"]:
print(f" {f[\"id\"]} {f[\"priority\"]:8} {f[\"category\"]:14} {f[\"resource\"]}")
'
# Every prescan flag id must come back exactly once in coverage_check.
printf '%s' "$AUDIT" | python3 -c '
import sys, json
seen = [c["id"] for c in json.load(sys.stdin)["coverage_check"]]
want = ["settings:debug-true", "settings:allowed-hosts-wildcard"]
missing = [i for i in want if seen.count(i) != 1]
if missing:
raise SystemExit("unreconciled prescan flags: " + ", ".join(missing))
print("coverage_check reconciles")
'
audit = json.loads(job["output"]["output"])
# 1. Every prescan flag id appears exactly once in coverage_check.
sent = [f["id"] for f in INPUT["prescan_facts"]["flags"]]
seen = [c["id"] for c in audit["coverage_check"]]
missing = [i for i in sent if seen.count(i) != 1]
extra = [i for i in seen if i not in sent]
if missing or extra:
raise RuntimeError(f"coverage_check drift: missing={missing} extra={extra}")
# 2. Every focus_areas finding id exists in findings.
ids = {f["id"] for f in audit["findings"]}
for area in audit["focus_areas"]:
for fid in area["finding_ids"]:
if fid not in ids:
raise RuntimeError(f"focus_areas references unknown finding {fid}")
# 3. A truncated reply is a prefix, not an audit. Retry, do not repair.
if job.get("truncated"):
INPUT["retry_note"] = (
"The previous reply was truncated. Return the same twelve controls but at most "
"eight findings, each with a shorter snippet."
)
# ... resubmit with an incremented attempt suffix in the Idempotency-Key.
for c in audit["controls"]:
print(f"{c['status']:8} {c['control']:32} {c['evidence']}")
for f in audit["findings"]:
print(f["id"], f["priority"], f["category"], f["resource"])
print(audit["hardened_settings"])
print("\n".join(audit["commands"])) # always ends with: python manage.py check --deploy
const audit = JSON.parse(job.output.output);
// 1. Every prescan flag id appears exactly once in coverage_check.
const sent = INPUT.prescan_facts.flags.map((f) => f.id);
const seen = audit.coverage_check.map((c) => c.id);
const missing = sent.filter((id) => seen.filter((s) => s === id).length !== 1);
const extra = seen.filter((id) => !sent.includes(id));
if (missing.length || extra.length) {
throw new Error(`coverage_check drift: missing=${missing} extra=${extra}`);
}
// 2. Every focus_areas finding id exists in findings.
const ids = new Set(audit.findings.map((f) => f.id));
for (const area of audit.focus_areas) {
for (const fid of area.finding_ids) {
if (!ids.has(fid)) throw new Error(`focus_areas references unknown finding ${fid}`);
}
}
// 3. A truncated reply is a prefix, not an audit. Retry, do not repair.
if (job.truncated) {
INPUT.retry_note =
"The previous reply was truncated. Return the same twelve controls but at most eight findings.";
}
for (const c of audit.controls) console.log(c.status.padEnd(8), c.control, "—", c.evidence);
for (const f of audit.findings) console.log(f.id, f.priority, f.category, f.resource);
console.log(audit.hardened_settings);
console.log(audit.commands.join("\n")); // always ends with: python manage.py check --deploy
type control struct {
Control string `json:"control"`
Status string `json:"status"`
Evidence string `json:"evidence"`
Requirement string `json:"requirement"`
}
type finding struct {
ID string `json:"id"`
Category string `json:"category"`
Severity string `json:"severity"`
Priority string `json:"priority"`
Resource string `json:"resource"`
Problem string `json:"problem"`
Fix string `json:"fix"`
Snippet string `json:"snippet"`
}
type audit struct {
Posture string `json:"posture"`
Verdict string `json:"verdict"`
DjangoVersion string `json:"django_version"`
SettingsModule string `json:"settings_module"`
Controls []control `json:"controls"`
Findings []finding `json:"findings"`
HardenedSettings string `json:"hardened_settings"`
Commands []string `json:"commands"`
CoverageCheck []struct {
ID string `json:"id"`
Addressed bool `json:"addressed"`
Note string `json:"note"`
} `json:"coverage_check"`
}
var a audit
if err := json.Unmarshal([]byte(job.Output.Output), &a); err != nil {
panic(err)
}
// Every prescan flag id must come back exactly once in coverage_check.
count := map[string]int{}
for _, c := range a.CoverageCheck {
count[c.ID]++
}
for _, id := range []string{"settings:debug-true", "settings:allowed-hosts-wildcard"} {
if count[id] != 1 {
panic("unreconciled prescan flag: " + id)
}
}
fmt.Println(a.Posture, a.DjangoVersion, a.SettingsModule, len(a.Findings), "findings")
// The audit JSON is a string inside data.output.output - parse it, then check
// the two invariants before you trust it:
//
// 1. every prescan_facts.flags id appears exactly once in coverage_check;
// 2. every focus_areas[].finding_ids entry names an id present in findings.
//
// A `truncated` job is a prefix, not an audit: resubmit with a retry_note such as
// "The previous reply was truncated. Return the same twelve controls but at most
// eight findings" and an incremented attempt suffix on the Idempotency-Key.
String auditJson = /* data.output.output */ call("jobs/" + jobId, null);
System.out.println(auditJson);
// controls[] is always the same twelve entries in the same order, so a table can
// be rendered by index without searching for a control by name.
audit = JSON.parse(job["output"]["output"])
# 1. Every prescan flag id appears exactly once in coverage_check.
sent = input["prescan_facts"]["flags"].map { |f| f["id"] }
seen = audit["coverage_check"].map { |c| c["id"] }
missing = sent.reject { |id| seen.count(id) == 1 }
extra = seen - sent
raise "coverage_check drift: #{missing} / #{extra}" unless missing.empty? && extra.empty?
# 2. Every focus_areas finding id exists in findings.
ids = audit["findings"].map { |f| f["id"] }
audit["focus_areas"].each do |area|
area["finding_ids"].each { |fid| raise "unknown finding #{fid}" unless ids.include?(fid) }
end
audit["controls"].each { |c| puts format("%-8s %s", c["status"], c["control"]) }
audit["findings"].each { |f| puts "#{f['id']} #{f['priority']} #{f['category']} #{f['resource']}" }
puts audit["hardened_settings"]
puts audit["commands"].last # python manage.py check --deploy
<?php
$audit = json_decode($job["output"]["output"], true);
// 1. Every prescan flag id appears exactly once in coverage_check.
$sent = array_column($input["prescan_facts"]["flags"], "id");
$seen = array_column($audit["coverage_check"], "id");
$counts = array_count_values($seen);
foreach ($sent as $id) {
if (($counts[$id] ?? 0) !== 1) {
throw new RuntimeException("unreconciled prescan flag: " . $id);
}
}
// 2. Every focus_areas finding id exists in findings.
$ids = array_column($audit["findings"], "id");
foreach ($audit["focus_areas"] as $area) {
foreach ($area["finding_ids"] as $fid) {
if (!in_array($fid, $ids, true)) {
throw new RuntimeException("focus_areas references unknown finding " . $fid);
}
}
}
foreach ($audit["controls"] as $c) {
printf("%-8s %s\n", $c["status"], $c["control"]);
}
echo $audit["hardened_settings"], PHP_EOL;
var audit = JsonSerializer.Deserialize<JsonElement>(auditJson);
// 1. Every prescan flag id appears exactly once in coverage_check.
var seen = audit.GetProperty("coverage_check")
.EnumerateArray()
.Select(c => c.GetProperty("id").GetString())
.ToList();
foreach (var id in new[] { "settings:debug-true", "settings:allowed-hosts-wildcard" })
{
if (seen.Count(s => s == id) != 1)
throw new Exception($"unreconciled prescan flag: {id}");
}
// 2. Every focus_areas finding id exists in findings.
var ids = audit.GetProperty("findings")
.EnumerateArray()
.Select(f => f.GetProperty("id").GetString())
.ToHashSet();
foreach (var area in audit.GetProperty("focus_areas").EnumerateArray())
foreach (var fid in area.GetProperty("finding_ids").EnumerateArray())
if (!ids.Contains(fid.GetString()))
throw new Exception($"focus_areas references unknown finding {fid}");
foreach (var c in audit.GetProperty("controls").EnumerateArray())
Console.WriteLine($"{c.GetProperty("status")} {c.GetProperty("control")}");
The output contract
data.output.output is a JSON string holding one object. This is exactly what the web
app parses, so anything that renders here will render there:
{
"audit_name": "billing-api — production security audit",
"posture": "hardened | needs-hardening | exposed",
"verdict": "one sentence naming the single thing that decides the posture",
"django_version": "4.2.11",
"deploy_target": "production | staging | internal | unknown",
"settings_module": "config/settings/production.py",
"exec_summary": "2-3 paragraphs separated by blank lines",
"assumptions": ["..."],
"open_questions": ["..."],
"inventory": [
{ "kind": "Setting", "name": "DEBUG", "value": "True", "role": "what it controls here" }
],
"controls": [
{ "control": "DEBUG disabled", "status": "missing",
"evidence": "DEBUG = True at settings.py line 12",
"requirement": "DEBUG = False in every module a deployed process loads" }
],
"findings": [
{
"id": "DJ-001",
"category": "settings | authentication | authorization | injection | xss | csrf | data-exposure | dependencies | deployment",
"severity": "low | medium | high",
"likelihood": "low | medium | high",
"priority": "critical | high | medium | low",
"resource": "Setting/DEBUG",
"problem": "...",
"impact": "...",
"fix": "...",
"snippet": "corrected Python / template / shell fragment, or \"\""
}
],
"coverage_check": [
{ "id": "settings:debug-true", "addressed": true, "note": "DJ-001." }
],
"hardened_settings": "a corrected Python settings block, as a JSON string",
"commands": ["python manage.py check --deploy # verify the deployment controls"],
"quick_wins": ["..."],
"focus_areas": [{ "area": "...", "why": "...", "finding_ids": ["DJ-001"] }],
"summary": "closing paragraph"
}
Every key
| key | type | meaning |
|---|---|---|
audit_name | string | Short title naming the project and the posture audited against. |
posture | enum | hardened, needs-hardening or exposed. The single value a CI gate should branch on. |
verdict | string | One sentence justifying the posture and naming the thing that decides it. |
django_version | string | The version the paste shows, e.g. "4.2.11", or "unknown". Never guessed. |
deploy_target | enum | Echoes the posture audited against: production, staging, internal, unknown. |
settings_module | string | The settings file the audit is mostly about, or "(missing from the paste)". |
exec_summary | string | Two to three paragraphs on the dominant themes, separated by blank lines. |
assumptions | string[] | Explicit assumptions filling gaps in the paste — which module production loads, whether a proxy terminates TLS. |
open_questions | string[] | Questions whose answers would change the audit or its ordering. |
inventory | object[] | {kind, name, value, role}. kind is one of Setting, Middleware, App, View, Model, Serializer, Form, Template, UrlRoute, Query, Dependency. |
controls | object[] | {control, status, evidence, requirement}. Always the same twelve controls in the same fixed order — render by index, do not search by name. |
findings | object[] | {id, category, severity, likelihood, priority, resource, problem, impact, fix, snippet}. Ids are sequential DJ-001, DJ-002, … Always at least one entry. snippet is a pasteable fragment or "". |
coverage_check | object[] | {id, addressed, note}. One entry per prescan_facts.flags id, exactly once, and no ids the prescan did not send. addressed: false means deliberately set aside, with the reason in note. |
hardened_settings | string | A corrected Python settings fragment covering only the keys this project needs changed, with a comment on any line whose value the reader must decide. "" when no settings file was pasted. Never contains a real secret. |
commands | string[] | Ordered shell commands, each with a trailing comment. Read-only — nothing that deletes, migrates or deploys. Always ends with python manage.py check --deploy. |
quick_wins | string[] | One-line changes worth doing immediately. |
focus_areas | object[] | {area, why, finding_ids}. Every id in finding_ids must exist in findings. |
summary | string | Closing paragraph: what to do first and what remains after that. |
The enums
| field | values | notes |
|---|---|---|
posture | hardened, needs-hardening, exposed | hardened: the deployment controls are set as a set and the rest is improvement work. needs-hardening: named controls are missing or weak, but nothing is exposing data today. exposed: at least one finding means data or access is available to an attacker right now. |
findings[].category | settings, authentication, authorization, injection, xss, csrf, data-exposure, dependencies, deployment | Authentication and authorization are separate categories on purpose: a missing @login_required and an unscoped object lookup are different findings. |
findings[].severityfindings[].likelihood | low, medium, high | Severity is how bad it is if exploited; likelihood is how reachable it is from the code as pasted. |
findings[].priority | critical, high, medium, low | Severity by likelihood, adjusted for posture. critical is reserved for something exposing data or granting access now on an internet-facing deployment: DEBUG = True in production, a committed SECRET_KEY or database password, a request-reachable injection, or an unauthenticated view returning another user's records. |
controls[].status | present, missing, weak, unknown | unknown is a legitimate answer when the paste does not show enough to decide, and is preferred over a guess. weak means the control exists but does not do its job — a short HSTS max-age without includeSubDomains, say. |
The twelve controls
controls always carries these twelve, in this order, on every run — so a table can be
rendered by index and two audits of the same project are diffable row by row:
1. DEBUG disabled 7. Clickjacking protection
2. ALLOWED_HOSTS restricted 8. Content-type sniffing disabled
3. SECRET_KEY out of source 9. CSRF middleware active
4. HTTPS redirect and HSTS 10. Password validators configured
5. Secure session cookie 11. Authorization enforced per object
6. Secure CSRF cookie 12. Query parameterization
The audit never echoes a secret value. If the paste contains a literal SECRET_KEY, a
database password or an API key, the finding names the setting and says to rotate it — the value
itself does not appear in problem, snippet or
hardened_settings.
Your saved audits
Every run the app completes is written to the audits collection, so an audit follows
the user across devices. It is declared acl_read: owner and
acl_write: user: rows are scoped to the calling subject, which means a script must
reuse one token across the run and the query or it will see an empty collection. Each
POST /guest mints a new guest subject, so guest tokens are not a way to share
history.
| field | type | meaning |
|---|---|---|
uid | string | The app's own id for the run — stable across a re-render. |
title | string | audit_name from the reply. |
posture | string | hardened, needs-hardening or exposed. |
verdict | string | The one-sentence verdict. |
django_version | string | The version the paste showed, or unknown. |
settings_module | string | The settings file the audit was mostly about. |
input_hash | string | Hash of the submitted input — the cheap way to tell whether a project actually changed between runs. |
findings_count | number | findings.length. |
critical_count | number | How many findings came back priority: "critical". |
ran_at | timestamp | When the run completed. The natural sort key. |
Those ten fields are declared, and therefore filterable and orderable. The rest of the document —
the whole audit — round-trips intact but is not indexed. embed is
["title", "verdict", "posture"], so POST /collections/audits/similar with
a text or a record_id finds past audits that read like this one: useful
for "have we seen this failure shape before?" across a fleet of services.
Every where entry must be an operator object — eq, ne,
lt, lte, gt, gte, in (up to 20
values) or contains. The bare-value shorthand {"posture": "exposed"} is
rejected. Records come back wrapped: data.records[].doc holds the fields, alongside a
record_id.
# Your saved audits, newest first.
call collections/audits/query '{"order_by":[{"field":"ran_at","dir":"desc"}],"limit":10}'
# Only the ones that came back exposed with something critical in them.
call collections/audits/query '{"where":{"posture":{"eq":"exposed"},"critical_count":{"gte":1}},
"order_by":[{"field":"ran_at","dir":"desc"}],"limit":20}'
# Past audits that read like this one, using the declared embed fields.
call collections/audits/similar '{"text":"DEBUG left on in production with a committed SECRET_KEY","limit":5"}'
# Every `where` entry must be an operator object - the bare-value shorthand
# ({"posture": "exposed"}) is rejected.
recent = call("collections/audits/query", {
"where": {"posture": {"eq": "exposed"}, "critical_count": {"gte": 1}},
"order_by": [{"field": "ran_at", "dir": "desc"}],
"limit": 20,
})
for rec in recent["records"]:
d = rec["doc"] # the fields nest under .doc, not flat
print(d["ran_at"], d["title"], d["posture"], d["critical_count"], "critical")
# Nearest neighbours over embed = ["title", "verdict", "posture"].
similar = call("collections/audits/similar", {"text": recent["records"][0]["doc"]["verdict"], "limit": 5})
for rec in similar["records"]:
print(rec["doc"]["title"])
const recent = await call("collections/audits/query", {
where: { posture: { eq: "exposed" }, critical_count: { gte: 1 } },
order_by: [{ field: "ran_at", dir: "desc" }],
limit: 20,
});
for (const rec of recent.records) {
const d = rec.doc; // the fields nest under .doc, not flat
console.log(d.ran_at, d.title, d.posture, d.critical_count);
}
// Nearest neighbours over embed = ["title", "verdict", "posture"].
const similar = await call("collections/audits/similar", {
text: "DEBUG left on in production with a committed SECRET_KEY",
limit: 5,
});
console.log(similar.records.map((r) => r.doc.title));
query := map[string]any{
"where": map[string]any{"posture": map[string]any{"eq": "exposed"}},
"order_by": []map[string]string{{"field": "ran_at", "dir": "desc"}},
"limit": 20,
}
raw, err := call("collections/audits/query", query)
if err != nil {
panic(err)
}
var out struct {
Records []struct {
RecordID string `json:"record_id"`
Doc map[string]any `json:"doc"`
} `json:"records"`
}
_ = json.Unmarshal(raw, &out)
for _, r := range out.Records {
fmt.Println(r.Doc["ran_at"], r.Doc["title"], r.Doc["critical_count"])
}
// collections/audits/similar takes {"text": "...", "limit": 5} and ranks over
// the declared embed fields: title, verdict and posture.
String query = """
{"where":{"posture":{"eq":"exposed"},"critical_count":{"gte":1}},
"order_by":[{"field":"ran_at","dir":"desc"}],"limit":20}
""";
String audits = call("collections/audits/query", query);
System.out.println(audits);
// {"ok":true,"data":{"records":[{"record_id":"...","doc":{"title":"...","posture":"exposed",...}}]}}
// The fields nest under .doc - never read them flat off the record.
String similar = call("collections/audits/similar",
"{\"text\":\"DEBUG left on in production\",\"limit\":5}");
System.out.println(similar);
recent = call("collections/audits/query", {
"where" => { "posture" => { "eq" => "exposed" }, "critical_count" => { "gte" => 1 } },
"order_by" => [{ "field" => "ran_at", "dir" => "desc" }],
"limit" => 20,
})
recent["records"].each do |r|
d = r["doc"] # the fields nest under .doc, not flat
puts "#{d['ran_at']} #{d['title']} #{d['posture']}"
end
# Nearest neighbours over embed = ["title", "verdict", "posture"].
similar = call("collections/audits/similar", { "text" => "committed SECRET_KEY", "limit" => 5 })
similar["records"].each { |r| puts r["doc"]["title"] }
<?php
$recent = call("collections/audits/query", [
"where" => ["posture" => ["eq" => "exposed"], "critical_count" => ["gte" => 1]],
"order_by" => [["field" => "ran_at", "dir" => "desc"]],
"limit" => 20,
]);
foreach ($recent["records"] as $rec) {
$d = $rec["doc"]; // the fields nest under .doc, not flat
echo $d["ran_at"], " ", $d["title"], " ", $d["posture"], PHP_EOL;
}
// Nearest neighbours over embed = ["title", "verdict", "posture"].
$similar = call("collections/audits/similar", ["text" => "committed SECRET_KEY", "limit" => 5]);
foreach ($similar["records"] as $rec) {
echo $rec["doc"]["title"], PHP_EOL;
}
var query = new
{
where = new { posture = new { eq = "exposed" }, critical_count = new { gte = 1 } },
order_by = new[] { new { field = "ran_at", dir = "desc" } },
limit = 20,
};
var recent = await Clinic.Call("collections/audits/query", query);
foreach (var rec in recent.GetProperty("records").EnumerateArray())
{
var d = rec.GetProperty("doc"); // the fields nest under .doc, not flat
Console.WriteLine($"{d.GetProperty("ran_at")} {d.GetProperty("title")}");
}
// Nearest neighbours over embed = ["title", "verdict", "posture"].
var similar = await Clinic.Call("collections/audits/similar",
new { text = "committed SECRET_KEY", limit = 5 });
Console.WriteLine(similar.GetProperty("records").GetArrayLength());
A CI gate
The posture value is the natural exit code. Fail the job when a service drifts into
exposed, warn on needs-hardening, and pass on hardened —
with the Idempotency-Key derived from the input so a re-run of the same commit replays instead of
re-billing. Comparing input_hash against the last row in the audits
collection tells you whether it is even worth spending the credits.
POSTURE=$(printf '%s' "$AUDIT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["posture"])')
CRITICAL=$(printf '%s' "$AUDIT" | python3 -c 'import sys,json;print(sum(1 for f in json.load(sys.stdin)["findings"] if f["priority"]=="critical"))')
case "$POSTURE" in
exposed) echo "::error::Django audit: exposed ($CRITICAL critical)"; exit 1 ;;
needs-hardening) echo "::warning::Django audit: needs hardening"; exit 0 ;;
hardened) echo "Django audit: hardened"; exit 0 ;;
esac