@absolutejs/vulnerabilities-postgres 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +184 -2
- package/dist/manifest.js +4 -4
- package/dist/manifest.json +4 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -14,10 +14,11 @@ const osvSnapshots = persistence.snapshots();
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The package stores complete provider snapshots and records, appendable sync
|
|
17
|
-
history, tenant-scoped managed findings,
|
|
18
|
-
leases. Snapshot replacement is
|
|
19
|
-
|
|
20
|
-
disabled when application migrations
|
|
17
|
+
history, tenant-scoped managed findings, correlation observations, risk
|
|
18
|
+
assessments, and expiring distributed refresh leases. Snapshot replacement is
|
|
19
|
+
one Postgres statement, so readers never see a half-replaced record set. Schema
|
|
20
|
+
creation is lazy and idempotent, or can be disabled when application migrations
|
|
21
|
+
own the tables.
|
|
21
22
|
|
|
22
23
|
The SQL surface is compatible with postgres.js and Neon-style tagged-template
|
|
23
24
|
clients. Table prefixes are strictly validated before identifiers are included
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FeedSnapshotStore, type FeedSyncRunStore, type ManagedFindingStore } from "@absolutejs/vulnerabilities";
|
|
1
|
+
import { type FeedSnapshotStore, type FeedSyncRunStore, type ManagedFindingStore, type VulnerabilityObservationStore, type VulnerabilityRiskAssessmentStore } from "@absolutejs/vulnerabilities";
|
|
2
2
|
export type PostgresTag = {
|
|
3
3
|
(strings: TemplateStringsArray, ...values: never[]): PromiseLike<unknown[]>;
|
|
4
4
|
unsafe: (sql: string, ...args: never[]) => PromiseLike<unknown[]>;
|
|
@@ -17,6 +17,8 @@ export type PostgresVulnerabilityStore = {
|
|
|
17
17
|
ensureSchema: () => Promise<void>;
|
|
18
18
|
findings: ManagedFindingStore;
|
|
19
19
|
leases: FeedLeaseStore;
|
|
20
|
+
observations: VulnerabilityObservationStore;
|
|
21
|
+
riskAssessments: VulnerabilityRiskAssessmentStore;
|
|
20
22
|
snapshots: <T>() => FeedSnapshotStore<T>;
|
|
21
23
|
syncRuns: FeedSyncRunStore;
|
|
22
24
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import {
|
|
4
|
-
ManagedVulnerabilityFindingSchema
|
|
4
|
+
ManagedVulnerabilityFindingSchema,
|
|
5
|
+
VulnerabilityObservationSchema,
|
|
6
|
+
VulnerabilityRiskAssessmentSchema
|
|
5
7
|
} from "@absolutejs/vulnerabilities";
|
|
6
8
|
import { Value } from "@sinclair/typebox/value";
|
|
7
9
|
var IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
@@ -56,6 +58,8 @@ var normalizeCursor = (value) => {
|
|
|
56
58
|
var prefixTables = (prefix) => ({
|
|
57
59
|
findings: `${prefix}_findings`,
|
|
58
60
|
leases: `${prefix}_feed_leases`,
|
|
61
|
+
observations: `${prefix}_observations`,
|
|
62
|
+
riskAssessments: `${prefix}_risk_assessments`,
|
|
59
63
|
records: `${prefix}_feed_records`,
|
|
60
64
|
runs: `${prefix}_feed_sync_runs`,
|
|
61
65
|
snapshots: `${prefix}_feed_snapshots`
|
|
@@ -116,6 +120,35 @@ var vulnerabilityPostgresSchemaSql = (tablePrefix = "vulnerability") => {
|
|
|
116
120
|
ON ${table.findings} (tenant_id, severity, last_seen_at DESC);
|
|
117
121
|
CREATE INDEX IF NOT EXISTS ${table.findings}_tenant_asset_idx
|
|
118
122
|
ON ${table.findings} (tenant_id, asset_id, last_seen_at DESC);
|
|
123
|
+
CREATE TABLE IF NOT EXISTS ${table.observations} (
|
|
124
|
+
tenant_id text NOT NULL,
|
|
125
|
+
observation_id text NOT NULL,
|
|
126
|
+
asset_id text NOT NULL,
|
|
127
|
+
component_id text NOT NULL,
|
|
128
|
+
observed_at timestamptz NOT NULL,
|
|
129
|
+
value jsonb NOT NULL,
|
|
130
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
131
|
+
PRIMARY KEY (tenant_id, observation_id)
|
|
132
|
+
);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS ${table.observations}_tenant_asset_idx
|
|
134
|
+
ON ${table.observations} (tenant_id, asset_id, observed_at DESC);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS ${table.observations}_tenant_component_idx
|
|
136
|
+
ON ${table.observations} (tenant_id, component_id, observed_at DESC);
|
|
137
|
+
CREATE TABLE IF NOT EXISTS ${table.riskAssessments} (
|
|
138
|
+
tenant_id text NOT NULL,
|
|
139
|
+
finding_id text NOT NULL,
|
|
140
|
+
priority text NOT NULL,
|
|
141
|
+
assessed_at timestamptz NOT NULL,
|
|
142
|
+
remediate_by timestamptz,
|
|
143
|
+
value jsonb NOT NULL,
|
|
144
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
145
|
+
PRIMARY KEY (tenant_id, finding_id)
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS ${table.riskAssessments}_tenant_priority_idx
|
|
148
|
+
ON ${table.riskAssessments} (tenant_id, priority, assessed_at DESC);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS ${table.riskAssessments}_tenant_due_idx
|
|
150
|
+
ON ${table.riskAssessments} (tenant_id, remediate_by)
|
|
151
|
+
WHERE remediate_by IS NOT NULL;
|
|
119
152
|
CREATE TABLE IF NOT EXISTS ${table.leases} (
|
|
120
153
|
feed_id text PRIMARY KEY,
|
|
121
154
|
owner_id text NOT NULL,
|
|
@@ -378,6 +411,147 @@ var createPostgresVulnerabilityStore = (options) => {
|
|
|
378
411
|
save: async (finding) => saveFindings([finding]),
|
|
379
412
|
saveMany: saveFindings
|
|
380
413
|
};
|
|
414
|
+
const parseObservation = (value) => {
|
|
415
|
+
const observation = json(value, "Vulnerability observation");
|
|
416
|
+
if (!Value.Check(VulnerabilityObservationSchema, observation))
|
|
417
|
+
throw new Error("Stored vulnerability observation is invalid");
|
|
418
|
+
return observation;
|
|
419
|
+
};
|
|
420
|
+
const saveObservations = async (tenantId, observations2) => {
|
|
421
|
+
await ensureSchema();
|
|
422
|
+
const normalizedTenantId = requiredText(tenantId, "Tenant id");
|
|
423
|
+
if (observations2.length === 0)
|
|
424
|
+
return;
|
|
425
|
+
for (const observation of observations2) {
|
|
426
|
+
const observationId = observation.id;
|
|
427
|
+
if (!Value.Check(VulnerabilityObservationSchema, observation))
|
|
428
|
+
throw new Error(`Vulnerability observation ${observationId} is invalid`);
|
|
429
|
+
}
|
|
430
|
+
const payload = JSON.stringify(observations2.map((observation) => ({
|
|
431
|
+
asset_id: observation.assetId,
|
|
432
|
+
component_id: observation.componentId,
|
|
433
|
+
observation_id: observation.id,
|
|
434
|
+
observed_at: observation.observedAt,
|
|
435
|
+
value: observation
|
|
436
|
+
})));
|
|
437
|
+
await sql`
|
|
438
|
+
INSERT INTO ${sql.unsafe(table.observations)} (
|
|
439
|
+
tenant_id, observation_id, asset_id, component_id, observed_at, value,
|
|
440
|
+
updated_at
|
|
441
|
+
)
|
|
442
|
+
SELECT
|
|
443
|
+
${normalizedTenantId}, item.observation_id, item.asset_id,
|
|
444
|
+
item.component_id, item.observed_at::timestamptz, item.value, now()
|
|
445
|
+
FROM jsonb_to_recordset(${payload}::jsonb) AS item(
|
|
446
|
+
observation_id text, asset_id text, component_id text,
|
|
447
|
+
observed_at text, value jsonb
|
|
448
|
+
)
|
|
449
|
+
ON CONFLICT (tenant_id, observation_id) DO UPDATE SET
|
|
450
|
+
asset_id = EXCLUDED.asset_id,
|
|
451
|
+
component_id = EXCLUDED.component_id,
|
|
452
|
+
observed_at = EXCLUDED.observed_at,
|
|
453
|
+
value = EXCLUDED.value,
|
|
454
|
+
updated_at = now()
|
|
455
|
+
`;
|
|
456
|
+
};
|
|
457
|
+
const observations = {
|
|
458
|
+
get: async (tenantId, observationId) => {
|
|
459
|
+
await ensureSchema();
|
|
460
|
+
const rows = await sql`
|
|
461
|
+
SELECT value FROM ${sql.unsafe(table.observations)}
|
|
462
|
+
WHERE tenant_id = ${requiredText(tenantId, "Tenant id")}
|
|
463
|
+
AND observation_id = ${requiredText(observationId, "Observation id")}
|
|
464
|
+
`;
|
|
465
|
+
return rows[0] ? parseObservation(rows[0].value) : null;
|
|
466
|
+
},
|
|
467
|
+
list: async (filter) => {
|
|
468
|
+
await ensureSchema();
|
|
469
|
+
const tenantId = requiredText(filter.tenantId, "Tenant id");
|
|
470
|
+
const assetId = filter.assetId?.trim() || null;
|
|
471
|
+
const componentId = filter.componentId?.trim() || null;
|
|
472
|
+
const rows = await sql`
|
|
473
|
+
SELECT value FROM ${sql.unsafe(table.observations)}
|
|
474
|
+
WHERE tenant_id = ${tenantId}
|
|
475
|
+
AND (${assetId}::text IS NULL OR asset_id = ${assetId})
|
|
476
|
+
AND (${componentId}::text IS NULL OR component_id = ${componentId})
|
|
477
|
+
ORDER BY observed_at DESC, observation_id
|
|
478
|
+
LIMIT ${limit(filter.limit)}
|
|
479
|
+
`;
|
|
480
|
+
return rows.map(({ value }) => parseObservation(value));
|
|
481
|
+
},
|
|
482
|
+
save: async (tenantId, observation) => saveObservations(tenantId, [observation]),
|
|
483
|
+
saveMany: saveObservations
|
|
484
|
+
};
|
|
485
|
+
const parseRiskAssessment = (value) => {
|
|
486
|
+
const assessment = json(value, "Vulnerability risk assessment");
|
|
487
|
+
if (!Value.Check(VulnerabilityRiskAssessmentSchema, assessment))
|
|
488
|
+
throw new Error("Stored vulnerability risk assessment is invalid");
|
|
489
|
+
return assessment;
|
|
490
|
+
};
|
|
491
|
+
const saveRiskAssessments = async (tenantId, assessments) => {
|
|
492
|
+
await ensureSchema();
|
|
493
|
+
const normalizedTenantId = requiredText(tenantId, "Tenant id");
|
|
494
|
+
if (assessments.length === 0)
|
|
495
|
+
return;
|
|
496
|
+
for (const assessment of assessments) {
|
|
497
|
+
const findingId = assessment.findingId;
|
|
498
|
+
if (!Value.Check(VulnerabilityRiskAssessmentSchema, assessment))
|
|
499
|
+
throw new Error(`Vulnerability risk assessment ${findingId} is invalid`);
|
|
500
|
+
}
|
|
501
|
+
const payload = JSON.stringify(assessments.map((assessment) => ({
|
|
502
|
+
assessed_at: assessment.assessedAt,
|
|
503
|
+
finding_id: assessment.findingId,
|
|
504
|
+
priority: assessment.priority,
|
|
505
|
+
remediate_by: assessment.remediateBy,
|
|
506
|
+
value: assessment
|
|
507
|
+
})));
|
|
508
|
+
await sql`
|
|
509
|
+
INSERT INTO ${sql.unsafe(table.riskAssessments)} (
|
|
510
|
+
tenant_id, finding_id, priority, assessed_at, remediate_by, value,
|
|
511
|
+
updated_at
|
|
512
|
+
)
|
|
513
|
+
SELECT
|
|
514
|
+
${normalizedTenantId}, item.finding_id, item.priority,
|
|
515
|
+
item.assessed_at::timestamptz, item.remediate_by::timestamptz,
|
|
516
|
+
item.value, now()
|
|
517
|
+
FROM jsonb_to_recordset(${payload}::jsonb) AS item(
|
|
518
|
+
finding_id text, priority text, assessed_at text, remediate_by text,
|
|
519
|
+
value jsonb
|
|
520
|
+
)
|
|
521
|
+
ON CONFLICT (tenant_id, finding_id) DO UPDATE SET
|
|
522
|
+
priority = EXCLUDED.priority,
|
|
523
|
+
assessed_at = EXCLUDED.assessed_at,
|
|
524
|
+
remediate_by = EXCLUDED.remediate_by,
|
|
525
|
+
value = EXCLUDED.value,
|
|
526
|
+
updated_at = now()
|
|
527
|
+
`;
|
|
528
|
+
};
|
|
529
|
+
const riskAssessments = {
|
|
530
|
+
get: async (tenantId, findingId) => {
|
|
531
|
+
await ensureSchema();
|
|
532
|
+
const rows = await sql`
|
|
533
|
+
SELECT value FROM ${sql.unsafe(table.riskAssessments)}
|
|
534
|
+
WHERE tenant_id = ${requiredText(tenantId, "Tenant id")}
|
|
535
|
+
AND finding_id = ${requiredText(findingId, "Finding id")}
|
|
536
|
+
`;
|
|
537
|
+
return rows[0] ? parseRiskAssessment(rows[0].value) : null;
|
|
538
|
+
},
|
|
539
|
+
list: async (filter) => {
|
|
540
|
+
await ensureSchema();
|
|
541
|
+
const tenantId = requiredText(filter.tenantId, "Tenant id");
|
|
542
|
+
const priority = filter.priority ?? null;
|
|
543
|
+
const rows = await sql`
|
|
544
|
+
SELECT value FROM ${sql.unsafe(table.riskAssessments)}
|
|
545
|
+
WHERE tenant_id = ${tenantId}
|
|
546
|
+
AND (${priority}::text IS NULL OR priority = ${priority})
|
|
547
|
+
ORDER BY assessed_at DESC, finding_id
|
|
548
|
+
LIMIT ${limit(filter.limit)}
|
|
549
|
+
`;
|
|
550
|
+
return rows.map(({ value }) => parseRiskAssessment(value));
|
|
551
|
+
},
|
|
552
|
+
save: async (tenantId, assessment) => saveRiskAssessments(tenantId, [assessment]),
|
|
553
|
+
saveMany: saveRiskAssessments
|
|
554
|
+
};
|
|
381
555
|
const leases = {
|
|
382
556
|
acquire: async (request) => {
|
|
383
557
|
await ensureSchema();
|
|
@@ -415,7 +589,15 @@ var createPostgresVulnerabilityStore = (options) => {
|
|
|
415
589
|
return rows.length > 0;
|
|
416
590
|
}
|
|
417
591
|
};
|
|
418
|
-
return {
|
|
592
|
+
return {
|
|
593
|
+
ensureSchema,
|
|
594
|
+
findings,
|
|
595
|
+
leases,
|
|
596
|
+
observations,
|
|
597
|
+
riskAssessments,
|
|
598
|
+
snapshots,
|
|
599
|
+
syncRuns
|
|
600
|
+
};
|
|
419
601
|
};
|
|
420
602
|
export {
|
|
421
603
|
vulnerabilityPostgresSchemaSql,
|
package/dist/manifest.js
CHANGED
|
@@ -9,7 +9,7 @@ var manifest = defineManifest()({
|
|
|
9
9
|
intents: [
|
|
10
10
|
"persist vulnerability intelligence",
|
|
11
11
|
"retain vulnerability sync history",
|
|
12
|
-
"store managed vulnerability findings"
|
|
12
|
+
"store managed vulnerability findings, observations, and risk assessments"
|
|
13
13
|
],
|
|
14
14
|
keywords: ["vulnerabilities", "Postgres", "CVE", "feed-history"],
|
|
15
15
|
protocols: ["PostgreSQL", "AbsoluteJS vulnerability contracts"]
|
|
@@ -17,7 +17,7 @@ var manifest = defineManifest()({
|
|
|
17
17
|
identity: {
|
|
18
18
|
accent: "#336791",
|
|
19
19
|
category: "operations",
|
|
20
|
-
description: "Durable Postgres snapshots,
|
|
20
|
+
description: "Durable Postgres snapshots, sync history, findings, observations, risk assessments, and distributed refresh leases.",
|
|
21
21
|
docsUrl: "https://github.com/absolutejs/vulnerabilities-postgres",
|
|
22
22
|
name: "@absolutejs/vulnerabilities-postgres",
|
|
23
23
|
tagline: "Keep vulnerability intelligence durable and auditable."
|
|
@@ -30,7 +30,7 @@ var manifest = defineManifest()({
|
|
|
30
30
|
requires: {
|
|
31
31
|
env: [
|
|
32
32
|
{
|
|
33
|
-
description: "Postgres connection string for vulnerability intelligence and
|
|
33
|
+
description: "Postgres connection string for vulnerability intelligence, findings, observations, and risk assessments",
|
|
34
34
|
example: "postgres://user:pass@host/db",
|
|
35
35
|
key: "DATABASE_URL",
|
|
36
36
|
secret: true
|
|
@@ -45,7 +45,7 @@ var manifest = defineManifest()({
|
|
|
45
45
|
],
|
|
46
46
|
services: [
|
|
47
47
|
{
|
|
48
|
-
description: "Stores vulnerability intelligence,
|
|
48
|
+
description: "Stores vulnerability intelligence, findings, observations, risk assessments, and leases",
|
|
49
49
|
id: "postgres"
|
|
50
50
|
}
|
|
51
51
|
]
|
package/dist/manifest.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"intents": [
|
|
9
9
|
"persist vulnerability intelligence",
|
|
10
10
|
"retain vulnerability sync history",
|
|
11
|
-
"store managed vulnerability findings"
|
|
11
|
+
"store managed vulnerability findings, observations, and risk assessments"
|
|
12
12
|
],
|
|
13
13
|
"keywords": [
|
|
14
14
|
"vulnerabilities",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"identity": {
|
|
25
25
|
"accent": "#336791",
|
|
26
26
|
"category": "operations",
|
|
27
|
-
"description": "Durable Postgres snapshots,
|
|
27
|
+
"description": "Durable Postgres snapshots, sync history, findings, observations, risk assessments, and distributed refresh leases.",
|
|
28
28
|
"docsUrl": "https://github.com/absolutejs/vulnerabilities-postgres",
|
|
29
29
|
"name": "@absolutejs/vulnerabilities-postgres",
|
|
30
30
|
"tagline": "Keep vulnerability intelligence durable and auditable."
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"requires": {
|
|
38
38
|
"env": [
|
|
39
39
|
{
|
|
40
|
-
"description": "Postgres connection string for vulnerability intelligence and
|
|
40
|
+
"description": "Postgres connection string for vulnerability intelligence, findings, observations, and risk assessments",
|
|
41
41
|
"example": "postgres://user:pass@host/db",
|
|
42
42
|
"key": "DATABASE_URL",
|
|
43
43
|
"secret": true
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"services": [
|
|
54
54
|
{
|
|
55
|
-
"description": "Stores vulnerability intelligence,
|
|
55
|
+
"description": "Stores vulnerability intelligence, findings, observations, risk assessments, and leases",
|
|
56
56
|
"id": "postgres"
|
|
57
57
|
}
|
|
58
58
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/vulnerabilities-postgres",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Durable Postgres
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Durable Postgres feeds, findings, observations, risk assessments, and refresh leases for AbsoluteJS vulnerability management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSL-1.1",
|
|
7
7
|
"author": "Alex Kahn",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
],
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@absolutejs/manifest": "0.3.0",
|
|
56
|
-
"@absolutejs/vulnerabilities": "0.
|
|
56
|
+
"@absolutejs/vulnerabilities": "0.7.0",
|
|
57
57
|
"@sinclair/typebox": "0.34.52"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|