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
4 changes: 3 additions & 1 deletion apps/api/src/__tests__/feed-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ describe('Feed Registry', () => {
// /breaches sync only — no paid /breachedaccount).
// `ofac` joined as the free, authoritative on-chain attribution feed
// (OFAC SDN sanctioned crypto addresses; dual-sinks iocs + wallets).
// `aiid` joined as the AI-threat-landscape feed (AI Incident Database;
// sinks to ai_incidents).
const EXPECTED_FEEDS = [
'otx', 'cisa', 'cveorg', 'nvd', 'abusessl', 'threatfox',
'urlhaus', 'malwarebazaar', 'openphish', 'ofac', 'mitre', 'mispgalaxy',
'urlhaus', 'malwarebazaar', 'openphish', 'ofac', 'aiid', 'mitre', 'mispgalaxy',
'epss', 'hibp',
];

Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/queues/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ export const JOB_REGISTRY: ScheduledJobRegistration[] = [
queue: feedSyncQueue,
payload: { source: 'openphish' },
},
{
// AI Incident Database (incidentdatabase.ai). The live AI-threat
// landscape signal — real-world AI harm/failure incidents. Paged from
// the GraphQL API (~8 small requests for the ~1.5k corpus), so daily
// at 02:15 UTC is cheap. Idempotent upsert on incident_id.
key: 'aiidSync',
jobId: 'scheduled-aiid-sync',
name: 'aiid-sync',
description: 'Sync AI Incident Database (incidentdatabase.ai) incidents',
defaultCron: '15 2 * * *',
queue: feedSyncQueue,
payload: { source: 'aiid' },
},
{
// OFAC SDN sanctioned crypto addresses (US Treasury). The free,
// authoritative on-chain attribution feed — dual-sinks to iocs
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/routes/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import mitreRoutes from './v1/mitre';
import graphRoutes from './v1/graph';
import agentRoutes from './v1/agent';
import onchainRoutes from './v1/onchain';
import aiIncidentsRoutes from './v1/aiIncidents';
import v1SearchRoutes from './v1/search';
import intelligenceRoutes from './v1/intelligence';
import sightingRoutes from './v1/sightings';
Expand Down Expand Up @@ -133,6 +134,7 @@ v1.route('/', mitreRoutes); // /techniques, /threat-actors, /malware, /tool
v1.route('/', graphRoutes); // /graph/layout, /graph/neo4j/*
v1.route('/', agentRoutes); // /agent/tools, /agent/tool/:name (AA.1 tool plane)
v1.route('/', onchainRoutes); // /onchain/wallets (AA.6.1 follow-the-money)
v1.route('/', aiIncidentsRoutes); // /ai-incidents, /ai-incidents/stats (AI vertical)
v1.route('/', v1SearchRoutes); // /search, /search/vector, /search/similar/*
v1.route('/', intelligenceRoutes); // /intelligence/ioc/:value, /intelligence/cve/:cveId
v1.route('/', sightingRoutes); // /iocs/:id/sightings, /sightings/recent, /sightings/stats
Expand Down
34 changes: 34 additions & 0 deletions apps/api/src/routes/v1/aiIncidents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* /v1/ai-incidents — the AI-threat-landscape vertical.
*
* GET /ai-incidents list (filter: q, since, limit)
* GET /ai-incidents/stats total + monthly timeline + top developers (trend)
*
* Read-only: rows are feed-ingested from the AI Incident Database
* (incidentdatabase.ai) — there is no operator write path. Mirrors /v1/onchain
* + /v1/telco. Reads open to any authenticated user.
*/

import { Hono } from 'hono';
import { requireAuth } from '../../middleware/auth';
import { listAiIncidents, aiIncidentStats } from '../../services/aiIncidentStore';

const router = new Hono();
router.use('*', requireAuth);

router.get('/ai-incidents/stats', async (c) => {
const months = Number(c.req.query('months')) || 24;
const stats = await aiIncidentStats(months);
return c.json({ success: true, data: stats });
});

router.get('/ai-incidents', async (c) => {
const rows = await listAiIncidents({
q: c.req.query('q') || undefined,
since: c.req.query('since') || undefined,
limit: Number(c.req.query('limit')) || undefined,
});
return c.json({ success: true, data: rows, count: rows.length });
});

export default router;
69 changes: 69 additions & 0 deletions apps/api/src/services/aiIncidentStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* AI-incident store — read CRUD for the ai_incidents table (AI vertical).
*
* Rows are feed-ingested from the AI Incident Database (see
* apps/worker/src/feeds/ai-incidents.ts); this is the read side the dashboard
* + Hunt agent consume. `stats()` powers the "AI incidents over time" trend —
* the AI-vertical analogue of the IOC landscape-shift band.
*/

import { db, desc, ilike, and, gte, sql } from '@rinjani/db';
import { aiIncidents } from '@rinjani/db/schema';
import type { AiIncident } from '@rinjani/db/schema';

export async function listAiIncidents(filters: {
q?: string;
/** Filter to incidents on/after this YYYY-MM-DD. */
since?: string;
limit?: number;
} = {}): Promise<AiIncident[]> {
const conds = [];
if (filters.q) conds.push(ilike(aiIncidents.title, `%${filters.q}%`));
if (filters.since && /^\d{4}-\d{2}-\d{2}$/.test(filters.since)) {
conds.push(gte(aiIncidents.incidentDate, filters.since));
}
return db
.select()
.from(aiIncidents)
.where(conds.length ? and(...conds) : undefined)
.orderBy(desc(aiIncidents.incidentDate))
.limit(Math.min(filters.limit ?? 100, 500));
}

export interface AiIncidentStats {
total: number;
/** Incidents per month (YYYY-MM) over the window — the "over time" trend. */
timeline: Array<{ month: string; count: number }>;
/** Most-named developers across all incidents — the AI-vertical movers. */
topDevelopers: Array<{ name: string; count: number }>;
}

export async function aiIncidentStats(months = 24): Promise<AiIncidentStats> {
const [{ total }] = await db
.select({ total: sql<number>`count(*)::int` })
.from(aiIncidents);

// Monthly buckets by incident_date over the trailing window.
const timelineRows = await db.execute(sql`
SELECT to_char(incident_date, 'YYYY-MM') AS month, count(*)::int AS count
FROM ai_incidents
WHERE incident_date >= (CURRENT_DATE - (${months} || ' months')::interval)
GROUP BY month
ORDER BY month
`) as unknown as Array<{ month: string; count: number }>;

// Top alleged developers (jsonb string[] unnested).
const devRows = await db.execute(sql`
SELECT slug AS name, count(*)::int AS count
FROM ai_incidents, jsonb_array_elements_text(developers) AS slug
GROUP BY slug
ORDER BY count DESC
LIMIT 15
`) as unknown as Array<{ name: string; count: number }>;

return {
total: Number(total) || 0,
timeline: timelineRows.map((r) => ({ month: r.month, count: Number(r.count) })),
topDevelopers: devRows.map((r) => ({ name: r.name, count: Number(r.count) })),
};
}
6 changes: 6 additions & 0 deletions apps/api/src/services/feedSync/additionalFeeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export async function syncOFACFeed(): Promise<SyncResult> {
return normalise(await syncOFAC());
}

export async function syncAIIncidentsFeed(): Promise<SyncResult> {
// @ts-ignore — worker scripts outside rootDir, resolved at runtime
const { syncAIIncidents } = await import('../../../../worker/src/feeds/ai-incidents');
return normalise(await syncAIIncidents());
}

export async function syncMITREFeed(): Promise<SyncResult> {
try {
// @ts-ignore
Expand Down
6 changes: 5 additions & 1 deletion apps/api/src/services/feedSync/feedRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { syncCveOrgFeed } from './cveOrgSync';
import {
syncAbuseSSLFeed, syncThreatFoxFeed, syncURLhausFeed,
syncMalwareBazaarFeed, syncOpenPhishFeed, syncMITREFeed, syncMISPGalaxyFeed,
syncEPSSFeed, syncOFACFeed,
syncEPSSFeed, syncOFACFeed, syncAIIncidentsFeed,
} from './additionalFeeds';
import { syncHibpBreaches } from './hibpSync';
import { FeedManifest as FeedManifestSchema } from '@rinjani/feed-engine';
Expand Down Expand Up @@ -51,6 +51,10 @@ const FEED_REGISTRY: Record<string, FeedHandler> = {
// on-chain attribution source. Dual-sinks to iocs (tag `sanctioned`,
// surfaces in Landscape shift) + wallets (entityType `sanctioned`).
ofac: () => syncOFACFeed(),
// AI Incident Database — real-world AI harm/failure incidents
// (incidentdatabase.ai). The live AI-threat-landscape signal; sinks to
// the dedicated ai_incidents table.
aiid: () => syncAIIncidentsFeed(),
mitre: () => syncMITREFeed(),
mispgalaxy: () => syncMISPGalaxyFeed(),
// EPSS — FIRST.org's daily exploit-prediction score. Pairs with the
Expand Down
Loading
Loading