# Find vulnerable packages you run

> See which npm packages and Node.js versions your services run have known vulnerabilities, work through them, and follow your security score.

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

The Security Center shows the known vulnerabilities, and the malicious packages, in the packages your services actually run. The SDK reports the npm packages each service has installed and loaded, and the Node.js version of each running instance. They are checked against the GitHub Advisory Database (its reviewed advisories and its malware advisories) and the Node.js CVEs NVD publishes, with CISA's list of vulnerabilities exploited in the wild. There is no repository to connect and no scanner to install.

Every finding says what to do: the version that fixes it, the command to upgrade, and whether your WAF protects you against it in the meantime. A security score sums it up and shows which items cost points.

## Before you begin

- The Security Center is being rolled out account by account. When it is on for your account, **Security Center** is the first item under **Security** in a bucket's left menu, and **Security overview** in the **All buckets** menu opens it across every bucket.
- Your services run an `@apitraffic/utilities` release that reports dependency inventory (it comes with the Express, Fastify and other SDK packages). Older SDKs are listed under [Scan coverage](#scan-coverage) as "SDK too old to report packages".
- Dependency scanning is on by default. See [Settings](#settings) to turn it off or leave private packages out.

## What the SDK reports

A little after your app starts, in the background, the SDK walks the installed packages the way Node.js resolves them (npm, pnpm, Yarn classic and Yarn Plug'n'Play, monorepos included) and reports each package's name and version, whether your app lists it, and whether it has been loaded. Servers running the same release share one package list, which is uploaded once. Each server then reports a short summary once a day, which is also how the Security Center knows a release still runs.

The SDK never reports file contents, paths, scripts or environment variables. The walk never runs on your app's startup path. It works in passes of at most 2 seconds, with a pause between them, so a large app (a monorepo's shared `node_modules`, for example) is still read in full; it stops at 20,000 packages.

In a monorepo, "listed by your app" means listed in the app's own `package.json` (for example `apps/api/package.json`), not the repository root's, or in a workspace package the app lists (a shared library of the repository). Where a name is installed at several versions, only the copy your app resolves to counts as listed.

Your package lists and findings are shown only to your account. ApiTraffic staff see the Security Center across all accounts only as counts (how many instances report a package list, how many customer accounts an advisory affects, listed only once at least three of them have it), never which accounts they are or what any one of them runs.

An app bundled with esbuild, webpack or rollup (common on AWS Lambda) has no installed packages left to read. For those, upload the package list from your CI with the CLI: see [Scan a bundled app](#scan-a-bundled-app).

## Overview

The Overview sums up a bucket, every bucket with **Buckets: All buckets**, or the buckets of a [segment](https://www.apitraffic.io/docs/segments) chosen under **Segments** in the same menu, in the environment picked at the top of the app. A segment is read as a set of whole buckets: one it narrows to some services counts in full.

*The Security Center Overview for the sample bucket.*

![Security Center Overview with the security score and grade, the score breakdown by category, a 90-day score chart, open vulnerabilities by severity, the posture summary and the scan coverage table](https://www.apitraffic.io/docs/images/security-center/overview.png)

- **Security score:** a number from 0 to 100 with a grade, and how it moved since 7 and 30 days ago. See [The security score](#the-security-score).
- **Open vulnerabilities:** active findings by severity, how many are exploited in the wild (KEV), how many are loaded at runtime, and how many your WAF does not protect against (see [WAF coverage](#waf-coverage)). Select a number to open the list it counts.
- **Posture:** the posture checks that need attention, the costliest first. See [Posture](#posture).
- **Scan coverage:** how much of what runs is scanned. See [Scan coverage](#scan-coverage).

## The security score

The score is the sum of five categories, each scored out of its weight, and the breakdown always adds up to the score shown. Select a category's items to see each one and the points it takes off; **Biggest wins** lists the three changes that would raise the score most.

*The score breakdown with the Vulnerabilities items expanded.*

![Score breakdown table with Vulnerabilities expanded to show each Node.js finding and the points it costs, and the biggest wins list](https://www.apitraffic.io/docs/images/security-center/score-breakdown.png)

| Category | Points | What takes points off |
| --- | ---: | --- |
| Vulnerabilities | 40 | Open findings: 10 for a critical one, 5 high, 2 medium, 0.5 low. Doubled when exploited in the wild, halved when the package is not loaded (three quarters when that is unknown), halved outside production, halved when your WAF protects against it in block mode (see [WAF coverage](#waf-coverage); the package is still vulnerable, so it never drops to nothing). An accepted risk costs half; a fix deployed or a resolved finding costs nothing. |
| WAF protection | 25 | 10 for each bucket with traffic and no WAF policy; 3 for each policy in monitor mode for over 14 days; 2 for each policy pinned below the latest managed ruleset; 5 for each policy with managed rules off; 2 for each policy that switches managed rules off, or excludes them, everywhere; 2 when no policy uses IP reputation. |
| SDK and runtime | 10 | 4 when servers run an SDK release with a security advisory (before 2.0.0); 2 when servers a WAF policy covers run an SDK too old for an operator its rules use (the managed rules it runs and its own); 3 for each Node.js release line past its end of life that your instances run. |
| Certificates | 10 | For each certificate of an outbound host: 5 expired, 4 rejected by clients, 3 a broken or weak chain, 2 expiring within 14 days. |
| Account access | 15 | 8 when members have no MFA (and the account does not require it); 5 when tokens an SDK release before 2.0.0 used have not been replaced; 3 when API tokens have gone unused for 90 days. |

A category never goes below 0. Each item is one of the [posture checks](#posture), with what to do about it. The WAF and certificate items of a bucket's or an environment's score are that bucket's or environment's (a certificate added by hand, tied to no bucket, counts in every bucket). A policy for some environments does not cover servers that run with no environment. Tokens belong to the account or to one environment: an environment's score counts its own tokens and the account-wide ones, and every other scope counts them all. MFA counts for every scope. While certificate monitoring is switched off for the platform, Certificates is not assessed; on the sample data, Account access is not assessed (its members and tokens are its own).

**What cannot be seen caps the score.** When fewer than 90% of your running instances report a complete package list, the score is capped at 80; with no package list at all, at 60. The cap shows with the points it takes, and fixing [Scan coverage](#scan-coverage) lifts it.

**Before the first advisory sync, the score is provisional.** Until the GitHub Advisory Database has been read once, the Overview and the Vulnerabilities list say that package vulnerability data has not loaded yet, and that findings for npm packages will appear after the first sync. The score is marked **Provisional**, with no grade: it may leave npm vulnerabilities out until then, so it is not a sign that your services are clean.

The score is stored once a day. The chart shows the last 90 days and marks any day the scoring itself changed: the posture checks started scoring WAF protection, Certificates and Account access (they had kept their full points until then) with scoring version 2, so a drop on that day is the new checks, not your services. Scoring version 3 halves the points of a finding your WAF protects against in block mode, so the score can rise on that day.

**Every bucket's score side by side.** The **Overview** in the **All buckets** menu lists each bucket with its security score and grade next to its traffic, in the environment picked at the top of the app. Select a score to open that bucket's Security Center. A provisional score is shown greyed out, and a bucket whose score could not be read shows a dash.

## Vulnerabilities

The list shows one finding per vulnerable package version and advisory, in each bucket and environment, highest priority first: a malicious package, then exploited in the wild, severity, loaded at runtime, in production, then not protected by your WAF. Filter by status, severity, KEV or [WAF coverage](#waf-coverage).

*Vulnerabilities in the sample bucket, highest priority first.*

![Vulnerabilities list of Node.js runtime findings with package, advisory, severity with KEV badge, loaded, services, fixed version, WAF coverage and status columns](https://www.apitraffic.io/docs/images/security-center/vulnerabilities.png)

A finding's status is one of:

| Status | What it means |
| --- | --- |
| Open | Found, and no one has acted on it. |
| Acknowledged | Someone has seen it. |
| Accepted risk | An admin accepted it, with a reason and an expiry (90 days by default, at most a year). It returns to Open when it expires. |
| Fix deployed | No running instance reports the vulnerable version any more. A person confirms it by resolving it, or reopens it if it still runs somewhere the scan cannot see. |
| Resolved | A person confirmed it, or knows better than the scan (a service being retired). |
| Excluded | Its package is left out of scanning in the settings. It is not resolved: it is left out of the score, the open counts and the default list, and when you let the package in again it goes back to what it was before: Open, Acknowledged, Fix deployed, or Accepted risk with the same reason and expiry (Open if that expiry passed meanwhile). Filter by **Excluded** to see these. |

Nothing is resolved on its own, with one exception: a finding whose advisory is withdrawn, or no longer covers the version that runs. If the advisory covers it again, the finding goes back to the status it had, as an excluded one does. If the vulnerable version comes back after it went (a rollback), the finding reopens with its history kept.

To act on findings, select them and choose **Acknowledge**, **Accept risk** (admins), **Resolve** or **Reopen**. Each finding is checked on its own: one not in a status the action applies to is left as it is.

### A finding

Select a package to see what the vulnerability is, where it runs (services and instances), how it got there, the fixed version and the upgrade command, the advisory's references, and the finding's history: every change, when, why, and whether a person, an API token or the Security Center made it (the activity log names the person).

**How it got there** says whether your app lists the package itself, and shows the shortest ways it is pulled in as a tree, read from what your services run: the package your app lists at the top (such as `express@4.18.2`), each package on the way below it with its installed version, and the vulnerable package at the bottom (such as `qs@6.11.0`), marked with the version that fixes it. Ways that share packages are merged into one branch. When the package is pulled in by something your app does not list itself (in a monorepo, a package the repository root or another app lists, installed where your app finds it), the top of the tree says so. **Possibly** means a package on the way is installed at more than one version, so which one each parent uses cannot be told.

*One finding: the advisory, where it runs, the fix, WAF coverage and its history.*

![Finding detail for a Node.js CVE with Critical and KEV badges, summary, identifiers and CVSS, where it runs, the fix with the Node.js release to upgrade to, WAF coverage reading Not known yet, attack attempts, references and history](https://www.apitraffic.io/docs/images/security-center/finding.png)

Node.js findings are matched against each instance's own Node.js version: a service with 12 instances on one version and 3 on another lists only the instances on the vulnerable one.

### Malicious packages

GitHub also publishes malware advisories: packages, or releases of them, published with malicious code, such as a release pushed from a stolen maintainer account. A package your services run that one of them names is a critical finding marked **Malicious package**, at the top of the list, and it is always alerted (see [Alerts](#alerts)). No WAF rule can help: the code runs inside your app, not in a request. Remove the package or move to a release that is not malicious, then rotate the secrets the servers that ran it can read, since the code could have read them.

## WAF coverage

A finding also says whether your WAF protects you against it until you deploy the fix. ApiTraffic triages vulnerabilities against the managed rules and records, for each CVE, the rules that catch it and the first ruleset revision that has them, or why no WAF rule can help. The finding page reads that record against the WAF policy that runs where the finding runs: the most specific policy for its bucket and environment, as the SDK picks it. Every policy that applies is listed, with the one that runs marked.

| Coverage | What it means | What to do |
| --- | --- | --- |
| Protected | The policy that runs here has every rule that covers it, blocks, and every server runs an SDK with the rules' operators. | Nothing for the WAF. Deploy the fixed version. |
| Detecting only | As protected, but the policy is in monitor mode: it reports these attacks and blocks none. | Switch the policy to block mode. |
| Not protected, fixable | A covering rule does not run: managed rules are off, the policy is pinned to a ruleset older than the rule, its paranoia level is below the rule's, the rule is switched off or excluded everywhere in the policy, its anomaly threshold is above what a match of the rule scores on its own, or some servers run an SDK too old for the rule's operator. | The step is shown for each one: move the pin, raise the paranoia level, switch the rule back on, scope the exclusion, lower the threshold, or upgrade the SDK. |
| Switched off by ApiTraffic | ApiTraffic has a covering rule switched off for every customer, usually while a false positive is fixed. | Nothing in your policy: the rule runs again once it is switched back on, and [Coverage updates](#coverage-updates) says when. |
| No WAF policy | No enabled policy applies to this bucket and environment. | Create a WAF policy for it. |
| No WAF rule can help | Triage found that no rule can stop this one (the exploit is an ordinary request). The reason is shown. | Deploy the fixed version. |
| Not known yet | This vulnerability has not been triaged for WAF coverage yet. Nothing is claimed either way. | Deploy the fixed version; check back later. |

With several rules covering one vulnerability, a policy protects only when every one of them runs, and the revision it is covered from is the first that has them all. An advisory with several CVEs is protected only when each of them is: while one is not known yet, the finding is not known yet.

**Attack attempts** shows how often the rules that cover the finding matched requests to its bucket and environment in the last 7 and 30 days: how many were blocked, and how many a policy in monitor mode would have blocked. It is read from your WAF analytics. A request that two covering rules matched counts once for each. Open WAF insights from there to see the requests. A token with access scopes needs `waf:read` as well as `security:read` to read attack attempts.

## Coverage updates

**Coverage updates** lists the changes to the WAF protection your policies run, newest first:

- **A new ruleset revision**, with how many rules it added or patched, the CVEs it covers, and how many of your policies are pinned below it (they do not get it until you move the pin).
- **A CVE now covered**, from the revision whose rules catch it, with your open findings for it.
- **A managed rule switched off by ApiTraffic, or back on**, when one of your policies runs it, with the CVEs it covers and your findings for them.

*Coverage updates: ruleset releases, CVEs now covered and rules switched off or back on.*

![Coverage updates list with a ruleset release and the rules it added, a CVE covered from ruleset 1.1.2 with a KEV badge, each with its date](https://www.apitraffic.io/docs/images/security-center/coverage-updates.png)

## Tickets

With a [work tracking destination](https://www.apitraffic.io/docs/work-tracking) set up, select **Create ticket** on a finding to raise a Jira issue for it. The ticket carries the package and version, the advisory and CVE ids, the severity and whether it is exploited in the wild, the services and instances that run it, the fixed version and the upgrade step, its WAF coverage, and a link back to the finding. It never carries request data. Set the issue type for security findings (Bug, for example) in the destination's settings.

Raising a ticket acknowledges an open finding, with the ticket in its history. The same vulnerability in another service or bucket (the same package and advisory) is offered the open ticket, so it is added there instead of opening a second one.

## Alerts

The Security Center is an alert source, `security`, routed by your [alert rules and channels](https://www.apitraffic.io/docs/alerts) like any other. It alerts when:

- **A malicious package runs**, in any environment.
- **A new critical vulnerability, or one exploited in the wild, is found in production.** An environment counts as production as set under [Settings](#settings). Each alert carries the labels `environment`, `environment_sid` and `production`, so a rule can send other environments too, or fewer.
- **The account's security score drops by 10 points or more in a day.**

A finding's alert resolves when the finding is resolved. One package and advisory is one alert, however many buckets run it. Only findings first seen in the last two days alert, so switching alerts on does not send the ones you already have.

## Digest

The [API health digest](https://www.apitraffic.io/docs/account/notifications#api-health-digest) has a security line for each scope where the Security Center is on: new critical, exploited and malicious findings, how many were resolved, how many are still open, and how many instances are scanned.

## From an AI assistant

Through the [ApiTraffic MCP server](https://www.apitraffic.io/docs/integrations/mcp-tools), an assistant can list your findings (`list_security_findings`, with filters such as critical only or production only), read one with its fix and WAF coverage (`get_security_finding`), acknowledge it as you (`acknowledge_security_finding`), and open its ticket (`create_work_item` with `subjectType` `security_finding`). It answers questions such as "what critical vulnerabilities do we have in production?". Accepting a risk and resolving stay in the app: they are a person's call.

## Posture

Posture is a checklist of what we already know about how your traffic is protected: your WAF policies, the SDKs and Node.js versions your servers run, the certificates of the hosts they call, and who can get into the account. Each item says what is wrong, why it matters and the one step that fixes it, with the points it costs in [the security score](#the-security-score) and a link to where you fix it. The checks that pass are listed too.

*Posture checks for the sample bucket.*

![Posture page with the WAF protection, SDK and runtime, Certificates and Account access cards, the item to fix with why it matters, the fix and the points it costs, and the passing checks](https://www.apitraffic.io/docs/images/security-center/posture.png)

| Check | Fails when | Fix |
| --- | --- | --- |
| Every bucket with traffic has a WAF policy | A bucket has had traffic in the last 7 days, in an environment no enabled policy (in detect or block mode) applies to. | Create a WAF policy for it: in detect mode first, then block. |
| No policy stays in monitor mode for over 14 days | A policy has been in detect mode for more than 14 days. | Review what it would have blocked in WAF insights, then switch it to block mode. |
| Managed rules are on | A policy has managed rules turned off. | Turn managed rules on in the policy. |
| Policies run the latest managed ruleset | A policy is pinned to an older ruleset revision. | Move the pin to latest. |
| No managed rule is switched off or excluded everywhere | A policy switches rules off, or excludes a rule or category with no target, name or path. | Scope the exclusion to the path, argument or target that caused the false positive. |
| IP reputation is on | No policy uses IP reputation. | Connect AbuseIPDB and turn IP reputation on in a policy. |
| No server runs an SDK release with a security advisory | A server runs an SDK release before 2.0.0 (or the later release the platform requires), which published the API token in realtime presence. | Upgrade, then replace the tokens those servers used. |
| Servers run an SDK with every operator the WAF rules use | A server a policy covers runs an SDK too old for a rule's operator: that rule never matches there. | Upgrade the SDK to the version named. |
| No server runs a Node.js line past its end of life | A server runs a Node.js release line that gets no more security fixes. | Move to a supported line. |
| Certificates | A certificate of an outbound host has expired, expires within 14 days, has a broken chain, or is rejected by clients (the same reading as [certificate alerts](https://www.apitraffic.io/docs/certificates)). | Ask the vendor to renew it, or install the full chain. |
| Every member uses MFA | Members have no MFA, and the account does not require it. | Ask them to turn on MFA in **Settings** → **Sign-in & security** → **Two-factor & passkeys**, or require it for everyone with the MFA policy under **Settings** → **Access**. |
| No API token has gone unused for 90 days | An API token was created and has not been used for 90 days. | Delete the tokens nothing uses. |
| Tokens used by SDK releases with the presence advisory are rotated | A token such an SDK used has not been replaced. | Create a new token, move the SDKs to it, and delete the old one. |

A bucket has traffic when a server with the SDK reported from it in the last 7 days, or requests were counted for it. On **All buckets**, every bucket's items are listed; MFA and the account's tokens are listed in every scope.

## Scan coverage

Scan coverage lists every service that sends the bucket traffic, with its running instances, how many report a complete package list, and for each gap, its fix.

*Scan coverage, service by service.*

![Scan coverage table with service, running instances, scanned instances, and the gap and its fix](https://www.apitraffic.io/docs/images/security-center/scan-coverage.png)

| Gap | Fix |
| --- | --- |
| SDK too old to report packages | Upgrade `@apitraffic/utilities` (and your SDK package) to a release that reports dependency inventory. |
| Not reporting packages | Check that `API_TRAFFIC_DEPENDENCY_INVENTORY` is not set to `false`. A new process reports within a minute of starting. |
| Dependency scanning is off | Turn it on in [Settings](#settings). |
| Bundled app | The SDK cannot read packages from an esbuild, webpack or rollup bundle. Upload them from your CI: see [Scan a bundled app](#scan-a-bundled-app). When the service has uploads but none for the release its servers run, or the servers report no commit, the fix says so: check the commit the app reports. |
| Partial scan | The walk could not finish: it reached 20,000 packages, ran 30 passes, or met a file read that never returned (a stuck disk or network mount). Findings cover what it read. |
| CLI upload out of date | A package scope was let back in under [Settings](#settings) after the bundle's upload, and only an upload can send those packages. Upload again for the release that runs. |

A bundled service scanned from its upload shows **Scanned from the CLI upload**, and counts as complete.

## Scan a bundled app

Once an app is bundled, the SDK can no longer tell which packages went into the bundle. It reports the service as a bundle, with its service name and the commit it was built from, and the Security Center shows it as **Bundled app** under Scan coverage. To scan it, run the CLI in your CI after the build: it reads your lockfile (`package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml` or `yarn.lock`) and uploads the packages the app installs for production, tagged with the same service and commit. The Security Center then reads that list as what the bundle's servers run: findings open for them, Fix deployed works as it does for any service, and the score's coverage cap counts them as scanned. No repository connection is needed: the CLI runs where your code already is.

**Name the service and the commit on the deployed app**

A bundle has no `package.json` for the SDK to name it by, so set these on the deployed app (in the Lambda's environment, the container's, or your platform's settings):

- `API_TRAFFIC_SERVICE_NAME`: the service, for example `checkout-api`.
- `API_TRAFFIC_GIT_SHA`: the commit the bundle was built from. Many platforms set one already: `SOURCE_VERSION`, `VERCEL_GIT_COMMIT_SHA` and `HEROKU_SLUG_COMMIT` are read too.

Without a commit, set `API_TRAFFIC_SERVICE_VERSION` to the release's version instead, and make the upload with the same version. With neither, no upload can be matched to the servers: which build they run cannot be told.

**Upload the package list from CI**

After installing dependencies, with the same token the app uses (its ingest token) and its bucket:

```yaml
# GitHub Actions
- run: npm ci
- run: npm run build
- run: npx @apitraffic/cli inventory push
  env:
    API_TRAFFIC_TOKEN: ${{ secrets.API_TRAFFIC_TOKEN }}
    API_TRAFFIC_BUCKET: k3m9p2x
    API_TRAFFIC_SERVICE_NAME: checkout-api
```

The commit is read from `API_TRAFFIC_GIT_SHA`, then the CI's own variable (`GITHUB_SHA`, `CI_COMMIT_SHA` and others), then `git rev-parse HEAD`. Pass `--git-sha` or `--service` when your pipeline knows better. Use the token of the environment the app is deployed to: an upload belongs to its token's environment, and joins the servers there.

**Check Scan coverage**

Once the bundle's servers report (a minute after they start), the service shows **Scanned from the CLI upload**. If it still shows **Bundled app**, the fix under it says whether nothing was uploaded for the service, the commit the servers report matches no upload, or the servers report no commit at all.

Only the package names and versions, which ones your `package.json` lists, and the names each one depends on are uploaded: never your code, scripts or environment. A lockfile out of date with `package.json` is refused, since a package missing from the upload would read as gone from your servers; so is a token with no environment. Platform-specific binaries are sent for the servers' platform only (this machine's, or `--platform linux-arm64`). devDependencies are left out (the bundler, test tools); pass `--include-dev` to include them, and do pass it when your bundle carries packages you list under devDependencies (a bundler inlines whatever the code imports, whichever section lists it). A build that uploads an unchanged lockfile adds its commit to the list already stored, without sending it again. Whether a package is loaded cannot be known from a lockfile, so findings from an upload show loaded as unknown (three quarters of the points). Monorepos work: run it in the app's folder (or pass `--dir`), and the workspace's lockfile is found above it. When you let a package scope back in under [Settings](#settings), a bundle's findings for it wait for the next upload, which sends the whole list again. See the [CLI reference](https://www.apitraffic.io/docs/reference/cli#inventory-push) for every option.

## Settings

Owners and admins can change these settings; every member can read them.

*Security Center settings.*

![Settings with the Dependency scanning switch, the packages left out and the production environments](https://www.apitraffic.io/docs/images/security-center/settings.png)

- **Dependency scanning:** on by default. Off, SDKs stop reporting within about a minute. To turn it off for one process instead, set `API_TRAFFIC_DEPENDENCY_INVENTORY=false` (`0`, `off` and `no` work too).
- **Packages left out:** scopes such as `@acme/*`, or package names, that the SDK does not report. Public advisories never name private packages, so leaving your own scopes out loses nothing. Names you leave out are removed from what is stored, and their findings are marked Excluded (not resolved). Let a name in again and its findings go back to the status they had before.
- **Production environments:** findings in production rank higher and cost twice the points. An environment named production, prod or live counts as production unless you set it otherwise.

## What is not scanned

The Security Center reads the running Node.js process. It does not scan source code, operating system packages, OpenSSL linked into Node.js, or container images: an image scanner covers those.

See the [Security Center API](https://www.apitraffic.io/docs/api-reference/security-center) to read findings and the score from your own tools.
