Push Vulnerability Findings
Use the API to push findings into Scrut from security tools that don't have a native integration.
This guide shows you how to push findings from a security tool without a native integration into Scrut. You'll map the tool's findings to Scrut fields, create them as third-party findings, and confirm that they synced.
Before You Begin
You need:
- A Read & Write credential, created by an Org Admin in Settings → Developer Console. Creating findings requires the
vulnerability:writescope, which only Read & Write credentials have. See Create API Credentials. - An access token requested with that credential. Confirm that the
scopefield in the token response includesvulnerability:write. See the API Quickstart. - Access to the findings in your tool, through its API or an export.
The examples use https://api.scrut.io. Replace it with your region's base URL.
Syncing findings on a recurring schedule? Run the same three steps below from a cron job or a CI/CD step, and pace your requests to stay within the rate limits.
Map your findings to Scrut fields
Map each finding from your tool to the fields that POST /v1/vulnerabilities accepts. The request body accepts only these fields.
| Field | Required | Format and rules |
|---|---|---|
vulnerabilityId | Yes | A CVE ID, such as CVE-2024-1234, or VUL- followed by exactly four digits, such as VUL-5672. |
title | Yes | The finding title. |
severity | Yes | critical, high, medium, or low. |
status | Yes | open, acknowledged, or closed. You can't create a finding as ignored or risk. |
description | No | Up to 750 characters. |
remediation | No | Up to 750 characters. |
stepsToReproduce | No | Up to 750 characters. |
fixAvailable | No | yes or no. |
firstSeen | No | Unix epoch milliseconds. |
affectedResources | No | An array of resources. Each resource has a name, a type, and up to three tags. |
assigneeEmail | No | The email address of the person to assign the finding to. |
Note: You can't send source. Scrut sets it to api for every finding created through the API.
Create the finding
Call POST /v1/vulnerabilities for each finding. Each request creates one finding.
Each request also needs an Idempotency-Key header. Generate one key per finding and store it with the finding's record in your sync job.
Important: If a request fails with a network error, a timeout, a 429, or a 5xx status, retry it with the same key and the same body so Scrut doesn't create the finding twice.
curl -X POST https://api.scrut.io/v1/vulnerabilities \
-H "Authorization: Bearer $SCRUT_ACCESS_TOKEN" \
-H "Idempotency-Key: 3b2f6c1e-9a4d-4e7b-8c5a-1f2e3d4c5b6a" \
-H "Content-Type: application/json" \
-d '{
"vulnerabilityId": "VUL-5672",
"title": "Public S3 bucket",
"description": "Bucket is world-readable.",
"remediation": "Block public ACLs.",
"stepsToReproduce": "Open the bucket URL.",
"severity": "high",
"status": "open",
"fixAvailable": "no",
"firstSeen": 1704067200000,
"affectedResources": [
{ "name": "s3://bucket", "type": "s3", "tags": ["public", "prod"] }
],
"assigneeEmail": "jordan@example.com"
}'
const finding = {
vulnerabilityId: "VUL-5672",
title: "Public S3 bucket",
description: "Bucket is world-readable.",
remediation: "Block public ACLs.",
stepsToReproduce: "Open the bucket URL.",
severity: "high",
status: "open",
fixAvailable: "no",
firstSeen: 1704067200000,
affectedResources: [
{ name: "s3://bucket", type: "s3", tags: ["public", "prod"] },
],
assigneeEmail: "jordan@example.com",
};
const response = await fetch("https://api.scrut.io/v1/vulnerabilities", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
// Reuse the key stored for this finding when you retry.
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify(finding),
});
const { data } = await response.json();
console.log(data.vulnerabilityId);
import requests
finding = {
"vulnerabilityId": "VUL-5672",
"title": "Public S3 bucket",
"description": "Bucket is world-readable.",
"remediation": "Block public ACLs.",
"stepsToReproduce": "Open the bucket URL.",
"severity": "high",
"status": "open",
"fixAvailable": "no",
"firstSeen": 1704067200000,
"affectedResources": [
{"name": "s3://bucket", "type": "s3", "tags": ["public", "prod"]}
],
"assigneeEmail": "jordan@example.com",
}
response = requests.post(
"https://api.scrut.io/v1/vulnerabilities",
headers={
"Authorization": f"Bearer {access_token}",
# Reuse the key stored for this finding when you retry.
"Idempotency-Key": idempotency_key,
},
json=finding,
)
print(response.json()["data"]["vulnerabilityId"])
Expected response (201), with the created finding:
{
"data": {
"vulnerabilityId": "api%23VUL-5672",
"title": "Public S3 bucket",
"description": "Bucket is world-readable.",
"status": "open",
"severity": "high",
"source": "api",
"assignees": [
{ "name": "Jordan Kim", "email": "jordan@example.com", "isPrimary": true }
],
"firstSeen": 1704067200000,
"cveId": "",
"affectedResources": [
{ "name": "s3://bucket", "type": "s3", "tags": ["public", "prod"] }
],
"remediation": "Block public ACLs.",
"stepsToReproduce": "Open the bucket URL.",
"customFields": []
},
"meta": {
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
}
Copy the returned vulnerabilityId. You use it to read the finding in the next step.
If you retry with the same key and body within 24 hours, Scrut returns the stored response with the header Idempotent-Replayed: true and doesn't create the finding again. Stored responses are kept for 24 hours only, so track which findings your sync has already created instead of relying on the key alone. See Idempotency.
Verify your synced findings
Call GET /v1/vulnerabilities with source=api to list the findings your sync created.
curl "https://api.scrut.io/v1/vulnerabilities?source=api&count=100" \
-H "Authorization: Bearer $SCRUT_ACCESS_TOKEN"
The vulnerabilities list is paginated:
- Set
countto50or100. The default is50. - To fetch the next page, pass
meta.nextCursoras thenextCursorquery parameter.meta.nextCursorisnullon the last page. meta.totalCountis the number of findings that match your filters across all pages.
To read one finding, use the vulnerabilityId from the create response or the list. To include its affected resources, add affectedResources to fields:
curl "https://api.scrut.io/v1/vulnerabilities/api%23VUL-5672?fields=affectedResources" \
-H "Authorization: Bearer $SCRUT_ACCESS_TOKEN"
Important: The returned vulnerabilityId is already URL-encoded. Paste it into the path as is. Encoding it again causes a 404 not_found.
Note: Each request creates one finding, and there is no bulk create endpoint. The create request doesn't accept custom fields or a report link, and you can't create a finding with the status ignored or risk.
Rate Limits and Retries
Data endpoints allow 50 requests per 60 seconds per credential. Each finding you create uses one request, and every list call also counts toward this limit.
- Check the
X-RateLimit-Remainingheader to pace your sync. - When you receive
429 rate_limit_exceeded, wait for the number of seconds in theRetry-Afterheader before you retry. - Retry
429,500, and503responses with the sameIdempotency-Keyand the same body. - Don't retry
400,403, or404responses. Fix the request first.
Congratulations
You've pushed findings from your security tool into Scrut and confirmed that they synced. Each synced finding has source set to api, so you can filter for them at any time with source=api.
Contact support@scrut.io for further assistance.