# Security Center API

> List vulnerability findings in the packages your services run, work through them, and read scan coverage, the security score and the settings.

Source: https://www.apitraffic.io/docs/api-reference/security-center

The Security Center checks the npm packages your services run, and the Node.js version of each running instance, against the GitHub Advisory Database and Node.js CVEs from NVD. The SDK reports the packages; nothing needs a repository connection. See [Find vulnerable packages you run](https://www.apitraffic.io/docs/security-center) for how it works in the app.

**Availability:** the Security Center is being rolled out; while it is off for an account every route here answers `404` with `SECURITY_CENTER_NOT_ENABLED`. **Plan:** included on every plan. **Permissions:** any member, or a `user` or `administrator` API token, can read findings, coverage and the score, and acknowledge, resolve or reopen findings. Accepting a risk and changing the settings need an owner or admin, or an `administrator` API token. A token with access scopes needs `security:read` to read and `security:write` for the rest.

## The finding object

A finding is one vulnerable package version (or Node.js version) for one advisory, in one bucket and environment.

- **sid (string, required)**:
  The finding's identifier (`sfnd_` followed by 27 letters and digits).

- **bucketSid (string, required)**:
  The bucket.

- **environmentSid (string | null)**:
  The environment, or `null` for reports from a token without one.

- **kind (string, required)**:
  `package` (an npm package) or `node` (a Node.js runtime CVE, matched against each instance's own Node version).

- **packageName (string, required)**:
  The package, or `node`.

- **version (string, required)**:
  The vulnerable version that runs.

- **advisoryId (string, required)**:
  The GitHub advisory id (`GHSA-...`) for a package, or the CVE id for Node.js. `cveIds` lists the CVE ids and `summary` the advisory's summary.

- **severity (string, required)**:
  `critical`, `high`, `medium` or `low`.

- **kev (boolean, required)**:
  In CISA's Known Exploited Vulnerabilities catalog: exploited in the wild.

- **loaded (string, required)**:
  `true` when a running instance has loaded the package, `false` when it is installed but not loaded, `unknown` for ES modules (which Node does not list).

- **direct (boolean, required)**:
  Listed in the app's own `package.json`.

- **introducedBy (object)**:
  One finding only, for a package that runs: how it got there. `direct` when the app lists it itself, and `chains`, the shortest ways from a package the app lists to it, each a list of `{ name, version }` from the direct dependency to the vulnerable package (`express@4.18.2`, then `qs@6.11.0`), and `uncertain` when a package on a chain is installed at more than one version (dependencies are reported by name, so which version a parent pulls in cannot be told). `tree` holds the same ways merged on the packages they share: each node is `{ name, version, children }`, with `listed` on a top node (`true`: the app lists it; `false`: no way reaches a package the app lists, so the top is a package nothing installed declares, in a monorepo one the repository root or another app lists), `target` on the vulnerable package and `multipleVersions` on a package installed at more than one version. `truncated` when more ways of that length exist than the 8 shown. Read from the declared dependency names of what runs in the finding's bucket and environment now.

- **fixedVersion (string | null)**:
  The first version that fixes it, when the advisory names one.

- **status (string, required)**:
  `open`, `acknowledged`, `accepted_risk`, `fix_deployed`, `resolved` or `excluded`. **Fix deployed** is set when no running instance reports the vulnerable version any more; a person then resolves it. Nothing resolves on its own, except a finding whose advisory was withdrawn (`closedReason` is then `advisory_withdrawn`) or no longer covers a version that still runs (`not_covered`); if that changes back it returns on its own to the status it had, with its note and any accepted risk's expiry, as an Excluded finding does. **Excluded**: the account leaves the package out of scanning. It is never resolved, is left out of the score, the open counts and the default list, takes no action, and when the package is let in again goes back to the status it had before, with the same note, `statusBy` and, for an accepted risk, the same `acceptedUntil` (`open` if that has passed meanwhile). The history shows both changes (`excluded`, then `tracked_again`).

- **statusNote (string | null)**:
  The note or reason given with the last change, with `statusBy` (the user) and `statusAt`.

- **acceptedUntil (string | null)**:
  For an accepted risk, when it returns to Open.

- **present (boolean, required)**:
  Whether a running instance reports the vulnerable version now.

- **services (object[], required)**:
  The services that run it: `serviceSid` (or `serviceKey` before the service is known) and `instances`. `instanceCount` is the total.

- **production (boolean)**:
  Whether its environment counts as production.

- **priority (string[])**:
  Why it sits where it does in the list: `malicious`, `kev`, `severity:`, `loaded`, `production`, `not_protected` (the WAF does not protect against it, or only detects it).

- **malicious (boolean)**:
  A package published with malicious code (a GitHub malware advisory). Always critical; its WAF coverage is `not_addressable`.

- **wafCoverage (string | object)**:
  When the plan includes WAF coverage (`securityWafCoverage`): in a list, its state; for one finding, the coverage per policy (see [Get a finding](#get-a-finding)). States: `protected`, `detecting` (monitor mode), `fixable` (not protected, with the step), `switched_off` (ApiTraffic switched a covering rule off for everyone), `no_policy`, `not_addressable` (no WAF rule can help) and `unknown` (not known yet: the vulnerability has not been triaged for WAF coverage).

- **firstSeen (string, required)**:
  When it was first found, with `lastSeen` (when a running instance last reported it).

## List findings

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings`

Findings in priority order: a malicious package first, then in KEV, severity, loaded, in production, not protected by the WAF, newest. Returns `total` and `findings`, and `advisoriesLoaded`: `false` while the platform's GitHub advisory sync has not yet succeeded once, when npm findings cannot appear yet, so an empty list is not a clean one (left out when it cannot be read just now).

### Query parameters

- **bucketSid (query, string)**: Only this bucket.

- **segmentSid (query, string)**: Only the buckets of this [segment](https://www.apitraffic.io/docs/api-reference/segments), in place of `bucketSid`. See [Segments](#segments).

- **environmentSid (query, string)**: Only this environment.

- **status (query, string)**:
  Statuses, separated by commas: `open`, `acknowledged`, `accepted_risk`, `fix_deployed`, `resolved`, `excluded`. Default: `open,acknowledged,accepted_risk,fix_deployed`.

- **severity (query, string)**: Severities, separated by commas.

- **kind (query, string)**: `package` or `node`.

- **kev (query, boolean)**: Only findings in KEV.

- **packageName (query, string)**: Only this package.

- **wafCoverage (query, string)**: WAF coverage states, separated by commas (needs WAF coverage in the plan).

- **limit (query, integer, default 50)**: 1 to 200.

- **offset (query, integer, default 0)**: Findings to skip.

## Get a finding

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings/{findingSid}`

The finding with `advisory` (`source`, `cvssScore`, `cvssVector`, `cwes`, `references`, `publishedAt`, `withdrawnAt`) and `history`: every status change, oldest first, each with `action`, `fromStatus`, `toStatus`, `userSid` (the person, or `null` for an API token or the Security Center), `note`, `acceptedUntil` and `at`. People's actions are `acknowledged`, `risk_accepted`, `resolved` and `reopened`; the Security Center's are `opened`, `fix_deployed`, `version_returned` (the vulnerable version runs again), `tracked_again` (one the Security Center had closed or excluded is tracked again: its advisory covers the version again, or the package is scanned again), `risk_expired`, `withdrawn`, `not_covered` and `excluded`.

With WAF coverage in the plan, `wafCoverage` is an object: `state`; `cves`, each CVE with its triage `verdict` (`covered`, `new_rule`, `not_waf_addressable`, or `null` when not known yet), `rules`, `since` (the first ruleset revision with them) and `reason`; `rules`, every rule that covers it; `policy`, the policy the SDK runs here (the most specific one); `policies`, every policy that applies, each with `sid`, `name`, `mode`, `runs`, `state` and its `rules` (`ruleId`, `state`, `reason` and `fix` for each); and `fixes`, the steps that would protect it in the policy that runs.

## Attack attempts

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings/{findingSid}/attack-attempts`

Matches of the rules that cover a finding on requests to its bucket and environment, from the WAF analytics: `windows` for the last 7 and 30 days, each with `matched`, `blocked`, `wouldBlock` (by a policy in monitor mode), `tagged`, `lastSeen` and the same per rule in `rules`. A request two covering rules matched counts for each. `rules` is empty when no rule is known to cover it; `available` is `false` when the analytics could not be read just now. A token with access scopes needs `waf:read` too. The plan gate is `securityWafCoverage`.

## Coverage updates

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/coverage-updates`

Takes `bucketSid`, `environmentSid` and `limit` (1 to 200, default 50). Returns `total` and `items`, newest first, each with `kind`, `id` and `at`:

- `ruleset_released`: `rulesetId`, `version`, how many rules it `added`, `patched` and `deprecated`, the `cves` its rules cover, and `policies` (`withIt`: policies tracking latest or pinned to it or later; `pinnedBelow`).
- `cve_covered`: `cve`, `severity`, `kev`, `verdict`, `rulesetId`, `since`, `rules`, and `findings` (your active findings for it: `count` and up to five `examples`).
- `rule_switched_off` and `rule_switched_on`: `ruleId`, `rulesetId`, `rulesetVersions` (null: every revision), the `cves` it covers and your `findings` for them; a switch-off says whether it is still `active`. Only rules one of your policies runs.

The plan gate is `securityWafCoverage`.

## Work through findings

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings/acknowledge`

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings/accept-risk`

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings/resolve`

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/security/findings/reopen`

Each takes one finding or up to 100 at once, and checks each on its own. The reply lists `changed` (`sid`, `from`, `to`) and `refused` (`sid`, `reason`: `not_found` or `invalid_status`, with the finding's `status`).

| Action | From | Body |
| --- | --- | --- |
| Acknowledge | Open | `findingSids`, optional `note` |
| Accept risk (admins) | Open, Acknowledged, Accepted risk | `findingSids`, `reason` (required), optional `expiresAt` (90 days by default, at most a year) |
| Resolve | any status but Resolved | `findingSids`, optional `note` |
| Reopen | Acknowledged, Accepted risk, Fix deployed, Resolved | `findingSids`, optional `note` |

An accepted risk returns to Open when it expires. A finding whose vulnerable version comes back after it went reopens on its own, with its history kept.

```bash
curl -X POST https://api.apitraffic.io/v1/accounts/acc_.../security/findings/accept-risk \
  -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
  -d '{"findingSids": ["sfnd_..."], "reason": "Only reachable inside the VPN", "expiresAt": "2027-03-01T00:00:00Z"}'
```

## Overview and scan coverage

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/overview`

Takes `bucketSid` (or `segmentSid`) and `environmentSid`. Returns:

- `findings`: `open` (active findings by severity, with `total`), `kev`, `loaded`, `malicious` (malicious packages still active), and `byStatus`; with WAF coverage in the plan, `waf`: active findings by coverage state (`byState`), how many the WAF does not protect (`notProtected`) and how many are not known yet (`unknown`).
- `coverage`: `scanningEnabled`, `totals`, and `buckets`, each with its `services`: `serviceSid`, `running` instances, how many report a `complete` inventory, a `partial` one or a `bundled` app the SDK cannot read, how many of the complete ones are bundles scanned from a CLI upload (`cli`), `notScanned`, and the `reason`: `switched_off`, `old_sdk` (an SDK that cannot report packages; its versions in `oldSdkVersions`), `not_reporting` (an SDK that can, reporting nothing: check `API_TRAFFIC_DEPENDENCY_INVENTORY` and the settings), `bundled` (with `cliUploaded`: the service has CLI uploads, none of them for the release that runs; and `releaseReported`: false when its servers report no commit or version, so no upload can be matched to them), `partial`, or `cli_stale` (a CLI upload whose list predates a scope let back in: upload again).
- `sync`: when GitHub advisories (`githubAt`) and NVD (`nvdAt`) were last synced, and `advisoriesLoaded`: `false` until the GitHub advisory sync has succeeded once (package vulnerability data has not loaded yet, and findings for npm packages appear after the first sync), `null` when that cannot be read just now.

## Security score

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/score`

Takes `bucketSid` (or `segmentSid`) and `environmentSid`. The score from 0 to 100 now, with `grade` (A 90 and up, B 80, C 70, D 60, F below), `categories` (each with `weight`, `score`, `deducted`, `assessed` and the `items` that cost points, each with its `cost` and `link`), `cap` (when incomplete scanning caps the score: `limit`, `reason` and the `points` it takes), `biggestWins` (the three items that would raise it most, with the `gain`), `coverage` and `change` against 7 and 30 days ago. The categories always add up to the score.

**Vulnerabilities** (40 points: each open finding costs 10, 5, 2 or 0.5 by severity, doubled in KEV, halved when not loaded, halved outside production, halved when the WAF protects against it in block mode (scoring version 3, with WAF coverage in the plan; each item's `factors.waf` and `wafCoverage` say so); an accepted risk costs half, fix deployed and resolved nothing). **WAF protection** (25), **SDK and runtime** (10), **Certificates** (10) and **Account access** (15) are scored by the [posture checks](#posture), each item at its rule's cost; an item's `rule` names it (`waf.no_policy`, `account.mfa`, ...). Every category is `assessed` (scoring version 2; version 1 kept WAF protection, Certificates and Account access at their full weight), unless the plan leaves the checklist out (`notInPlan`) or a posture category could not be assessed (`unassessed`: `sample` or `monitoring_off`). The 7 and 30 day `change` compares only with scores of the same scoring version that assessed the same posture categories (so the checklist joining or leaving the plan, or certificate monitoring switched off, is not shown as a change). When the posture checks cannot be read, the request fails with `SECURITY_SCORE_UNAVAILABLE` (2977, status 503) rather than scoring without them. With the checklist in the plan, `posture` is the checklist as the posture endpoint returns it. Below 90% of instances reporting a complete inventory the score is capped at 80; with no inventory at all, at 60. Until the GitHub advisory sync has succeeded once, the score may leave npm findings out and is `provisional` (`reason` `advisories_not_loaded`, with a `message`): show it as provisional, not final. Otherwise `provisional` is `null`.

## Posture

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/posture`

Takes `bucketSid` (or `segmentSid`) and `environmentSid`. Returns `checks`, every posture check whether it passes or not: `rule`, `category` (`waf`, `sdk`, `certificates` or `account`), `title` (what passing means), `status` (`pass`, `attention`, or `not_assessed` with a `reason`: `sample` for the sample data, whose members and tokens are its own, or `monitoring_off` while certificate monitoring is switched off), `cost` (the points each item costs) and its `items`, each with `id`, `label` (what is wrong), `why`, `fix`, `cost` and `link` (where it is fixed: `waf`, `waf_policy` with `sid`, `services`, `node` with `major`, `certificate` with `host` and `watchSid`, `account_security` or `tokens`). `summary` counts the `items`, the `passing` checks and those `notAssessed`, and the `points` the items take off the score (each category's total, at most its weight). Needs `security:read`. The plan gate is `securityPosture`.

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/score/history`

The daily score, 90 days by default (`days`, 1 to 365), oldest first, with `scoringChanges`: the days the scoring itself changed.

## Settings

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/security/settings`

`PATCH https://api.apitraffic.io/v1/accounts/{accountSid}/security/settings`

`dependencyScanningEnabled` (on by default), `excludedScopes` (scopes such as `@acme/*` and package names the SDK leaves out), and `environments`: each environment's `isProduction`, decided by its name (`production`, `prod`, `live`) unless set. `PATCH` takes any of `dependencyScanningEnabled`, `excludedScopes` and `environments` (`[{ "sid": "...", "isProduction": true | false | null }]`, `null` going back to the name). Admins only. `API_TRAFFIC_DEPENDENCY_INVENTORY=false` switches reporting off for one process.

## Segments

The findings list, the Overview, the score and the posture checklist take `segmentSid` in place of `bucketSid`: they then cover the buckets of that [segment](https://www.apitraffic.io/docs/api-reference/segments) as it is when they run, members its label rule adds included. The Security Center reads whole buckets, so a bucket the segment narrows to some of its services counts in full. Each answer adds `scope`: the `segment` (`sid`, `name`), the `buckets` read, `outsideAccess` (how many of its buckets the credential cannot read, left out) and `narrowed` (how many it narrows to services). The score has no change against 7 and 30 days ago on a segment, and the daily history is kept per bucket and for the account, so `score/history` answers no days for one. Segments are not offered on the sample data.
