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
36 changes: 10 additions & 26 deletions apps/api/src/cloud-security/cloud-security-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getManifest } from '@trycompai/integration-platform';
import { sanitizeEvidence } from './evidence-sanitizer';
import { getLegacyFindings } from './cloud-security-query.legacy';
import { resolveCheckKey } from './check-definition.utils';
import { loadActiveExceptionSet } from './finding-exceptions';
import type {
CloudFinding,
CloudProvider,
Expand Down Expand Up @@ -198,38 +199,21 @@ export class CloudSecurityQueryService {
if (options.includeExceptions) return combined;

// Filter out findings under an active (non-revoked, non-expired)
// FindingException. Looked up in one query keyed by org so the cost
// stays constant regardless of finding count.
const activeExceptionKeys = await this.loadActiveExceptionKeys(organizationId);
if (activeExceptionKeys.size === 0) return combined;
// FindingException, via the shared exception set so this view stays matched
// with the task-check status/display (one source of truth).
const exceptions = await loadActiveExceptionSet(organizationId);
if (exceptions.size === 0) return combined;

return combined.filter((finding) => {
if (!finding.checkKey || !finding.resourceId) return true;
const key = `${finding.connectionId}::${finding.checkKey}::${finding.resourceId}`;
return !activeExceptionKeys.has(key);
return !exceptions.has(
finding.connectionId,
finding.checkKey,
finding.resourceId,
);
});
}

/**
* Return the set of (connectionId, checkId, resourceId) tuples that have
* an active exception in this org. One DB query per getFindings call.
*/
private async loadActiveExceptionKeys(
organizationId: string,
): Promise<Set<string>> {
const active = await db.findingException.findMany({
where: {
organizationId,
revokedAt: null,
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
},
select: { connectionId: true, checkId: true, resourceId: true },
});
return new Set(
active.map((e) => `${e.connectionId}::${e.checkId}::${e.resourceId}`),
);
}

private async getLatestRunsByConnection(
connectionIds: string[],
): Promise<Map<string, CloudProviderLatestRun>> {
Expand Down
57 changes: 57 additions & 0 deletions apps/api/src/cloud-security/finding-exceptions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
jest.mock('@db', () => ({
db: { findingException: { findMany: jest.fn() } },
}));

import { db } from '@db';
import {
ActiveExceptionSet,
loadActiveExceptionSet,
} from './finding-exceptions';

const findMany = jest.mocked(db.findingException.findMany);

describe('ActiveExceptionSet', () => {
it('matches by (connectionId, checkId, resourceId)', () => {
const set = new ActiveExceptionSet([
ActiveExceptionSet.key('c1', 'aws-s3-public-access', 'bucket-1'),
]);
expect(set.has('c1', 'aws-s3-public-access', 'bucket-1')).toBe(true);
expect(set.has('c1', 'aws-s3-public-access', 'bucket-2')).toBe(false);
expect(set.has('c2', 'aws-s3-public-access', 'bucket-1')).toBe(false);
expect(set.size).toBe(1);
});
});

describe('loadActiveExceptionSet', () => {
beforeEach(() => jest.clearAllMocks());

it('builds the set from active exceptions', async () => {
findMany.mockResolvedValue([
{
connectionId: 'c1',
checkId: 'aws-s3-public-access',
resourceId: 'bucket-1',
},
] as never);

const set = await loadActiveExceptionSet('org_1');

expect(set.has('c1', 'aws-s3-public-access', 'bucket-1')).toBe(true);
// Query only active exceptions (not revoked, not expired).
expect(findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
organizationId: 'org_1',
revokedAt: null,
}),
}),
);
});

it('fail-safe: returns an empty set if the lookup throws (suppress nothing)', async () => {
findMany.mockRejectedValue(new Error('db down'));
const set = await loadActiveExceptionSet('org_1');
expect(set.size).toBe(0);
expect(set.has('c1', 'aws-s3-public-access', 'bucket-1')).toBe(false);
});
});
73 changes: 73 additions & 0 deletions apps/api/src/cloud-security/finding-exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { db } from '@db';

/**
* The set of findings — keyed by (connectionId, checkId, resourceId) — that
* currently have an ACTIVE exception (not revoked, not expired) for an org.
*
* This is the SINGLE source of truth for "is this finding excepted?". Every
* place that turns a check result into a pass/fail must go through here so an
* exception is honored consistently:
* - the Cloud Tests findings view (cloud-security-query.getFindings)
* - the task-check display (task-integrations getTaskCheckRuns)
* - the task-check run paths that set task status (manual run-check + the
* scheduled Trigger task)
*
* Centralizing the key format + the active filter here is deliberate: it's the
* easy-to-get-wrong part, and duplicating it risks honoring an exception in one
* surface but not another.
*/
export class ActiveExceptionSet {
private readonly keys: Set<string>;

constructor(keys: Iterable<string>) {
this.keys = new Set(keys);
}

/** Canonical key. The only place this format is defined. */
static key(
connectionId: string,
checkId: string,
resourceId: string,
): string {
return `${connectionId}::${checkId}::${resourceId}`;
}

get size(): number {
return this.keys.size;
}

has(connectionId: string, checkId: string, resourceId: string): boolean {
return this.keys.has(
ActiveExceptionSet.key(connectionId, checkId, resourceId),
);
}
}

/**
* Load active finding exceptions for an org as an {@link ActiveExceptionSet}.
*
* Fail-safe: on any DB error this returns an EMPTY set (suppress nothing) so a
* lookup failure can never hide a real finding — we would rather show a finding
* the customer excepted than silently pass a genuine one.
*/
export async function loadActiveExceptionSet(
organizationId: string,
): Promise<ActiveExceptionSet> {
try {
const active = await db.findingException.findMany({
where: {
organizationId,
revokedAt: null,
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
},
select: { connectionId: true, checkId: true, resourceId: true },
});
return new ActiveExceptionSet(
active.map((e) =>
ActiveExceptionSet.key(e.connectionId, e.checkId, e.resourceId),
),
);
} catch {
return new ActiveExceptionSet([]);
}
}
Loading
Loading