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
50 changes: 43 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ jobs:
package-lock.json
api-service/package-lock.json
packages/risk-engine/package-lock.json
packages/integrations/package-lock.json
lambdas/collector/package-lock.json
lambdas/incremental-collector/package-lock.json
lambdas/list-accounts/package-lock.json
lambdas/graph-writer/package-lock.json
lambdas/dspm-scanner/package-lock.json
ui/package-lock.json
cdk/package-lock.json

Expand All @@ -60,9 +62,13 @@ jobs:
working-directory: packages/risk-engine
run: npx prettier --check "src/**/*.ts"

- name: Check formatting (Integrations)
working-directory: packages/integrations
run: npx prettier --check "src/**/*.ts"

- name: Check formatting (Lambdas)
run: |
for dir in lambdas/collector lambdas/incremental-collector lambdas/list-accounts lambdas/graph-writer; do
for dir in lambdas/collector lambdas/incremental-collector lambdas/list-accounts lambdas/graph-writer lambdas/dspm-scanner; do
if [ -f "$dir/package.json" ]; then
echo "Checking $dir..."
npx prettier --check "$dir/**/*.ts" || exit 1
Expand All @@ -85,9 +91,13 @@ jobs:
working-directory: packages/risk-engine
run: npx eslint src --ext .ts || true

- name: Lint Integrations
working-directory: packages/integrations
run: npx eslint src --ext .ts || true

- name: Lint Lambdas
run: |
for dir in lambdas/collector lambdas/incremental-collector lambdas/list-accounts lambdas/graph-writer; do
for dir in lambdas/collector lambdas/incremental-collector lambdas/list-accounts lambdas/graph-writer lambdas/dspm-scanner; do
if [ -f "$dir/package.json" ] && [ -f "$dir/.eslintrc.js" -o -f "$dir/.eslintrc.json" ]; then
echo "Linting $dir..."
(cd "$dir" && npx eslint . --ext .ts) || exit 1
Expand All @@ -111,22 +121,28 @@ jobs:
package-lock.json
api-service/package-lock.json
packages/risk-engine/package-lock.json
packages/integrations/package-lock.json
lambdas/collector/package-lock.json
lambdas/incremental-collector/package-lock.json
lambdas/list-accounts/package-lock.json
lambdas/graph-writer/package-lock.json
lambdas/dspm-scanner/package-lock.json
ui/package-lock.json
cdk/package-lock.json

- name: Install all workspace deps
run: npm ci --workspaces

- name: Type check API service
working-directory: api-service
run: npx tsc --noEmit
- name: Build Integrations (required by risk-engine & api-service)
working-directory: packages/integrations
run: npm run build

- name: Type check Risk Engine
- name: Build Risk Engine (required by api-service & CDK)
working-directory: packages/risk-engine
run: npm run build

- name: Type check API service
working-directory: api-service
run: npx tsc --noEmit

- name: Type check Collector
Expand All @@ -145,6 +161,10 @@ jobs:
working-directory: lambdas/graph-writer
run: npx tsc --noEmit

- name: Type check DSPM Scanner
working-directory: lambdas/dspm-scanner
run: npx tsc --noEmit

- name: Type check UI
working-directory: ui
run: npx tsc --noEmit
Expand All @@ -171,17 +191,23 @@ jobs:
package-lock.json
api-service/package-lock.json
packages/risk-engine/package-lock.json
packages/integrations/package-lock.json
lambdas/collector/package-lock.json
lambdas/incremental-collector/package-lock.json
lambdas/list-accounts/package-lock.json
lambdas/graph-writer/package-lock.json
lambdas/dspm-scanner/package-lock.json
ui/package-lock.json
cdk/package-lock.json

- name: Install all workspace deps
run: npm ci --workspaces

- name: Build Risk Engine (must build first — other packages import it)
- name: Build Integrations (must build first — risk-engine imports it)
working-directory: packages/integrations
run: npm run build

- name: Build Risk Engine (must build before API service)
working-directory: packages/risk-engine
run: npm run build

Expand All @@ -201,6 +227,10 @@ jobs:
working-directory: lambdas/graph-writer
run: npm run build

- name: Build DSPM Scanner
working-directory: lambdas/dspm-scanner
run: npm run build

- name: Build API service
working-directory: api-service
run: npm run build
Expand Down Expand Up @@ -235,15 +265,21 @@ jobs:
package-lock.json
api-service/package-lock.json
packages/risk-engine/package-lock.json
packages/integrations/package-lock.json
lambdas/collector/package-lock.json
lambdas/incremental-collector/package-lock.json
lambdas/list-accounts/package-lock.json
lambdas/graph-writer/package-lock.json
lambdas/dspm-scanner/package-lock.json
ui/package-lock.json
cdk/package-lock.json

- name: Install all workspace deps
run: npm ci --workspaces

- name: Build Integrations (required by risk-engine tests)
working-directory: packages/integrations
run: npm run build

- name: Run all tests (root jest)
run: npx jest
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
| `BackupVault` | AWS Backup | `arn` |
| `BackupPlan` | AWS Backup | `arn` |
| `HostedZone` | Route53 | `arn` |
| `DataClassificationFinding` | DSPM Scanner | `arn`, `pii_types`, `secret_count`, `data_classification`, `data_class_source`, `confidence`, `classifier` |
| `EffectivePermission` | Policy Evaluator | `principal_arn`, `allowed_actions`, `denied_actions`, `is_admin`, `blast_radius` |
| `EscalationPath` | Policy Evaluator | `source_arn`, `target_arn`, `risk_level`, `escalation_type` |

### Edge Labels

Expand All @@ -139,6 +142,7 @@
| `RUNS_ON` | ContainerImage → workload (not yet created) |
| `HAS_CVE` | ContainerImage → Vulnerability (not yet created) |
| `HAS_ALIAS` / `HAS_STAGE` | Function → Alias/Stage |
| `CLASSIFIES` | DataClassificationFinding → S3Bucket/RdsInstance |

### Risk Rule Properties (queried by Gremlin rules)

Expand Down
11 changes: 10 additions & 1 deletion api-service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {
} from './routes/compliance';
import { validateGremlinSelectors, validateArnParam } from './middleware/gremlin-validator';
import { authenticate } from './middleware/auth';
import { requireViewer } from './middleware/rbac';
import { requireViewer, requireAnalyst } from './middleware/rbac';
import {
getEffectivePermissions,
getEscalationPaths,
getUnusedPermissions,
getRightsizingRecommendation,
getTrustGraph,
} from './routes/identity';
import { getTrend, listTrendMetrics } from './routes/trends';
import { getIntegrationsStatus, suppressIssue, reopenIssue } from './routes/integrations';

const app = express();

Expand Down Expand Up @@ -79,6 +81,13 @@ app.get('/identity/unused-permissions', requireViewer, getUnusedPermissions);
app.get('/identity/rightsizing/:principal', requireViewer, getRightsizingRecommendation);
app.get('/identity/trust-graph', requireViewer, getTrustGraph);

app.get('/trends', requireViewer, listTrendMetrics);
app.get('/trends/:metric', requireViewer, getTrend);

app.get('/integrations/status', requireViewer, getIntegrationsStatus);
app.post('/issues/:id/suppress', requireAnalyst, suppressIssue);
app.post('/issues/:id/reopen', requireAnalyst, reopenIssue);

app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('Unhandled error:', err);
res.status(500).json({
Expand Down
67 changes: 67 additions & 0 deletions api-service/src/routes/integrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { Request, Response } from 'express';
import { IssueIntegrationsOrchestrator } from '@khalifa/integrations';
import { IssueStore } from '../services/issue-store';

const issueStore = new IssueStore();

export async function getIntegrationsStatus(_req: Request, res: Response): Promise<void> {
try {
const orchestrator = new IssueIntegrationsOrchestrator();
const health = await orchestrator.healthCheck();
res.json({
configuredSinks: orchestrator.configuredSinks,
health,
});
} catch (error) {
console.error('Error getting integrations status:', error);
res.status(500).json({
code: 'INTERNAL_ERROR',
message: 'Failed to get integrations status',
});
}
}

export async function suppressIssue(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const { reason } = req.body || {};

const existing = await issueStore.getIssue(id);
if (!existing) {
res.status(404).json({ code: 'NOT_FOUND', message: `Issue not found: ${id}` });
return;
}

await issueStore.updateStatus(id, 'suppressed');
res.json({ id, status: 'suppressed', reason: reason || null });
} catch (error) {
console.error('Error suppressing issue:', error);
res.status(500).json({
code: 'INTERNAL_ERROR',
message: 'Failed to suppress issue',
details: error instanceof Error ? error.message : undefined,
});
}
}

export async function reopenIssue(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;

const existing = await issueStore.getIssue(id);
if (!existing) {
res.status(404).json({ code: 'NOT_FOUND', message: `Issue not found: ${id}` });
return;
}

await issueStore.updateStatus(id, 'open');
res.json({ id, status: 'open' });
} catch (error) {
console.error('Error reopening issue:', error);
res.status(500).json({
code: 'INTERNAL_ERROR',
message: 'Failed to reopen issue',
details: error instanceof Error ? error.message : undefined,
});
}
}
64 changes: 64 additions & 0 deletions api-service/src/routes/trends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Request, Response } from 'express';
import { PostureTrendStore } from '@khalifa/risk-engine';
import type { TrendPoint, TrendResponse } from '../types';

const trendStore = new PostureTrendStore();

const VALID_METRICS = [
'openIssuesBySeverity',
'exposedResourcesByType',
'failedControlsByFramework',
'publicBuckets',
'usersWithoutMfa',
'crossAccountTrusts',
];

export async function getTrend(req: Request, res: Response): Promise<void> {
try {
const { metric } = req.params;
const days = req.query.days ? parseInt(req.query.days as string, 10) : 90;

if (!metric || !VALID_METRICS.includes(metric)) {
res.status(400).json({
code: 'BAD_REQUEST',
message: `metric must be one of: ${VALID_METRICS.join(', ')}`,
});
return;
}

if (days < 1 || days > 365) {
res.status(400).json({
code: 'BAD_REQUEST',
message: 'days must be between 1 and 365',
});
return;
}

const points = await trendStore.getSeries(metric as any, days);
const trendPoints: TrendPoint[] = points.map((p) => ({
metric: p.metric,
date: p.date,
value: p.value,
accountIds: p.accountIds,
recordedAt: p.recordedAt,
}));

const response: TrendResponse = {
metric,
points: trendPoints,
};

res.json(response);
} catch (error) {
console.error('Error getting trend:', error);
res.status(500).json({
code: 'INTERNAL_ERROR',
message: 'Failed to get trend data',
details: error instanceof Error ? error.message : undefined,
});
}
}

export async function listTrendMetrics(_req: Request, res: Response): Promise<void> {
res.json({ metrics: VALID_METRICS });
}
18 changes: 17 additions & 1 deletion api-service/src/services/issue-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {
GetItemCommand,
QueryCommand,
ScanCommand,
UpdateItemCommand,
} from '@aws-sdk/client-dynamodb';
import type { QueryCommandInput, ScanCommandInput } from '@aws-sdk/lib-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { unmarshall } from '@aws-sdk/util-dynamodb';
import type { Issue, IssueListQuery, IssueListResponse, Severity } from '../types';
import type { Issue, IssueListQuery, IssueListResponse, Severity, IssueStatus } from '../types';

const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 1000;
Expand Down Expand Up @@ -40,6 +41,21 @@ export class IssueStore {
return unmarshall(result.Item) as Issue;
}

async updateStatus(id: string, status: IssueStatus): Promise<void> {
await this.docClient.send(
new UpdateItemCommand({
TableName: this.tableName,
Key: { id: { S: id } } as Record<string, AttributeValue>,
UpdateExpression: 'SET #status = :status, updatedAt = :updatedAt',
ExpressionAttributeNames: { '#status': 'status' },
ExpressionAttributeValues: {
':status': { S: status },
':updatedAt': { S: new Date().toISOString() },
},
})
);
}

async listIssues(query: IssueListQuery): Promise<IssueListResponse> {
const { severity, team, status, ruleId, limit = DEFAULT_LIMIT, nextToken } = query;

Expand Down
21 changes: 21 additions & 0 deletions api-service/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface Issue {
owningTeam: string;
remediationHint: string;
metadata: Record<string, unknown>;
externalRefs?: ExternalRef[];
}

export interface GraphVertex {
Expand Down Expand Up @@ -109,6 +110,26 @@ export interface RiskRule {
enabled: boolean;
}

export interface ExternalRef {
system: string;
id: string;
url?: string;
emittedAt: string;
}

export interface TrendPoint {
metric: string;
date: string;
value: number;
accountIds?: string[];
recordedAt: string;
}

export interface TrendResponse {
metric: string;
points: TrendPoint[];
}

export interface ApiError {
code: string;
message: string;
Expand Down
Loading
Loading