Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,20 @@ Use the optional `scope` field to control which parts of each record participate

Date filters use the API's full UTC timestamp format and inclusive boundaries. When both boundaries for publication, modification, SSVC timestamp, or KEV date-added filters are supplied, the start must be before or equal to the end.

CVSS v3.0, v3.1, combined v3, and v4 severity filters accept `NONE`, `LOW`, `MEDIUM`, `HIGH`, and `CRITICAL`, case-insensitively. They match the stored `baseSeverity` label in scope-selected CNA/ADP metrics; the API does not recalculate the label from scores. CVSS v2 instead maps `LOW`, `MEDIUM`, and `HIGH` to base-score ranges and does not accept `NONE` or `CRITICAL`.

CVSS metric filters accept partial vectors, such as `AV:N/AC:L`, with case-insensitive metric names and values from the selected CVSS version, including optional metrics. A `CVSS:2.0`, `CVSS:3.0`, `CVSS:3.1`, or `CVSS:4.0` prefix is optional but must match the requested filter. For `cvssV3Metrics`, omitting the prefix searches both v3.0 and v3.1; providing one restricts matching to that version. Unknown metrics or values, empty segments, prefix-only values, and conflicting repeated metrics return HTTP 400 `CVSS_METRICS_FORMAT`. Repeated identical tokens are accepted. Every supplied token must occur in the same eligible stored vector; filters do not calculate a CVSS score.

`cweIds` accepts one or more comma-separated IDs, such as `"CWE-79, cwe-89"`, following the same input pattern as `cveIds`. Each ID must use `CWE-<positive integer>`, without leading zeros. Values are case-insensitive, whitespace around each ID is trimmed, and duplicate IDs do not change results. A record matches when any requested ID occurs in the scope-selected CNA or ADP containers; the CWE filter is combined with other filters using AND. Matching uses complete identifiers rather than analyzed text. Arrays, bare numbers, empty entries, wildcard patterns, and other malformed IDs return HTTP 400 `CWE_IDS_FORMAT`; any invalid entry rejects the whole request. Limits apply before deduplication: at most 100 IDs, 32 characters per ID, and 4,096 characters overall. The API validates format, not membership in the CWE catalog. Valid IDs without indexed matches contribute no results and do not prevent other IDs from matching. The former `cweId` request field is no longer supported.

Scope applies to filter matching as follows:

| Filters | Data used for matching |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `cveIds`, `pubStartDate`, `pubEndDate`, `lastModStartDate`, `lastModEndDate` | Base record metadata in every scope |
| `noRejected` | Base record state in every scope, plus the legacy CNA state when CNA data is selected |
| `keywordSearch`, `keywordExactMatch` | Description text in the selected CNA and/or ADP containers |
| `cweId`, all CVSS severity and metric filters | Problem types or metrics in the selected CNA and/or ADP containers |
| `cweIds`, all CVSS severity and metric filters | Problem types or metrics in the selected CNA and/or ADP containers |
| All SSVC filters, `knownExploited`, KEV date filters, `referenceURL`, `disputed` | Structured metrics, references, or tags in the selected CNA and/or ADP containers |
| `cpeName`, `isVulnerable`, `virtualMatchString`, and version range filters | CNA CPE applicability data; incompatible with `ADP_ONLY` |
| `resultsPerPage`, `pageNumber` | Response controls; they do not select record data |
Expand All @@ -161,7 +167,7 @@ String inputs are bounded before query construction:
| ---------------------------- | ---------------------------------------------------------------------: |
| `keywordSearch` | 512 characters and 64 whitespace-delimited terms |
| `cveIds` | 4,096 characters overall, 100 IDs, and 64 characters per ID |
| `cweId` | 32 characters |
| `cweIds` | 4,096 characters overall, 100 IDs, and 32 characters per ID |
| `cpeName` | 2,048 characters |
| `virtualMatchString` | 2,048 characters |
| `versionStart`, `versionEnd` | 256 characters each |
Expand Down
32 changes: 18 additions & 14 deletions api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"keywordExactMatch": true,
"cveIds": "CVE-2024-1234,CVE-2024-5678",
"noRejected": true,
"cweId": "CWE-79",
"cweIds": "CWE-79,CWE-89",
"cvssV2Severity": "HIGH",
"cvssV3Severity": "CRITICAL",
"cvssV3_0Severity": "CRITICAL",
Expand Down Expand Up @@ -690,11 +690,11 @@
"default": false,
"description": "When true, excludes CVE records with REJECT, REJECTED, or Rejected status. JSON booleans and the lowercase strings \"true\" and \"false\" are accepted."
},
"cweId": {
"cweIds": {
"type": "string",
"maxLength": 32,
"example": "CWE-79",
"description": "Returns CVE records whose scope-selected containers include the provided Common Weakness Enumeration ID."
"maxLength": 4096,
"example": "CWE-79,CWE-89",
"description": "Returns CVE records whose scope-selected containers include any ID in the comma-separated list of CWE IDs. Each ID must use CWE-<positive integer>, without leading zeros. Input is case-insensitive, surrounding whitespace is trimmed per ID, and duplicate IDs do not change results. Matching uses complete identifiers, with OR between IDs and AND with other filters. Limited to 100 CWE IDs, 32 characters per ID, and 4096 characters overall. Arrays, empty entries, and malformed IDs return HTTP 400 CWE_IDS_FORMAT; one invalid entry rejects the whole request. The API validates format, not membership in the CWE catalog. The former cweId field is unsupported."
},
"cvssV2Severity": {
"type": "string",
Expand All @@ -708,72 +708,76 @@
"cvssV3Severity": {
"type": "string",
"enum": [
"NONE",
"LOW",
"MEDIUM",
"HIGH",
"CRITICAL"
],
"description": "Returns CVE records with the provided qualitative severity in a scope-selected CVSS v3.0 or v3.1 metric. Values are case-insensitive."
"description": "Returns CVE records with the provided stored baseSeverity label in a scope-selected CVSS v3.0 or v3.1 metric. Values are case-insensitive."
},
"cvssV3_0Severity": {
"type": "string",
"enum": [
"NONE",
"LOW",
"MEDIUM",
"HIGH",
"CRITICAL"
],
"description": "Returns CVE records with the provided qualitative severity in a scope-selected CVSS v3.0 metric. Values are case-insensitive."
"description": "Returns CVE records with the provided stored baseSeverity label in a scope-selected CVSS v3.0 metric. Values are case-insensitive."
},
"cvssV3_1Severity": {
"type": "string",
"enum": [
"NONE",
"LOW",
"MEDIUM",
"HIGH",
"CRITICAL"
],
"description": "Returns CVE records with the provided qualitative severity in a scope-selected CVSS v3.1 metric. Values are case-insensitive."
"description": "Returns CVE records with the provided stored baseSeverity label in a scope-selected CVSS v3.1 metric. Values are case-insensitive."
},
"cvssV4Severity": {
"type": "string",
"enum": [
"NONE",
"LOW",
"MEDIUM",
"HIGH",
"CRITICAL"
],
"description": "Returns CVE records with the provided qualitative severity in a scope-selected CVSS v4.0 metric. Values are case-insensitive."
"description": "Returns CVE records with the provided stored baseSeverity label in a scope-selected CVSS v4.0 metric. Values are case-insensitive."
},
"cvssV2Metrics": {
"type": "string",
"maxLength": 256,
"example": "AV:N/AC:L/Au:N/C:P/I:P/A:P",
"description": "Returns CVE records with a scope-selected CVSS v2 vector containing every supplied slash-delimited metric token. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
"description": "Returns CVE records with a scope-selected CVSS v2.0 vector containing every supplied slash-delimited metric token. Partial vectors and optional metrics are accepted. Names and values are case-insensitive and must be valid for the selected CVSS version. An optional CVSS:version prefix must match that version. Unknown tokens, empty segments, prefix-only values, and conflicting repeated metrics return HTTP 400 CVSS_METRICS_FORMAT. Repeated identical tokens are accepted. Every token must match the same eligible vector. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
},
"cvssV3Metrics": {
"type": "string",
"maxLength": 256,
"example": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"description": "Returns CVE records with a scope-selected CVSS v3.0 or v3.1 vector containing every supplied slash-delimited metric token. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
"description": "Returns CVE records with a scope-selected CVSS v3.0 or v3.1 vector containing every supplied slash-delimited metric token. Without a prefix, both versions are searched; CVSS:3.0 or CVSS:3.1 restricts matching to that version. Partial vectors and optional metrics are accepted. Names and values are case-insensitive and must be valid for the selected CVSS version. An optional CVSS:version prefix must match that version. Unknown tokens, empty segments, prefix-only values, and conflicting repeated metrics return HTTP 400 CVSS_METRICS_FORMAT. Repeated identical tokens are accepted. Every token must match the same eligible vector. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
},
"cvssV3_0Metrics": {
"type": "string",
"maxLength": 256,
"example": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"description": "Returns CVE records with a scope-selected CVSS v3.0 vector containing every supplied slash-delimited metric token. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
"description": "Returns CVE records with a scope-selected CVSS v3.0 vector containing every supplied slash-delimited metric token. Partial vectors and optional metrics are accepted. Names and values are case-insensitive and must be valid for the selected CVSS version. An optional CVSS:version prefix must match that version. Unknown tokens, empty segments, prefix-only values, and conflicting repeated metrics return HTTP 400 CVSS_METRICS_FORMAT. Repeated identical tokens are accepted. Every token must match the same eligible vector. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
},
"cvssV3_1Metrics": {
"type": "string",
"maxLength": 256,
"example": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"description": "Returns CVE records with a scope-selected CVSS v3.1 vector containing every supplied slash-delimited metric token. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
"description": "Returns CVE records with a scope-selected CVSS v3.1 vector containing every supplied slash-delimited metric token. Partial vectors and optional metrics are accepted. Names and values are case-insensitive and must be valid for the selected CVSS version. An optional CVSS:version prefix must match that version. Unknown tokens, empty segments, prefix-only values, and conflicting repeated metrics return HTTP 400 CVSS_METRICS_FORMAT. Repeated identical tokens are accepted. Every token must match the same eligible vector. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
},
"cvssV4Metrics": {
"type": "string",
"maxLength": 256,
"example": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
"description": "Returns CVE records with a scope-selected CVSS v4.0 vector containing every supplied slash-delimited metric token. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
"description": "Returns CVE records with a scope-selected CVSS v4.0 vector containing every supplied slash-delimited metric token. Partial vectors and optional metrics are accepted. Names and values are case-insensitive and must be valid for the selected CVSS version. An optional CVSS:version prefix must match that version. Unknown tokens, empty segments, prefix-only values, and conflicting repeated metrics return HTTP 400 CVSS_METRICS_FORMAT. Repeated identical tokens are accepted. Every token must match the same eligible vector. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token."
},
"cpeName": {
"type": "string",
Expand Down
6 changes: 6 additions & 0 deletions api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ describe('OpenAPI contract', () => {
expect(schema.properties.cveIds.description).toContain(`${constants.MAX_CVE_ID_LENGTH} characters per ID`);
expect(schema.properties.cveIds.description).toContain(`before ${constants.FIRST_CVE_RECORD_YEAR}`);
expect(schema.properties.cveIds.description).toContain('non-fatal response warning');
expect(schema.properties.cweIds.type).toBe('string');
expect(schema.properties.cweIds.description).toContain('comma-separated list of CWE IDs');
expect(schema.properties.cweIds.description).toContain(`${constants.MAX_CWE_IDS} CWE IDs`);
expect(schema.properties.cweIds.description).toContain(`${constants.MAX_CWE_ID_LENGTH} characters per ID`);
expect(schema.properties.cweIds.description).toContain('OR between IDs and AND with other filters');
expect(schema.properties.cweId).toBeUndefined();
expect(schema.properties.keywordSearch.description).toContain(
`${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms`
);
Expand Down
30 changes: 23 additions & 7 deletions controllers/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ const constants = require('../utils/constants');
const cpe = require('../utils/cpe');
const searchScope = require('../utils/searchScope');
const ssvc = require('../utils/ssvc');
const cvss = require('../utils/cvss');
const error = new errors.errors();

const cvssV2SeverityEnum = ['LOW', 'MEDIUM', 'HIGH'];

const cvssV3_4SeverityEnum = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
const cvssV3_4SeverityEnum = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];

const severityValidators = [
{
Expand Down Expand Up @@ -43,7 +44,7 @@ const ssvcEnumValidators = [
{ param: 'ssvcTechnicalImpact', allowed: ssvc.TECHNICAL_IMPACT_VALUES }
];

const cvssMetricsParams = ['cvssV2Metrics', 'cvssV3Metrics', 'cvssV3_0Metrics', 'cvssV3_1Metrics', 'cvssV4Metrics'];
const cvssMetricsParams = cvss.PARAMS;

const dateParams = [
'lastModStartDate',
Expand All @@ -67,6 +68,7 @@ const versionRangeTypeParams = ['versionStartType', 'versionEndType'];
const nonStringSearchParams = [
'keywordExactMatch',
'cveIds',
'cweIds',
'isVulnerable',
'noRejected',
'knownExploited',
Expand All @@ -79,6 +81,7 @@ const nonEmptyStringParams = constants.SEARCH_QUERY_PARAMS.filter((param) => !no

const dateTimeFormat = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/;
const cveIdFormat = /^CVE-\d{4}-\d{4,}$/i;
const cweIdFormat = /^CWE-[1-9][0-9]*$/i;

function isTrueParam(value) {
return value === true || String(value).toLowerCase() === 'true';
Expand Down Expand Up @@ -128,13 +131,13 @@ function countWhitespaceDelimitedTokens(value) {
return value.trim().split(/\s+/).length;
}

function parseCveIds(value) {
function parseIds(value, format) {
if (typeof value !== 'string') return null;

const cveIds = value.split(',').map((id) => id.trim());
if (!cveIds.length || cveIds.some((id) => !id || !cveIdFormat.test(id))) return null;
const ids = value.split(',').map((id) => id.trim());
if (!ids.length || ids.some((id) => !id || !format.test(id))) return null;

return cveIds;
return ids;
}

function getRequestedResultsPerPage(params) {
Expand Down Expand Up @@ -228,7 +231,7 @@ function validateParams(req, res, next) {
}

if (hasParam(params, 'cveIds')) {
const cveIds = parseCveIds(params.cveIds);
const cveIds = parseIds(params.cveIds, cveIdFormat);
if (!cveIds) {
return res.status(400).json(error.cveIdsFormatError());
}
Expand All @@ -240,6 +243,15 @@ function validateParams(req, res, next) {
}
}

if (hasParam(params, 'cweIds')) {
const cweIds = parseIds(params.cweIds, cweIdFormat);
if (!cweIds) return res.status(400).json(error.cweIdsFormatError());
if (cweIds.some((id) => id.length > constants.MAX_CWE_ID_LENGTH)) {
return res.status(400).json(error.cweIdLengthError());
}
if (cweIds.length > constants.MAX_CWE_IDS) return res.status(400).json(error.cweIdsLimitError());
}

if (hasParam(params, 'noRejected') && ![true, false, ''].includes(params.noRejected)) {
return res.status(400).json(error.noRejectedValueError());
}
Expand Down Expand Up @@ -368,6 +380,10 @@ function validateParams(req, res, next) {
if (params?.[param] && hasInvalidCvssMetricsShape(params[param])) {
return res.status(400).json(error.cvssMetricsLimitError(param));
}
if (hasParam(params, param)) {
const parsed = cvss.parseMetricFilter(param, params[param]);
if (!parsed.valid) return res.status(400).json(error.cvssMetricsFormatError(param, parsed.reason));
}
}

//Ensures date params are valid timestamps matching yyyy-MM-ddTHH:mm:ss.SSS'Z' and real calendar dates.
Expand Down
Loading
Loading