@absolutejs/vulnerabilities-postgres 0.1.1 → 0.2.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 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, and expiring distributed refresh
18
- leases. Snapshot replacement is one Postgres statement, so readers never see a
19
- half-replaced record set. Schema creation is lazy and idempotent, or can be
20
- disabled when application migrations own the tables.
17
+ history, tenant-scoped managed findings and correlation observations, and
18
+ expiring distributed refresh leases. Snapshot replacement is one Postgres
19
+ statement, so readers never see a half-replaced record set. Schema creation is
20
+ lazy and idempotent, or can be disabled when application migrations own the
21
+ 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 } 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,7 @@ export type PostgresVulnerabilityStore = {
17
17
  ensureSchema: () => Promise<void>;
18
18
  findings: ManagedFindingStore;
19
19
  leases: FeedLeaseStore;
20
+ observations: VulnerabilityObservationStore;
20
21
  snapshots: <T>() => FeedSnapshotStore<T>;
21
22
  syncRuns: FeedSyncRunStore;
22
23
  };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // @bun
2
2
  // src/index.ts
3
3
  import {
4
- ManagedVulnerabilityFindingSchema
4
+ ManagedVulnerabilityFindingSchema,
5
+ VulnerabilityObservationSchema
5
6
  } from "@absolutejs/vulnerabilities";
6
7
  import { Value } from "@sinclair/typebox/value";
7
8
  var IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
@@ -56,6 +57,7 @@ var normalizeCursor = (value) => {
56
57
  var prefixTables = (prefix) => ({
57
58
  findings: `${prefix}_findings`,
58
59
  leases: `${prefix}_feed_leases`,
60
+ observations: `${prefix}_observations`,
59
61
  records: `${prefix}_feed_records`,
60
62
  runs: `${prefix}_feed_sync_runs`,
61
63
  snapshots: `${prefix}_feed_snapshots`
@@ -116,6 +118,20 @@ var vulnerabilityPostgresSchemaSql = (tablePrefix = "vulnerability") => {
116
118
  ON ${table.findings} (tenant_id, severity, last_seen_at DESC);
117
119
  CREATE INDEX IF NOT EXISTS ${table.findings}_tenant_asset_idx
118
120
  ON ${table.findings} (tenant_id, asset_id, last_seen_at DESC);
121
+ CREATE TABLE IF NOT EXISTS ${table.observations} (
122
+ tenant_id text NOT NULL,
123
+ observation_id text NOT NULL,
124
+ asset_id text NOT NULL,
125
+ component_id text NOT NULL,
126
+ observed_at timestamptz NOT NULL,
127
+ value jsonb NOT NULL,
128
+ updated_at timestamptz NOT NULL DEFAULT now(),
129
+ PRIMARY KEY (tenant_id, observation_id)
130
+ );
131
+ CREATE INDEX IF NOT EXISTS ${table.observations}_tenant_asset_idx
132
+ ON ${table.observations} (tenant_id, asset_id, observed_at DESC);
133
+ CREATE INDEX IF NOT EXISTS ${table.observations}_tenant_component_idx
134
+ ON ${table.observations} (tenant_id, component_id, observed_at DESC);
119
135
  CREATE TABLE IF NOT EXISTS ${table.leases} (
120
136
  feed_id text PRIMARY KEY,
121
137
  owner_id text NOT NULL,
@@ -378,6 +394,77 @@ var createPostgresVulnerabilityStore = (options) => {
378
394
  save: async (finding) => saveFindings([finding]),
379
395
  saveMany: saveFindings
380
396
  };
397
+ const parseObservation = (value) => {
398
+ const observation = json(value, "Vulnerability observation");
399
+ if (!Value.Check(VulnerabilityObservationSchema, observation))
400
+ throw new Error("Stored vulnerability observation is invalid");
401
+ return observation;
402
+ };
403
+ const saveObservations = async (tenantId, observations2) => {
404
+ await ensureSchema();
405
+ const normalizedTenantId = requiredText(tenantId, "Tenant id");
406
+ if (observations2.length === 0)
407
+ return;
408
+ for (const observation of observations2) {
409
+ const observationId = observation.id;
410
+ if (!Value.Check(VulnerabilityObservationSchema, observation))
411
+ throw new Error(`Vulnerability observation ${observationId} is invalid`);
412
+ }
413
+ const payload = JSON.stringify(observations2.map((observation) => ({
414
+ asset_id: observation.assetId,
415
+ component_id: observation.componentId,
416
+ observation_id: observation.id,
417
+ observed_at: observation.observedAt,
418
+ value: observation
419
+ })));
420
+ await sql`
421
+ INSERT INTO ${sql.unsafe(table.observations)} (
422
+ tenant_id, observation_id, asset_id, component_id, observed_at, value,
423
+ updated_at
424
+ )
425
+ SELECT
426
+ ${normalizedTenantId}, item.observation_id, item.asset_id,
427
+ item.component_id, item.observed_at::timestamptz, item.value, now()
428
+ FROM jsonb_to_recordset(${payload}::jsonb) AS item(
429
+ observation_id text, asset_id text, component_id text,
430
+ observed_at text, value jsonb
431
+ )
432
+ ON CONFLICT (tenant_id, observation_id) DO UPDATE SET
433
+ asset_id = EXCLUDED.asset_id,
434
+ component_id = EXCLUDED.component_id,
435
+ observed_at = EXCLUDED.observed_at,
436
+ value = EXCLUDED.value,
437
+ updated_at = now()
438
+ `;
439
+ };
440
+ const observations = {
441
+ get: async (tenantId, observationId) => {
442
+ await ensureSchema();
443
+ const rows = await sql`
444
+ SELECT value FROM ${sql.unsafe(table.observations)}
445
+ WHERE tenant_id = ${requiredText(tenantId, "Tenant id")}
446
+ AND observation_id = ${requiredText(observationId, "Observation id")}
447
+ `;
448
+ return rows[0] ? parseObservation(rows[0].value) : null;
449
+ },
450
+ list: async (filter) => {
451
+ await ensureSchema();
452
+ const tenantId = requiredText(filter.tenantId, "Tenant id");
453
+ const assetId = filter.assetId?.trim() || null;
454
+ const componentId = filter.componentId?.trim() || null;
455
+ const rows = await sql`
456
+ SELECT value FROM ${sql.unsafe(table.observations)}
457
+ WHERE tenant_id = ${tenantId}
458
+ AND (${assetId}::text IS NULL OR asset_id = ${assetId})
459
+ AND (${componentId}::text IS NULL OR component_id = ${componentId})
460
+ ORDER BY observed_at DESC, observation_id
461
+ LIMIT ${limit(filter.limit)}
462
+ `;
463
+ return rows.map(({ value }) => parseObservation(value));
464
+ },
465
+ save: async (tenantId, observation) => saveObservations(tenantId, [observation]),
466
+ saveMany: saveObservations
467
+ };
381
468
  const leases = {
382
469
  acquire: async (request) => {
383
470
  await ensureSchema();
@@ -415,7 +502,14 @@ var createPostgresVulnerabilityStore = (options) => {
415
502
  return rows.length > 0;
416
503
  }
417
504
  };
418
- return { ensureSchema, findings, leases, snapshots, syncRuns };
505
+ return {
506
+ ensureSchema,
507
+ findings,
508
+ leases,
509
+ observations,
510
+ snapshots,
511
+ syncRuns
512
+ };
419
513
  };
420
514
  export {
421
515
  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 and observations"
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, records, sync history, managed findings, and distributed refresh leases.",
20
+ description: "Durable Postgres snapshots, records, sync history, managed findings, observations, 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 findings",
33
+ description: "Postgres connection string for vulnerability intelligence, findings, and observations",
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, history, findings, and leases",
48
+ description: "Stores vulnerability intelligence, history, findings, observations, and leases",
49
49
  id: "postgres"
50
50
  }
51
51
  ]
@@ -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 and observations"
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, records, sync history, managed findings, and distributed refresh leases.",
27
+ "description": "Durable Postgres snapshots, records, sync history, managed findings, observations, 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 findings",
40
+ "description": "Postgres connection string for vulnerability intelligence, findings, and observations",
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, history, findings, and leases",
55
+ "description": "Stores vulnerability intelligence, history, findings, observations, 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.1.1",
4
- "description": "Durable Postgres feed snapshots, sync history, managed findings, and refresh leases for AbsoluteJS vulnerability management.",
3
+ "version": "0.2.0",
4
+ "description": "Durable Postgres feed snapshots, sync history, findings, observations, 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.6.0",
56
+ "@absolutejs/vulnerabilities": "0.6.1",
57
57
  "@sinclair/typebox": "0.34.52"
58
58
  },
59
59
  "peerDependencies": {