Skip to content
Merged
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
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ Rename `.env.example` to `.env` and replace or check the following values.
- `SearchOpenSearchRequestTimeoutMs` - optional timeout for each OpenSearch request, default `20000`
- `SearchRequestDeadlineMs` - optional overall `/search` processing deadline, default `30000`
- `SearchReadinessTimeoutMs` - optional timeout for the OpenSearch index readiness check, default `5000`
- `SearchLogPerformanceTimings` - optional aggregate `/search` performance telemetry flag, default `false`
- `SearchPerformanceTelemetryIntervalMs` - optional telemetry aggregation interval, default `10000` and minimum `1000`
- `RequestLogSuccessSampleRate` - optional fraction of successful non-health-check access logs to retain, default `0.1`
- `RequestLogHealthChecks` - optional flag to retain successful health-check access logs, default `false`
- `WebSearchEnabled` - optional `/webSearch` availability flag, default `true`
Expand Down Expand Up @@ -152,7 +150,7 @@ curl -X POST "http://localhost:3000/search" \
-d '{"noRejected":true}'
```

When filtering with `cpeName`, provide a CPE 2.3 name with non-wildcard `part`, `vendor`, `product`, and `version` components. Invalid `cpeName` values return HTTP 404. The API compares the provided CPE Name against CPE Match Criteria, including exact match strings and version range match strings when the indexed CVE data includes range fields. Use `virtualMatchString` when you intentionally need to search for partial CPE Match String values without those required components.
When filtering with `cpeName`, provide a CPE 2.3 name beginning with `cpe:2.3` and containing concrete `part`, `vendor`, `product`, and `version` components; `part` must be `a`, `h`, or `o`. Empty components, malformed escaping, and unsupported part values return HTTP 400. The API compares the provided CPE Name against CPE Match Criteria, including exact match strings and version range match strings when the indexed CVE data includes range fields. Use `virtualMatchString` when you intentionally need to search for partial CPE Match String values without those required components.

### CPE Name Search

Expand Down Expand Up @@ -182,7 +180,9 @@ Set `isVulnerable` to `true` with `cpeName` to require the matching CPE Match Cr

### Virtual Match String Search

Use `virtualMatchString` when a user needs CPE Match Criteria compatibility matching instead of concrete asset matching. String literal components are compared without regard to lexical case. Omitted trailing components and `*` components act as broad matches.
Use `virtualMatchString` when a user needs CPE Match Criteria compatibility matching instead of concrete asset matching. It must begin with `cpe:2.3` and include at least a valid `part` component: `a`, `h`, `o`, `*`, or `-`. A bare `cpe:2.3` prefix, empty components, malformed escaping, and unsupported part values return HTTP 400. String literal components are compared without regard to lexical case. Omitted trailing components and `*` components act as broad matches.

Successful searches can include CPE interpretation warnings before the `records` array. `CPE_COMPONENTS_DEFAULTED` identifies trailing `cpeName` components that were omitted and treated as wildcards. `BROAD_VIRTUAL_MATCH_STRING` identifies wildcard or omitted leading `virtualMatchString` components that broadened the search. Invalid CPE strings are rejected rather than converted into warnings.

Partial product example:

Expand Down Expand Up @@ -216,7 +216,7 @@ CPE version comparisons used by both `cpeName` applicability and `virtualMatchSt

Numeric segments used in version range comparisons ignore leading zeros. A stored exclusive upper bound of `10.05.1` therefore includes `10.5.0`, and CVE-2026-33280's exclusive `6.02` upper bound includes `6.1` but excludes `6.2`. This normalization applies only to range comparison. Exact CPE Match Criteria preserve leading zeros, so the CVE-2025-9392 versions `1.0.04.001` and `1.0.4.1` are not equivalent. This comparison rule is not a universal firmware-versioning rule and can differ from a vendor's actual release sequence.

Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. Narrowing the accepted searches should be handled as a policy decision before implementation.
Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring a concrete `part` and `vendor` for normal searches and a concrete `part`, `vendor`, and `product` for version range searches would reduce the most expensive public search shapes. Narrowing the accepted searches beyond the current valid-part requirement should be handled as a policy decision before implementation.

`POST /webSearch` is a supported raw-query endpoint. It is enabled by default and can be disabled without a code change by setting `WebSearchEnabled=false` and restarting the service. When disabled, it returns HTTP `410` with the stable error code `WEB_SEARCH_DISABLED` before validating the request or contacting OpenSearch.

Expand Down Expand Up @@ -278,12 +278,6 @@ Every request receives an `X-Request-ID` response header. A caller-provided `X-R

Access logs omit query strings and request bodies. By default, the API retains 10% of successful non-health-check access logs, suppresses successful `/api/health-check` and `/api/ready-check` access logs, and retains every HTTP 4xx or 5xx access log. Set `RequestLogSuccessSampleRate` from `0` through `1` to change successful-request sampling. Set `RequestLogHealthChecks=true` to retain successful health-check logs.

Set `SearchLogPerformanceTimings=true` during controlled performance diagnostics to emit one structured `search_performance_summary` JSON event per active aggregation interval. The interval defaults to 10 seconds and can be changed with `SearchPerformanceTelemetryIntervalMs`; values below 1 second fall back to the default. Telemetry is disabled by default and does not record request bodies, filter values, credentials, or returned records.

Each event reports process memory and CPU, event-loop delay and utilization, searches started and completed during the interval, searches still active at its end, maximum in-flight searches, and measurements grouped by search type. Starts and completions are separate because a search can cross an interval boundary. `processCpu.percentOfOneCore` measures the process's CPU time relative to one fully occupied core and can exceed 100 when native work uses multiple cores. Direct searches report `standardOpenSearchMs`. Semantic searches report `candidateOpenSearchMs`, `candidateCount`, `postFilterMs`, `matchedCandidateCount`, and `pageFetchOpenSearchMs` when a page fetch is required. The corresponding `standardOpenSearchTookMs`, `candidateOpenSearchTookMs`, and `pageFetchOpenSearchTookMs` measurements contain OpenSearch's server-reported `took` values, allowing them to be compared with API wall-clock durations. Every completed search reports `totalRequestMs` and an outcome such as `success`, `timeout`, `client_disconnected`, `opensearch_unavailable`, or `upstream_error`. Each measurement includes its count, sampled count, average, minimum, p50, p95, p99, and maximum for the interval. Average, minimum, and maximum use every observation; percentile calculation uses a bounded reservoir of at most 2,000 observations per measurement and interval.

Use these fields together when diagnosing load behavior. If an OpenSearch wall-clock measurement and its corresponding `TookMs` measurement rise together, execution or queueing inside OpenSearch is contributing to latency. If wall time rises while `TookMs` remains stable, transport or application-side waiting is contributing. Growth in `postFilterMs` and event-loop delay indicates application-side semantic processing pressure; growth in `activeRequests` with stage timings stable indicates waiting elsewhere in the request lifecycle. Summaries describe one Node process, so aggregate events from every ECS task log stream before comparing `startedRequests` with the external load generator's delivered request count. Correlate each event's timestamps with load-balancer, ECS, and OpenSearch metrics, then disable telemetry after the controlled run.

Each `/search` request owns a lifecycle cancellation signal. When its overall deadline expires, the API aborts the active OpenSearch transport request before returning HTTP 504. When the caller disconnects, the API aborts work that is needed only by that caller and does not attempt to write a response. This prevents timed-out or abandoned standard searches from continuing until the transport timeout.

### Semantic Post-Filtering
Expand All @@ -299,7 +293,7 @@ The reported total and pagination are complete only within that bounded candidat

`/search` orders search results by CVE ID so repeated shallow-page requests use a deterministic sort. The response `pagination.pageSize` preserves the requested `resultsPerPage` value, while `pagination.resultsReturned` reports the number of records actually returned and may be smaller for a partial or out-of-range page. A stable sort does not provide a point-in-time snapshot, so index updates between separate page requests can still shift results until PIT-based pagination is implemented.

The API does not cache, coalesce, queue, or rate-limit searches in process. Each request executes its own required OpenSearch operations. Public rate enforcement, admission control, shared caching, and durable metrics collection must be supplied by the deployment infrastructure. The optional aggregate telemetry described above writes diagnostic events to standard output for collection by the deployment logging system.
The API does not cache, coalesce, queue, rate-limit, or aggregate performance telemetry in process. Each request executes its own required OpenSearch operations. Public rate enforcement, admission control, shared caching, and durable metrics collection must be supplied by the deployment infrastructure.

OpenSearch operations use one attempt and return a sanitized HTTP 504 `SEARCH_TIMEOUT` response when they exceed the 20-second default timeout. The same response is returned when a `/search` request exceeds the 30-second default overall deadline, and the active OpenSearch request is aborted.

Expand Down
34 changes: 22 additions & 12 deletions api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,6 @@
}
}
},
"404": {
"description": "Invalid cpeName value",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorResponse"
}
}
}
},
"415": {
"description": "A non-empty request body did not use the application/json media type with UTF-8 encoding",
"content": {
Expand Down Expand Up @@ -423,6 +413,26 @@
"items": {
"type": "string"
}
},
"components": {
"type": "array",
"description": "CPE component names associated with this warning, when applicable.",
"items": {
"type": "string",
"enum": [
"part",
"vendor",
"product",
"version",
"update",
"edition",
"language",
"sw_edition",
"target_sw",
"target_hw",
"other"
]
}
}
}
},
Expand Down Expand Up @@ -709,7 +719,7 @@
"type": "string",
"maxLength": 2048,
"example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*",
"description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches."
"description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The value must begin with cpe:2.3 and contain concrete part, vendor, product, and version components; part must be a, h, or o. Empty components, malformed escaping, and unsupported part values return HTTP 400. CPE string literal comparisons are insensitive to lexical case. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The response includes a CPE_COMPONENTS_DEFAULTED warning when omitted trailing components are treated as wildcards. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; numeric range boundaries on wildcard-version criteria do not exclude it. Use virtualMatchString for partial CPE Match String searches."
},
"isVulnerable": {
"oneOf": [
Expand Down Expand Up @@ -798,7 +808,7 @@
"type": "string",
"maxLength": 2048,
"example": "cpe:2.3:o:microsoft:windows_10_1809",
"description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. CPE string literal comparisons are insensitive to lexical case. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component."
"description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. The value must begin with cpe:2.3 and include a valid part component: a, h, o, *, or -. A bare prefix, empty components, malformed escaping, and unsupported part values return HTTP 400. CPE string literal comparisons are insensitive to lexical case. Omitted trailing components and * components act as broad matches; wildcard or omitted leading components produce a BROAD_VIRTUAL_MATCH_STRING response warning. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component."
},
"ssvcExploitation": {
"type": "string",
Expand Down
10 changes: 8 additions & 2 deletions api-docs/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require('path');
const openapi = require('./openapi.json');
const packageInfo = require('../package.json');
const constants = require('../utils/constants');
const cpe = require('../utils/cpe');
const { generateOpenApi } = require('../routes/swagger');

function jsonRequestBodySchema(path, method) {
Expand Down Expand Up @@ -105,6 +106,8 @@ describe('OpenAPI contract', () => {
expect(schema.properties.isVulnerable.description).toContain('same CPE Match Criteria entry');
expect(schema.properties.isVulnerable.description).toContain('False or omitted');
expect(schema.properties.cpeName.description).toContain('Maven-style ordering');
expect(schema.properties.cpeName.description).toContain('HTTP 400');
expect(schema.properties.cpeName.description).toContain('CPE_COMPONENTS_DEFAULTED');
expect(schema.properties.cpeName.description).toContain(
'Every concrete non-version component must be compatible with the same CPE Match Criteria entry'
);
Expand Down Expand Up @@ -132,6 +135,8 @@ describe('OpenAPI contract', () => {
expect(schema.properties.virtualMatchString.description).toContain('CPE Match Criteria');
expect(schema.properties.virtualMatchString.description).toContain('compatible');
expect(schema.properties.virtualMatchString.description).toContain('insensitive to lexical case');
expect(schema.properties.virtualMatchString.description).toContain('valid part component');
expect(schema.properties.virtualMatchString.description).toContain('BROAD_VIRTUAL_MATCH_STRING');
expect(schema.properties.virtualMatchString.description).toContain('without a version component');
expect(schema.properties.virtualMatchString.example).toBe('cpe:2.3:o:microsoft:windows_10_1809');
expect(Object.keys(mediaType.examples)).toStrictEqual([
Expand Down Expand Up @@ -170,10 +175,10 @@ describe('OpenAPI contract', () => {
expect(cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros');
expect(cpeName.description).toContain('logical value NA');
expect(cpeName.description).toContain('numeric range boundaries');
expect(cpeName.description).toContain('HTTP 404');
expect(cpeName.description).toContain('HTTP 400');
expect(cpeName.description).toContain('virtualMatchString');
expect(cpeName.example).toBe('cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*');
expect(openapi.paths['/search'].post.responses['404'].description).toBe('Invalid cpeName value');
expect(openapi.paths['/search'].post.responses['404']).toBeUndefined();
expect(openapi.paths['/search'].post.responses['429']).toBeUndefined();
});

Expand Down Expand Up @@ -211,6 +216,7 @@ describe('OpenAPI contract', () => {
expect(schemas.SearchWarning.additionalProperties).toBe(false);
expect(schemas.SearchWarning.required).toStrictEqual(['code', 'message']);
expect(schemas.SearchWarning.properties.cveIds.items.type).toBe('string');
expect(schemas.SearchWarning.properties.components.items.enum).toStrictEqual(cpe.CPE_COMPONENT_NAMES);
expect(schemas.SearchPagination.additionalProperties).toBe(false);
expect(schemas.SearchPagination.required).toStrictEqual([
'page',
Expand Down
4 changes: 1 addition & 3 deletions config/custom-environment-variables.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
"allowUnknownSslCerts": "OpenSearchAllowUnknownSslCerts",
"openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs",
"requestDeadlineMs": "SearchRequestDeadlineMs",
"readinessTimeoutMs": "SearchReadinessTimeoutMs",
"performanceTelemetryEnabled": "SearchLogPerformanceTimings",
"performanceTelemetryIntervalMs": "SearchPerformanceTelemetryIntervalMs"
"readinessTimeoutMs": "SearchReadinessTimeoutMs"
},
"requestLogging": {
"successSampleRate": "RequestLogSuccessSampleRate",
Expand Down
6 changes: 1 addition & 5 deletions config/default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
// Limits total processing time for a /search request.
"requestDeadlineMs": "30000",
// Limits the minimal OpenSearch request used by /api/ready-check.
"readinessTimeoutMs": "5000",
// Emits aggregate search-stage timing summaries for controlled diagnostics.
"performanceTelemetryEnabled": "FALSE",
// Controls how often enabled performance summaries are emitted.
"performanceTelemetryIntervalMs": "10000"
"readinessTimeoutMs": "5000"
},
// Access logging settings. Error responses are always logged.
"requestLogging": {
Expand Down
Loading
Loading