BETA
Skip to content

Remediation

Once you have triaged a finding, Krafter Audit gives you a full remediation workflow: create tasks, run verifications, configure policies, connect external tools, and generate reports. This guide covers each stage end-to-end.

Remediation tasks

A task is the unit of work that closes a finding. It carries an owner, a priority, an SLA, and optional pointers to an external ticket or pull request. Each task belongs to exactly one finding.

Creating a task

Create a task from a finding:

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/tasks" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "finding_id": "d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
    "title": "Add Content-Security-Policy header",
    "priority": "high",
    "owner_id": "22222222-2222-2222-2222-222222222222",
    "sla_due_at": "2025-07-01T00:00:00Z"
  }'
json
// 201 Created
{
  "data": {
    "id": "f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
    "finding_id": "d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
    "title": "Add Content-Security-Policy header",
    "status": "backlog",
    "priority": "high",
    "owner_id": "22222222-2222-2222-2222-222222222222",
    "sla_due_at": "2025-07-01T00:00:00Z",
    "external_ticket_key": null,
    "pr_url": null,
    "created_at": "2025-06-15T12:00:00Z",
    "updated_at": "2025-06-15T12:00:00Z"
  },
  "meta": {
    "request_id": "F-mxbAQk6m9lXeQAAATj"
  },
  "error": null
}

Tasks have no description or assignee (email) fields. The remediation guidance lives on the linked finding's recommended_fix object, and ownership is tracked by owner_id (a user UUID). To link an external tracker or PR, use external_ticket_key and pr_url instead.

Task statuses

Tasks follow this lifecycle:

backlog → in_progress → ready_verify → done
       ↘ blocked ↗
StatusMeaning
backlogTask has been created, work has not started. The default initial status.
in_progressSomeone is actively working on the fix.
blockedWork is paused waiting on an external dependency.
ready_verifyWork is complete; the next step is to run a verification.
doneThe fix has been verified. Set automatically when a verification is approved.

priority is one of low, medium, high, critical and defaults to medium for new tasks.

Don't set done by hand

Approving a verification automatically transitions the linked task to done and the linked finding to resolved. The recommended path is to move the task to ready_verify, run a verification, and let the approval close the loop.

Updating a task

Only the fields you provide are changed; the request must include at least one updatable field.

bash
curl -X PATCH "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/tasks/f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "in_progress",
    "pr_url": "https://github.com/acme/web-app/pull/142"
  }'

The response is the full task shape with the new fields applied. The finding_id cannot be changed once the task is created — make a new task instead if you need to re-link work.

Listing tasks

Filter tasks by status:

bash
curl "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/tasks?status=in_progress&limit=25" \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": [
    {
      "id": "f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
      "finding_id": "d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
      "title": "Add Content-Security-Policy header",
      "status": "in_progress",
      "priority": "high",
      "owner_id": "22222222-2222-2222-2222-222222222222",
      "sla_due_at": "2025-07-01T00:00:00Z",
      "external_ticket_key": null,
      "pr_url": "https://github.com/acme/web-app/pull/142",
      "created_at": "2025-06-15T12:00:00Z",
      "updated_at": "2025-06-15T13:00:00Z"
    }
  ],
  "meta": {
    "request_id": "G-nyc8sPL2vXfRBBBEUk",
    "total": 1,
    "next_cursor": null
  },
  "error": null
}

Verification workflow

A verification confirms that a remediation task actually fixed the underlying finding. Verifications are created against a single task — the task's linked finding is what gets re-checked. Each call consumes one unit of the team's AI quota.

Running a verification

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/verifications/run" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "task_id": "f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c"
  }'
json
// 202 Accepted
{
  "data": {
    "id": "f1e2d3c4-b5a6-9788-7c6d-5e4f3a2b1c0d",
    "task_id": "f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
    "result": "pending",
    "checks": [
      { "key": "security_header_csp", "before": "failed", "after": "pending", "status": "running" }
    ],
    "verified_by_id": null,
    "verified_at": null,
    "created_at": "2025-06-20T15:00:00Z",
    "updated_at": "2025-06-20T15:00:00Z"
  },
  "meta": {
    "request_id": "H-pzd9tQM3wYgSCCCFVl"
  },
  "error": null
}

result is one of pending, passed, failed. checks carries per-check {key, before, after, status} records that the worker fills in as it runs. There are no reviewer or notes fields — the audit trail is the per-check before/after pairs.

One task per call

The endpoint takes a single task_id, not an array. To verify multiple tasks, call /verifications/run once per task.

Approving a verification

If the verification confirms the fix, approve it. This is a transactional action: on success the verification's result becomes passed, the linked task moves to done, the linked finding moves to resolved, and any open regressions for that finding are closed.

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/verifications/f1e2d3c4-b5a6-9788-7c6d-5e4f3a2b1c0d/approve" \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": {
    "id": "f1e2d3c4-b5a6-9788-7c6d-5e4f3a2b1c0d",
    "task_id": "f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
    "result": "passed",
    "checks": [
      { "key": "security_header_csp", "before": "failed", "after": "passed", "status": "passed" }
    ],
    "verified_by_id": "22222222-2222-2222-2222-222222222222",
    "verified_at": "2025-06-20T15:30:00Z",
    "created_at": "2025-06-20T15:00:00Z",
    "updated_at": "2025-06-20T15:30:00Z"
  },
  "meta": {
    "request_id": "I-qae0uRN4xZhTDDDGWm"
  },
  "error": null
}

Reopening a verification

If the issue persists, reopen the verification. This is the inverse transactional action: result becomes failed, the linked task moves back to in_progress, the linked finding moves back to in_progress, and a new regression is recorded against the finding with trigger: "verification_reopened".

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/verifications/f1e2d3c4-b5a6-9788-7c6d-5e4f3a2b1c0d/reopen" \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": {
    "id": "f1e2d3c4-b5a6-9788-7c6d-5e4f3a2b1c0d",
    "task_id": "f6a7b8c9-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
    "result": "failed",
    "checks": [
      { "key": "security_header_csp", "before": "failed", "after": "pending", "status": "failed" }
    ],
    "verified_by_id": "22222222-2222-2222-2222-222222222222",
    "verified_at": "2025-06-20T16:00:00Z",
    "created_at": "2025-06-20T15:00:00Z",
    "updated_at": "2025-06-20T16:00:00Z"
  },
  "meta": {
    "request_id": "J-rbf1vSO5yajUEEEHXn"
  },
  "error": null
}

Listing verifications

bash
curl "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/verifications" \
  -H "Authorization: Bearer kr_live_abc123def456"

The list response is the array form of the same task_id / result / checks shape shown above. See the Verifications API reference for the full field reference.

Policy management

Each project has a single active policy interpreted by the scanner. The config field is a free-form JSON object — the API stores it opaquely and does not validate the shape of individual rules. The example below shows the default policy the API creates on first read; once you write your own config it is echoed back exactly as sent.

Getting the current policy

bash
curl "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/policies/current" \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": {
    "id": "b9c8d7e6-5a4f-3e2d-1c0b-9a8b7c6d5e4f",
    "name": "Default Audit Policy",
    "org_id": "11111111-1111-1111-1111-111111111111",
    "project_id": "production-website",
    "config": {
      "severity_rules": {
        "security": "critical",
        "performance": "high",
        "accessibility": "medium",
        "seo": "low"
      },
      "sla_hours": { "critical": 24, "high": 72, "medium": 168, "low": 336 },
      "alert_routing": { "critical": ["slack", "email"], "high": ["slack"] }
    },
    "active": true,
    "updated_at": "2025-06-08T12:00:00Z"
  },
  "meta": {
    "request_id": "K-scg2wTP6zbkVFFFIYo"
  },
  "error": null
}

Updating the policy

PUT replaces the active policy's name and config. The config field is replaced wholesale, not merged — always send the complete config you want to retain.

bash
curl -X PUT "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/policies/current" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Strict Production Policy",
    "config": {
      "severity_rules": { "security": "critical", "performance": "high" },
      "sla_hours": { "critical": 12, "high": 48 }
    }
  }'

The response is the full policy shape with the new fields reflected.

Testing a policy

The test endpoint runs a what-if simulation against the project's current findings. Today it returns the project's existing finding count, the count already at critical severity, and echoes the submitted config back as config_preview. Use it to wire up the simulation UI without surprises — note that the supplied config is not yet applied to override severities.

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/policies/test" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "config": { "severity_rules": { "performance": "high" } }
  }'
json
{
  "data": {
    "simulated_findings": 42,
    "escalated_to_critical": 3,
    "config_preview": {
      "severity_rules": { "performance": "high" }
    }
  },
  "meta": {
    "request_id": "L-tdh3xUQ7aclWGGGJZp"
  },
  "error": null
}

Integrations

Connect external systems to your audit workflow. Supported providers: ga4, gsc, sentry, github, gitlab, jira, linear, cloudflare. The Audit API tracks one row per provider per project.

Listing integrations

The list endpoint always returns one entry per supported provider — providers that have never been connected return a placeholder row with status: "disconnected" and id: null.

bash
curl "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/integrations" \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": [
    {
      "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "org_id": "11111111-1111-1111-1111-111111111111",
      "project_id": "production-website",
      "provider": "github",
      "status": "connected",
      "last_sync_at": "2025-06-10T14:00:00Z",
      "created_at": "2025-06-01T10:00:00Z",
      "updated_at": "2025-06-10T14:00:00Z"
    },
    {
      "id": null,
      "provider": "sentry",
      "status": "disconnected",
      "credentials": {},
      "last_sync_at": null,
      "inserted_at": null,
      "updated_at": null
    }
  ],
  "meta": {
    "request_id": "M-uei4yVR8bdmXHHHKaq"
  },
  "error": null
}

status is one of connected, disconnected, syncing, error. Credentials are never echoed back — see the Integrations API → Security for details on AES-256-GCM encryption at rest.

Connecting an integration

The request body is provider-agnostic: pass the provider's secrets in credentials (a free-form map) or a short-lived OAuth auth_code. Per-provider key names (e.g. token, repository) are conventions the worker interprets, not validated by the API.

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/integrations/github/connect" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": {
      "token": "ghp_xxxxxxxxxxxx",
      "repository": "acme/web-app"
    }
  }'

The connect call upserts on (team, project, provider) — calling it again replaces the stored credentials.

Syncing an integration

Trigger a manual sync. The integration is moved to status: "syncing" synchronously and a background worker pushes/pulls findings and tasks. The endpoint takes no body — it always syncs everything pending for that provider.

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/integrations/github/sync" \
  -H "Authorization: Bearer kr_live_abc123def456"

Poll List Integrations to see when status flips back to connected (or error) and last_sync_at advances.

Reports

Aggregate audit metrics, export project state, and share the result over email, Slack, or a signed link.

Report summary

The summary returns a 7-day risk burn-down series, the verification pass rate, the regression rate, and the count of open findings. Counts always reflect the project's current state — date filtering query parameters are accepted but not yet wired up.

bash
curl "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/reports/summary" \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": {
    "risk_burn_down": [
      { "date": "2025-06-04", "open_risk_score": 320 },
      { "date": "2025-06-05", "open_risk_score": 305 },
      { "date": "2025-06-06", "open_risk_score": 290 },
      { "date": "2025-06-07", "open_risk_score": 260 },
      { "date": "2025-06-08", "open_risk_score": 240 },
      { "date": "2025-06-09", "open_risk_score": 220 },
      { "date": "2025-06-10", "open_risk_score": 210 }
    ],
    "verification_pass_rate": 0.84,
    "regression_rate": 0.12,
    "open_findings": 11
  },
  "meta": {
    "request_id": "N-vfj5zWS9cenYIIILbr"
  },
  "error": null
}

open_findings counts findings in non-terminal statuses (new, triaged, in_progress, ready_verify). regression_rate is open_regressions / (resolved + dismissed findings).

Exporting a report

Queue an export of the current report summary as JSON, CSV, or PDF. Each export consumes one unit of the team's AI quota.

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/reports/export" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "json"
  }'
json
// 202 Accepted
{
  "data": {
    "id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
    "org_id": "11111111-1111-1111-1111-111111111111",
    "project_id": "production-website",
    "period_start": "2025-06-04",
    "period_end": "2025-06-10",
    "format": "json",
    "status": "queued",
    "snapshot": { "open_findings": 11, "verification_pass_rate": 0.84 },
    "exported_at": null,
    "shared_at": null,
    "created_at": "2025-06-12T15:00:00Z",
    "updated_at": "2025-06-12T15:00:00Z"
  },
  "meta": {
    "request_id": "O-wgk6aXT0dfoZJJJMcs"
  },
  "error": null
}

status transitions through queued → exporting → exported → shared as the worker progresses (or failed if it errors). Available formats: pdf, csv, json. The pdf format currently produces an HTML artefact while the binary renderer is being wired up.

Sharing a report

Share the project's most recent report. The endpoint always operates on the latest report row — there is no report_id parameter.

bash
curl -X POST "https://app.krafter.dev/api/v1/orgs/:org_id/projects/:project_id/audit/reports/share" \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "link"
  }'
json
{
  "data": {
    "id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
    "channel": "link",
    "recipients": [],
    "shared_at": "2025-06-20T16:00:00Z",
    "share_url": "https://app.krafter.dev/insights/audit/reports/shared/SFMyNTY.g2gDdAAAAAQ..."
  },
  "meta": {
    "request_id": "P-xhl7bYU1egpaKKKNdt"
  },
  "error": null
}

share_url is populated only when channel: "link". For email, pass a recipients array of addresses. For slack, pass a webhook_url (Slack incoming-webhook URL — the URL is SSRF-validated and delivery is best-effort).

Next steps

  • Quickstart — Run your first scan and triage a finding
  • Findings Guide — Severity levels, status lifecycle, filtering, bulk operations, and regression tracking

Built by Krafter Studio