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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ The compose file runs only the Search API with `NODE_ENV=production` and `NODE_C

Use Node.js 24 for the repository's current runtime and development tooling. CI and the Docker dependency-build stage use Node.js 24.14.1; the final runtime image uses Distroless Node.js 24.

**Upstream compatibility caveat:** The locked `cve-core` package still declares Node.js `>= 16.20.2 < 22`. That metadata conflicts with this repository's Node.js 24 baseline and can cause installation warnings or failures when strict engine checks are enabled. Resolving the declaration requires coordination with the `cve-core` maintainers; it is not a recommendation to use Node.js 16 or 18 with this repository's current development tooling.
The API depends directly on the official OpenSearch JavaScript client. `utils/searchProvider.js` owns its configuration and reuses one client per process for both endpoints and readiness checks. Query construction remains separate: `utils/searchQueryBuilder.js` implements `/search`, and `utils/webSearchQueryBuilder.js` implements `/webSearch`.

`utils/appConfig.js` loads `.env` before reading `config/` through node-config. Existing process environment variables take precedence over `.env`; the mappings in `config/custom-environment-variables.jsonc` override deployment-file values. `NODE_CONFIG_ENV` selects a deployment configuration when set, otherwise node-config uses `NODE_ENV`. No credentials or index configuration need to change for this dependency migration.

1. Set `OpenSearchDomainEndpoint` in `.env` to an existing OpenSearch deployment:

Expand Down Expand Up @@ -111,7 +113,7 @@ The Search API uses `PORT`, defaulting to `http://localhost:3000`. The tracked P

`GET /api/health-check` is a process liveness check. It returns HTTP 200 when the Node.js process is running and deliberately does not contact OpenSearch. Use it where a dependency outage should not restart otherwise healthy API tasks.

`GET /api/ready-check` first verifies that the configured CVE index exposes compatible field capabilities for each logical `/search` feature, then sends a minimal zero-result search. CNA, ADP, legacy, and CVSS-version fields that implement the same feature are treated as alternatives, while mapped fields must still use compatible searchable types. This checks mappings, connectivity, index availability, and the same search permission used by the API without retrieving CVE records or counting all matches. It returns HTTP 200 when these checks pass; it does not prove data completeness or capacity for every search. Configuration errors, connection failures, timeouts, incompatible `cve-core` SearchReader contracts, missing logical capabilities, incompatible mapped fields, and a missing index return a sanitized HTTP 503 `SEARCH_NOT_READY` response with `Retry-After: 5`. `SearchReadinessTimeoutMs` defaults to five seconds for the overall readiness check, covering both field-capability and search operations. Each individual operation also uses that value as its transport timeout, and the overall deadline can abort it earlier.
`GET /api/ready-check` first verifies that the configured CVE index exposes compatible field capabilities for each logical `/search` feature, then sends a minimal zero-result search. CNA, ADP, legacy, and CVSS-version fields that implement the same feature are treated as alternatives, while mapped fields must still use compatible searchable types. This checks mappings, connectivity, index availability, and the same search permission used by the API without retrieving CVE records or counting all matches. It returns HTTP 200 when these checks pass; it does not prove data completeness or capacity for every search. Configuration errors, connection failures, timeouts, missing logical capabilities, incompatible mapped fields, and a missing index return a sanitized HTTP 503 `SEARCH_NOT_READY` response with `Retry-After: 5`. `SearchReadinessTimeoutMs` defaults to five seconds for the overall readiness check, covering both field-capability and search operations. Each individual operation also uses that value as its transport timeout, and the overall deadline can abort it earlier.

An AWS load balancer can use `/api/ready-check` to stop routing search traffic to an unready task while the container orchestrator continues using `/api/health-check` for process liveness. Coordinate that probe selection with the infrastructure team before changing deployed health-check behavior.

Expand Down Expand Up @@ -294,7 +296,11 @@ While enabled, `POST /webSearch` requires a non-empty JSON `query` string. It al
- `from` must be an integer greater than or equal to `0`
- `size` must be an integer from `1` to `500`

The controller validates successful `BasicSearchManager` responses before returning HTTP 200. A response without the expected status metadata, total-hit count, or hit array is treated as an invalid upstream response and returns a sanitized HTTP 502.
The raw-query builder preserves the original expression and uses `query_string` with an `AND` default operator. Standalone calendar dates such as `2024-01-01` and ranges such as `2024-01-01..2024-01-31` search the supported record/container date fields using inclusive boundaries. Range endpoints can also use year or month precision (`2023..2024`, `2024-01..2024-03`); a standalone year or month remains a text query. Timestamps accept UTC or explicit offsets, and timestamps without a zone are interpreted as UTC. Date-query years are limited to 1900 through 2100. Quoted dates remain literal query expressions. This date syntax is specific to `/webSearch`; `/search` retains its explicit timestamp filters.

Both endpoints use the shared OpenSearch client and reject incomplete upstream responses. A response with failed shards, a timeout, missing hit data, or an invalid total-hit count cannot produce a successful `/webSearch` response. Backend failures return sanitized HTTP 502 responses; transport timeouts return HTTP 504.

The `/webSearch` response retains `searchMetadata`, `resultsTotal`, and `data`. Metadata includes `searchStatus`, the response-format `version`, `notes`, `errors`, and `queryTime_ms` when available. The obsolete `cveCore` and `cveMitreCore` package-version fields have been removed. `/search` keeps its existing API-owned response format, warnings, and filter behavior.

## Swagger API docs

Expand Down
17 changes: 15 additions & 2 deletions api-docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@
"CVE Record"
],
"summary": "Raw query search",
"description": " <h2>Expected Behavior</h2> <p>Retrieves matching CVE records using the raw query expression supplied in the JSON request body. Service operators can disable this endpoint through runtime configuration.</p>",
"description": " <h2>Expected Behavior</h2> <p>Retrieves matching CVE records using the raw query expression supplied in the JSON request body. Service operators can disable this endpoint through runtime configuration. Standalone dates and date ranges search the supported record/container date fields; other expressions use raw query syntax with an AND default operator.</p>",
"operationId": "webSearch",
"responses": {
"200": {
Expand Down Expand Up @@ -380,6 +380,7 @@
"properties": {
"query": {
"type": "string",
"description": "Raw search expression. Examples: remote code execution, apache*, 2024-01-01, or 2024-01-01..2024-01-31. Date ranges are inclusive and allow year/month endpoints; standalone years/months and quoted dates remain text queries. Date-query years must be between 1900 and 2100.",
"minLength": 1
},
"sort": {
Expand Down Expand Up @@ -598,6 +599,7 @@
"searchMetadata": {
"type": "object",
"additionalProperties": true,
"description": "API-owned raw-query metadata. Includes the response-format version, not dependency package versions.",
"required": [
"searchStatus",
"version",
Expand All @@ -608,7 +610,18 @@
"type": "string"
},
"version": {
"type": "string"
"type": "string",
"description": "Version of the raw-query response format."
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
},
"queryTime_ms": {
"type": "number",
"description": "Query processing duration when available."
},
"errors": {
"type": "array",
Expand Down
15 changes: 2 additions & 13 deletions config/custom-environment-variables.jsonc
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{
// this file is used by node-config to map a node-config (AppConfig)
// hierarchy of constants to an environment variable
// Note that much of the environment variables mapped here existed for some time
// without AppConfig, this file bridges the historical uses of those with the new
// as we transition to AppConfig
// Map appConfig settings to environment variables. utils/appConfig.js loads .env
// before node-config resolves these overrides; existing process variables take precedence.
"appConfig": {
// constants for search capability
"search": {
Expand All @@ -21,14 +18,6 @@
},
"features": {
"webSearchEnabled": "WebSearchEnabled"
},
// constants for testing node-config
// these values are only used to test node-config in AppConfig.test.int.ts
// DO NOT USE THIS FOR ANYTHING ELSE
"test": {
"appConfigTest": {
"test": "JEST_env_config_test"
}
}
}
}
4 changes: 2 additions & 2 deletions config/default.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// These values are overridable using other *.jsonc (e.g., prod.jsonc)
// as well as using environment variables (e.g., in `.env`)
// - Each configuration is mapped using custom-environment-variables.jsonc to enable environment variable overrides.
// - For more information, see cve-core/src/adapters/config/AppConfig.ts
// NOTE for consistency, all values need to be strings for proper type when using .env overrides
// - utils/appConfig.js loads .env before reading these files through node-config.
// Values are normalized by their consumers because environment overrides are strings.
"appConfig": {
// constants for search capability
"search": {
Expand Down
26 changes: 1 addition & 25 deletions config/devel.jsonc
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
// development configurations
// overrides values specified in default.jsonc, read additional comments there
// and in cve-core/src/adapters/config/AppConfig.ts
// NOTE for consistency, all values need to be strings for proper type when using .env overrides
// and in utils/appConfig.js
"appConfig": {
// constants for search capability
"search": {
Expand All @@ -14,29 +13,6 @@
// TRUE disables verification for isolated local development with self-signed certificates.
// DO NOT USE THIS IN ANY PUBLIC OR PRODUCTION ENVIRONMENTS
"allowUnknownSslCerts": "TRUE"
},
// constants for unit, int, e2e testing
"test": {
// constants for testing search capability
"searchTest": {
// many tests for search uses snapshots, which requires CVEs to remain unchanged
// since the live server is updated all the time, a fixture containing fixed CVEs
// is required to keep the test consistent. "fixtures" provides the link
// to the cve-fixtures repository
"fixtures": {
// @todo these constants needs to be in sync in cve-fixtures
// so that testing snapshots are consistent and valid
"name": "fixtures-search-baseline-1086", // release tag
"numCves": "1086" // possible identifier assuming we always add cves to a new release
}
},
// constants for testing node-config
"appConfigTest": {
// these values are only used to test node-config in AppConfig.test.int.ts
// DO NOT USE THIS FOR ANYTHING ELSE
"two": "2",
"five": "5"
}
}
}
}
3 changes: 1 addition & 2 deletions config/prod.jsonc
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
// production (example) constants
// overrides values specified in default.jsonc, read additional comments there
// and in cve-core/src/adapters/config/AppConfig.ts
// NOTE for consistency, all values need to be strings for proper type when using .env overrides
// and in utils/appConfig.js
"appConfig": {
"search": {
"providerEndpoint": "", // preference is to specify this in a (secret) environment variable on production platforms
Expand Down
56 changes: 12 additions & 44 deletions controllers/searchController.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const PackageInfo = require('cve-core').packageInfo;
const constants = require('../utils/constants');
const errors = require('../utils/errors');
const searchDeadline = require('../utils/searchDeadline');
const searchProvider = require('../utils/searchProvider');
const searchQueryBuilder = require('../utils/searchQueryBuilder');
const searchPostFilters = require('../utils/searchPostFilters');
const searchResponseBuilder = require('../utils/searchResponseBuilder');
const webSearchQueryBuilder = require('../utils/webSearchQueryBuilder');
const error = new errors.errors();
const openSearchUnavailableErrorNames = new Set(['ConnectionError', 'NoLivingConnectionsError']);
const openSearchUnavailableStatusCodes = new Set([502, 503, 504]);
Expand All @@ -23,7 +23,6 @@ function getOpenSearchErrorStatusCode(err) {

function isOpenSearchUnavailableError(err) {
return (
searchProvider.isSearchProviderContractError?.(err) === true ||
openSearchUnavailableErrorNames.has(err?.name) ||
openSearchUnavailableStatusCodes.has(getOpenSearchErrorStatusCode(err))
);
Expand Down Expand Up @@ -55,28 +54,6 @@ function buildWebSearchSort(sort) {
return undefined;
}

function getWebSearchSuccessData(searchResponse) {
if (typeof searchResponse?.isOk !== 'function' || searchResponse.isOk() !== true) return null;

const hits = searchResponse?.data?.hits;
const totalValue = hits?.total?.value;
const notes = searchResponse.notes === undefined ? [] : searchResponse.notes;
const responseErrors = searchResponse.errors === undefined ? [] : searchResponse.errors;
const hasValidMetadata =
typeof searchResponse.status === 'string' && Array.isArray(notes) && Array.isArray(responseErrors);

if (!hasValidMetadata || !Array.isArray(hits?.hits) || !Number.isSafeInteger(totalValue) || totalValue < 0) {
return null;
}

return {
hits: hits.hits,
totalValue,
notes,
errors: responseErrors
};
}

function getHitCveId(hit) {
return hit?._source?.cveMetadata?.cveId ?? hit?._id;
}
Expand Down Expand Up @@ -221,13 +198,10 @@ async function webSearchHandler(req, res, next) {
const sort = buildWebSearchSort(req.body.sort);

// initialize response body to error condition
let status;
let body = {
const body = {
searchMetadata: {
searchStatus: 'errors',
version: kLambdaTimeVersion,
cveCore: PackageInfo['version'],
cveMitreCore: PackageInfo['version'],
notes: [],
errors: []
},
Expand All @@ -236,38 +210,32 @@ async function webSearchHandler(req, res, next) {
};

try {
const searchManager = searchProvider.getBasicSearchManager();
const _startTime = Date.now();

const searchResponse = await searchManager.search(`${userProvidedQuery}`, {
const request = webSearchQueryBuilder.buildWebSearchRequest(userProvidedQuery, {
sort,
from: req.body.from,
size: req.body.size
});
const searchData = await searchProvider.searchOpenSearch(request.body);
const queryTime = Date.now() - _startTime;
body.searchMetadata.queryTime_ms = queryTime;

const successData = getWebSearchSuccessData(searchResponse);
if (successData) {
status = 200;
body.searchMetadata.searchStatus = searchResponse.status;
body.searchMetadata.notes = successData.notes;
body.searchMetadata.errors = successData.errors;
body.resultsTotal = successData.totalValue;
body.data = successData.hits;
} else {
status = 502;
body.searchMetadata.errors = [error.upstreamSearchError()];
const total = searchData?.hits?.total?.value;
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(searchData?.hits?.hits)) {
throw new Error('Invalid web search results.');
}
body.searchMetadata.searchStatus = 'ok';
body.searchMetadata.notes = request.notes;
body.resultsTotal = total;
body.data = searchData.hits.hits;
return res.status(200).json(body);
} catch (ex) {
const timedOut = searchDeadline.isSearchTimeoutError(ex);
const err = timedOut ? error.searchTimeoutError() : error.upstreamSearchError();
body.searchMetadata.errors = [err];

return res.status(timedOut ? 504 : 502).json(body);
}

return res.status(status).json(body);
}

module.exports = {
Expand Down
Loading
Loading