Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d7a9114
docs(reports): design posture report corrections
Jul 14, 2026
4b93378
docs(reports): plan posture report corrections
Jul 14, 2026
86b0d7e
fix(reports): load vulnerability counts across RLS contexts
Jul 14, 2026
f9bbe3f
fix(reports): count posture vulnerabilities accurately
Jul 14, 2026
2fe335d
fix(reports): include all detected security products
Jul 14, 2026
b28259a
test(reports): cover multi-device product aggregation
Jul 14, 2026
f96d8d2
feat(reports): make backup a posture report requirement
Jul 14, 2026
664ffe9
test(reports): verify neutral backup metric status
Jul 14, 2026
9aaba0a
feat(reports): configure posture backup requirement
Jul 14, 2026
82fc8cd
fix(reports): continue posture products across PDF pages
Jul 14, 2026
13609b7
test(reports): verify multi-page posture products
Jul 14, 2026
5262a27
docs(reports): explain posture backup requirements
Jul 14, 2026
fc00927
fix(reports): bound vulnerability catalog lookups
Jul 14, 2026
a235468
fix(reports): correct posture report wording
Jul 14, 2026
b07f1f9
docs(test): plan API suite stabilization
Jul 14, 2026
84517f7
test(api): move validator import guard to eslint
Jul 14, 2026
b92f33e
test(api): lighten metrics route initialization
Jul 14, 2026
a8fe24c
test(api): reset metrics state without module reloads
Jul 14, 2026
1031f35
test(api): reset sentinelone metric snapshots
Jul 14, 2026
b9ccc99
test(api): stabilize oauth interaction route setup
Jul 14, 2026
07bd026
test(api): reuse mcp bootstrap route graph
Jul 14, 2026
e9d56f0
test(api): reuse mcp authorization route graphs
Jul 14, 2026
1dcec1c
docs(test): extend API stability plan
Jul 14, 2026
b4f1825
test(api): stabilize oauth route setup
Jul 14, 2026
8fe5126
test(api): stabilize mcp transport route setup
Jul 14, 2026
d7e9aca
fix(api): clean up aborted mcp SSE sessions
Jul 14, 2026
27e9c46
test(api): stabilize encrypted column registry setup
Jul 14, 2026
35f2ea2
docs(test): plan final API stability fixes
Jul 14, 2026
0618a49
test(api): stabilize oauth security harnesses
Jul 14, 2026
52fbf78
test(api): isolate migration ordering contract
Jul 14, 2026
11c6971
test(api): type oauth harness delegates
Jul 14, 2026
2256bd3
docs(test): plan API worker cap
Jul 14, 2026
f2b8aba
test(api): bound vitest worker parallelism
Jul 14, 2026
36caa07
docs(test): plan final cold-path fixes
Jul 14, 2026
043822a
test(api): stabilize final cold-path harnesses
Jul 14, 2026
58e7703
test(api): preload partner org-scope routes
Jul 14, 2026
1bdc08e
docs(test): plan bootstrap parity isolation
Jul 15, 2026
c95e92a
test(api): isolate bootstrap permission parity
Jul 15, 2026
5c44d51
fix(reports): preserve report config through builder edits
Jul 15, 2026
3aa1633
fix(reports): retry and surface failed scheduled runs
Jul 15, 2026
9781528
fix(reports): harden posture product inventory guards
Jul 15, 2026
46847f3
test(reports): pin posture config schema parity
Jul 15, 2026
9400825
fix(reports): restore em-dash punctuation in the posture PDF
Jul 15, 2026
760a6a0
docs(reports): restore posture config defaults
Jul 15, 2026
41a530c
docs(reports): park third-party backup evidence design
Jul 15, 2026
c9c74e7
fix(test): give mcpServer.bootstrapCarveout's authed-key fixture SR2-…
Jul 16, 2026
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
17 changes: 17 additions & 0 deletions apps/api/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,21 @@ export default [
linterOptions: { reportUnusedDisableDirectives: 'off' },
rules: {},
},
{
files: ['src/**/*.ts'],
ignores: ['src/**/*.test.ts', 'src/lib/validation.ts'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@hono/zod-validator',
message: 'Import zValidator from src/lib/validation instead.',
},
],
},
],
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import './setup';

import { describe, expect, it } from 'vitest';

import { db, withDbAccessContext, withSystemDbAccessContext } from '../../db';
import { devices, deviceVulnerabilities, vulnerabilities } from '../../db/schema';
import { generateSecurityCompliancePostureReport } from '../../services/securityComplianceReport';
import { loadOpenVulnerabilityCounts } from '../../services/securityComplianceReportVulnerabilities';
import { setupTestEnvironment } from './db-utils';

const runDb = it.runIf(!!process.env.DATABASE_URL);

describe('security compliance report vulnerability isolation', () => {
runDb('counts its own open catalog findings and excludes another organization', async () => {
const envA = await setupTestEnvironment({ scope: 'organization' });
const envB = await setupTestEnvironment({ scope: 'organization' });

const seeded = await withSystemDbAccessContext(async () => {
const [deviceA, deviceB] = await db
.insert(devices)
.values([
{
orgId: envA.organization.id,
siteId: envA.site.id,
agentId: `posture-a-${Date.now()}`,
hostname: 'posture-a',
osType: 'windows',
osVersion: '11',
architecture: 'x86_64',
agentVersion: 'test',
status: 'offline',
},
{
orgId: envB.organization.id,
siteId: envB.site.id,
agentId: `posture-b-${Date.now()}`,
hostname: 'posture-b',
osType: 'windows',
osVersion: '11',
architecture: 'x86_64',
agentVersion: 'test',
status: 'offline',
},
])
.returning({ id: devices.id, orgId: devices.orgId });

const [catalogA, catalogB, patchedCatalog, mitigatedCatalog, acceptedCatalog] = await db
.insert(vulnerabilities)
.values([
{ cveId: 'CVE-2026-71001', source: 'nvd', description: 'org A open finding', severity: 'HIGH', rawPayload: {} },
{ cveId: 'CVE-2026-71002', source: 'msrc', description: 'org B open finding', severity: 'Critical', rawPayload: {} },
{ cveId: 'CVE-2026-71003', source: 'nvd', description: 'org A patched finding', severity: 'CRITICAL', rawPayload: {} },
{ cveId: 'CVE-2026-71004', source: 'nvd', description: 'org A mitigated finding', severity: 'CRITICAL', rawPayload: {} },
{ cveId: 'CVE-2026-71005', source: 'nvd', description: 'org A accepted finding', severity: 'CRITICAL', rawPayload: {} },
])
.returning({ id: vulnerabilities.id });

await db.insert(deviceVulnerabilities).values([
{
orgId: envA.organization.id,
deviceId: deviceA!.id,
vulnerabilityId: catalogA!.id,
status: 'open',
detectedAt: new Date(),
},
{
orgId: envB.organization.id,
deviceId: deviceB!.id,
vulnerabilityId: catalogB!.id,
status: 'open',
detectedAt: new Date(),
},
{
orgId: envA.organization.id,
deviceId: deviceA!.id,
vulnerabilityId: patchedCatalog!.id,
status: 'patched',
detectedAt: new Date(),
},
{
orgId: envA.organization.id,
deviceId: deviceA!.id,
vulnerabilityId: mitigatedCatalog!.id,
status: 'mitigated',
detectedAt: new Date(),
},
{
orgId: envA.organization.id,
deviceId: deviceA!.id,
vulnerabilityId: acceptedCatalog!.id,
status: 'accepted',
detectedAt: new Date(),
},
]);

return { deviceA: deviceA!.id, deviceB: deviceB!.id };
});

const { result, vulnerabilityCounts } = await withDbAccessContext(
{
scope: 'organization',
orgId: envA.organization.id,
accessibleOrgIds: [envA.organization.id],
userId: envA.user.id,
},
async () => {
const result = await generateSecurityCompliancePostureReport(envA.organization.id, {
includeCis: false,
});
const vulnerabilityCounts = await loadOpenVulnerabilityCounts([
seeded.deviceA,
seeded.deviceB,
]);
return { result, vulnerabilityCounts };
},
);

expect(result.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
hostname: 'posture-a',
openVulnHigh: 1,
openVulnCritical: 0,
}),
]),
);
expect(result.rows).not.toEqual(
expect.arrayContaining([expect.objectContaining({ hostname: 'posture-b' })]),
);
expect(vulnerabilityCounts.get(seeded.deviceA)).toEqual({ high: 1, critical: 0 });
expect(vulnerabilityCounts.has(seeded.deviceB)).toBe(false);
});
});
87 changes: 1 addition & 86 deletions apps/api/src/db/autoMigrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
partitionLedgerRows,
} from './autoMigrate';
import { createHash } from 'node:crypto';
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

Expand Down Expand Up @@ -114,91 +114,6 @@ describe('autoMigrate', () => {
});
});

// Regression test for issue #506. A `localeCompare` sort places
// `2026-04-19-installer-bootstrap-tokens-constraints.sql` before
// `...-tokens.sql` (because '-' < '.'), so the constraints migration ran
// before the table that owns those constraints existed. This scans every
// migration in the same order autoMigrate uses and asserts that each
// referenced table was created in this file or an earlier one.
describe('migration ordering', () => {
const MIGRATION_FILE_PATTERN = /^\d{4}-.*\.sql$/;
const migrationsDir = path.resolve(__dirname, '../../migrations');

const SYSTEM_TABLES = new Set([
'pg_policies',
'pg_indexes',
'pg_class',
'pg_namespace',
'pg_trigger',
'pg_proc',
'pg_constraint',
'pg_attribute',
'pg_type',
'pg_tables',
'information_schema',
]);

function collectMatches(sql: string, pattern: RegExp): string[] {
const out: string[] = [];
for (const match of sql.matchAll(pattern)) {
if (match[1]) out.push(match[1].toLowerCase());
}
return out;
}

function extractCreatedTables(sql: string): string[] {
return collectMatches(
sql,
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
);
}

function extractReferencedTables(sql: string): string[] {
const stripped = sql
.replace(/--[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '');
// Only patterns that hard-fail when the table is missing. Tolerant
// forms like `DROP TABLE IF EXISTS` or `ALTER TABLE IF EXISTS` are
// intentionally excluded — they're a no-op against an absent table.
const patterns = [
/\bREFERENCES\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bALTER\s+TABLE\s+(?!IF\s+EXISTS\b)(?:ONLY\s+)?(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bCREATE\s+POLICY\s+[^;]*?\bON\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?[^;]*?\bON\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bCREATE\s+TRIGGER\s+[^;]*?\bON\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
];
const refs: string[] = [];
for (const pattern of patterns) refs.push(...collectMatches(stripped, pattern));
return refs;
}

it('every referenced table is created in the same file or an earlier one', () => {
const files = readdirSync(migrationsDir)
.filter((name) => MIGRATION_FILE_PATTERN.test(name))
.sort((a, b) => a.localeCompare(b));

expect(files.length).toBeGreaterThan(0);

const created = new Set<string>();
const violations: string[] = [];

for (const file of files) {
const sql = readFileSync(path.join(migrationsDir, file), 'utf8');
// Add tables created in this file BEFORE checking references so a
// file that creates a table and immediately alters or self-references
// it passes.
for (const t of extractCreatedTables(sql)) created.add(t);
for (const ref of extractReferencedTables(sql)) {
if (SYSTEM_TABLES.has(ref)) continue;
if (created.has(ref)) continue;
violations.push(`${file} references "${ref}" before it is created`);
}
}

expect(violations).toEqual([]);
});
});

describe('hasNoTransactionDirective', () => {
it('returns true when "-- @no-transaction" is the first line', () => {
expect(hasNoTransactionDirective('-- @no-transaction\nCREATE INDEX foo ON bar (x);')).toBe(
Expand Down
92 changes: 92 additions & 0 deletions apps/api/src/db/migrationOrdering.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it, expect } from 'vitest';
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';

// Regression test for issue #506. A `localeCompare` sort places
// `2026-04-19-installer-bootstrap-tokens-constraints.sql` before
// `...-tokens.sql` (because '-' < '.'), so the constraints migration ran
// before the table that owns those constraints existed. This scans every
// migration in the same order autoMigrate uses and asserts that each
// referenced table was created in this file or an earlier one.
describe('migration ordering', () => {
const MIGRATION_FILE_PATTERN = /^\d{4}-.*\.sql$/;
const migrationsDir = path.resolve(__dirname, '../../migrations');

const SYSTEM_TABLES = new Set([
'pg_policies',
'pg_indexes',
'pg_class',
'pg_namespace',
'pg_trigger',
'pg_proc',
'pg_constraint',
'pg_attribute',
'pg_type',
'pg_tables',
'information_schema',
]);

function collectMatches(sql: string, pattern: RegExp): string[] {
const out: string[] = [];
for (const match of sql.matchAll(pattern)) {
if (match[1]) out.push(match[1].toLowerCase());
}
return out;
}

function extractCreatedTables(sql: string): string[] {
return collectMatches(
sql,
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
);
}

function extractReferencedTables(sql: string): string[] {
const stripped = sql
.replace(/--[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '');
// Only patterns that hard-fail when the table is missing. Tolerant
// forms like `DROP TABLE IF EXISTS` or `ALTER TABLE IF EXISTS` are
// intentionally excluded — they're a no-op against an absent table.
const patterns = [
/\bREFERENCES\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bALTER\s+TABLE\s+(?!IF\s+EXISTS\b)(?:ONLY\s+)?(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bCREATE\s+POLICY\s+[^;]*?\bON\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?[^;]*?\bON\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
/\bCREATE\s+TRIGGER\s+[^;]*?\bON\s+(?:"?public"?\.)?"?([a-zA-Z_][a-zA-Z0-9_]*)"?/gi,
];
const refs: string[] = [];
for (const pattern of patterns) refs.push(...collectMatches(stripped, pattern));
return refs;
}

it('every referenced table is created in the same file or an earlier one', async () => {
const files = (await readdir(migrationsDir))
.filter((name) => MIGRATION_FILE_PATTERN.test(name))
.sort((a, b) => a.localeCompare(b));

expect(files.length).toBeGreaterThan(0);

const migrations = await Promise.all(files.map(async (file) => ({
file,
sql: await readFile(path.join(migrationsDir, file), 'utf8'),
})));

const created = new Set<string>();
const violations: string[] = [];

for (const { file, sql } of migrations) {
// Add tables created in this file BEFORE checking references so a
// file that creates a table and immediately alters or self-references
// it passes.
for (const t of extractCreatedTables(sql)) created.add(t);
for (const ref of extractReferencedTables(sql)) {
if (SYSTEM_TABLES.has(ref)) continue;
if (created.has(ref)) continue;
violations.push(`${file} references "${ref}" before it is created`);
}
}

expect(violations).toEqual([]);
});
});
Loading
Loading