@littlebearapps/platform-admin-sdk 1.4.2 → 2.0.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/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/scripts/ops/universal-backfill.ts +147 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Auditor Worker
|
|
3
|
+
*
|
|
4
|
+
* SDK integration auditor using 4-layer triangulation:
|
|
5
|
+
* 1. Config checks — Verify PLATFORM_CACHE KV and PLATFORM_TELEMETRY queue bindings
|
|
6
|
+
* 2. Code smell tests — String matching for SDK patterns in source files
|
|
7
|
+
* 3. Runtime checks — Cross-reference D1 system_health_checks heartbeats
|
|
8
|
+
* 4. AI Judge — Gemini via AI Gateway deep code analysis (rubric-based scoring)
|
|
9
|
+
*
|
|
10
|
+
* Audit targets are loaded from KV (CONFIG:AUDIT_TARGETS) — configure in config/audit-targets.yaml
|
|
11
|
+
* and sync via `npm run sync:config`.
|
|
12
|
+
*
|
|
13
|
+
* Schedule:
|
|
14
|
+
* Sunday Midnight UTC — Full deep scan (all dimensions, all projects)
|
|
15
|
+
* Wednesday Midnight UTC — Focused scan (weak dimensions, projects < threshold)
|
|
16
|
+
*
|
|
17
|
+
* @module workers/platform-auditor
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
KVNamespace,
|
|
22
|
+
ExecutionContext,
|
|
23
|
+
ScheduledEvent,
|
|
24
|
+
D1Database,
|
|
25
|
+
Fetcher,
|
|
26
|
+
} from '@cloudflare/workers-types';
|
|
27
|
+
import {
|
|
28
|
+
withFeatureBudget,
|
|
29
|
+
withCronBudget,
|
|
30
|
+
CircuitBreakerError,
|
|
31
|
+
completeTracking,
|
|
32
|
+
createLogger,
|
|
33
|
+
createLoggerFromRequest,
|
|
34
|
+
createTraceContext,
|
|
35
|
+
health,
|
|
36
|
+
pingHeartbeat,
|
|
37
|
+
type Logger,
|
|
38
|
+
} from '@littlebearapps/platform-consumer-sdk';
|
|
39
|
+
import {
|
|
40
|
+
validateAIJudgeResponse,
|
|
41
|
+
calculateCompositeScore,
|
|
42
|
+
formatValidationErrors,
|
|
43
|
+
DEFAULT_RUBRIC_WEIGHTS,
|
|
44
|
+
type RubricScores,
|
|
45
|
+
type CategorisedIssue,
|
|
46
|
+
} from './lib/ai-judge-schema';
|
|
47
|
+
import {
|
|
48
|
+
runFeatureCoverageAudit,
|
|
49
|
+
storeFeatureCoverageAudit,
|
|
50
|
+
type FeatureCoverageReport,
|
|
51
|
+
} from './lib/auditor/feature-coverage';
|
|
52
|
+
import {
|
|
53
|
+
generateComprehensiveReport,
|
|
54
|
+
storeComprehensiveReport,
|
|
55
|
+
} from './lib/auditor/comprehensive-report';
|
|
56
|
+
import type {
|
|
57
|
+
AuditorEnv,
|
|
58
|
+
AuditTarget,
|
|
59
|
+
AuditTargetsConfig,
|
|
60
|
+
AuditDimension,
|
|
61
|
+
AuditStatus,
|
|
62
|
+
ScanType,
|
|
63
|
+
ConfigCheckResult,
|
|
64
|
+
CodeSmellResult,
|
|
65
|
+
RuntimeCheckResult,
|
|
66
|
+
AIJudgeResult,
|
|
67
|
+
BehavioralResult,
|
|
68
|
+
ProjectAuditResult,
|
|
69
|
+
} from './lib/auditor/types';
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Feature IDs (register these in budgets.yaml)
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
const FEATURE_AUDITOR = 'platform:monitor:auditor';
|
|
76
|
+
const FEATURE_HEARTBEAT = 'platform:heartbeat:auditor';
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Constants (defaults — overridden by audit-targets.yaml config)
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
const DEFAULT_HEARTBEAT_FRESHNESS_HOURS = 24;
|
|
83
|
+
const DEFAULT_AI_CACHE_TTL_SECONDS = 3 * 24 * 60 * 60; // 3 days
|
|
84
|
+
const DEFAULT_FOCUSED_SCAN_THRESHOLD = 90;
|
|
85
|
+
const DEEP_SCAN_FILE_LIMIT = 50000; // Max chars per file
|
|
86
|
+
const SUPPLEMENTARY_CONTENT_WARN_LIMIT = 3_000_000; // ~750K tokens safety
|
|
87
|
+
const DEFAULT_BEHAVIORAL_ANALYSIS_DAYS = 30;
|
|
88
|
+
const DEFAULT_HOTSPOT_MIN_COMMITS = 3;
|
|
89
|
+
const AI_MAX_RETRIES = 2;
|
|
90
|
+
|
|
91
|
+
const GITHUB_API_BASE = 'https://api.github.com';
|
|
92
|
+
const AI_GATEWAY_URL = 'https://gateway.ai.cloudflare.com/v1';
|
|
93
|
+
|
|
94
|
+
const DEFAULT_SDK_PATTERNS = [
|
|
95
|
+
'withFeatureBudget', 'withCronBudget', 'withQueueBudget',
|
|
96
|
+
'trackedEnv', 'platform-sdk', 'completeTracking',
|
|
97
|
+
'CircuitBreakerError', 'PLATFORM_CACHE', 'PLATFORM_TELEMETRY',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Wrangler Config Shape (for JSONC parsing)
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
interface WranglerConfig {
|
|
105
|
+
kv_namespaces?: Array<{ binding: string }>;
|
|
106
|
+
queues?: { producers?: Array<{ binding: string }> };
|
|
107
|
+
observability?: {
|
|
108
|
+
enabled?: boolean;
|
|
109
|
+
logs?: { enabled?: boolean; sampling_rate?: number; invocation_logs?: boolean };
|
|
110
|
+
traces?: { enabled?: boolean; head_sampling_rate?: number };
|
|
111
|
+
};
|
|
112
|
+
upload_source_maps?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// GitHub Types
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
interface GitHubCommit {
|
|
120
|
+
sha: string;
|
|
121
|
+
commit: { message: string; author: { name: string; date: string } };
|
|
122
|
+
files?: Array<{ filename: string; status: string; patch?: string }>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// CONFIG LOADING
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
/** Load audit targets config from KV */
|
|
130
|
+
async function loadAuditConfig(
|
|
131
|
+
env: AuditorEnv,
|
|
132
|
+
log: Logger
|
|
133
|
+
): Promise<AuditTargetsConfig> {
|
|
134
|
+
try {
|
|
135
|
+
const configJson = await env.PLATFORM_CACHE.get('CONFIG:AUDIT_TARGETS');
|
|
136
|
+
if (configJson) {
|
|
137
|
+
return JSON.parse(configJson) as AuditTargetsConfig;
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
log.warn('Failed to load audit targets from KV, using defaults', error);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
targets: {},
|
|
145
|
+
sdkPatterns: DEFAULT_SDK_PATTERNS,
|
|
146
|
+
rubricWeights: { ...DEFAULT_RUBRIC_WEIGHTS },
|
|
147
|
+
heartbeatFreshnessHours: DEFAULT_HEARTBEAT_FRESHNESS_HOURS,
|
|
148
|
+
focusedScanThreshold: DEFAULT_FOCUSED_SCAN_THRESHOLD,
|
|
149
|
+
aiCacheTtlSeconds: DEFAULT_AI_CACHE_TTL_SECONDS,
|
|
150
|
+
behavioralAnalysisDays: DEFAULT_BEHAVIORAL_ANALYSIS_DAYS,
|
|
151
|
+
hotspotMinCommits: DEFAULT_HOTSPOT_MIN_COMMITS,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// SCAN TYPE HELPERS
|
|
157
|
+
// =============================================================================
|
|
158
|
+
|
|
159
|
+
function determineScanType(scheduledTime: number): ScanType {
|
|
160
|
+
const day = new Date(scheduledTime).getUTCDay();
|
|
161
|
+
return day === 0 ? 'full' : 'focused';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function getWeakDimensions(
|
|
165
|
+
project: string,
|
|
166
|
+
threshold: number,
|
|
167
|
+
env: AuditorEnv,
|
|
168
|
+
log: Logger
|
|
169
|
+
): Promise<{ dimensions: AuditDimension[]; previousScore: number }> {
|
|
170
|
+
try {
|
|
171
|
+
const latest = await env.PLATFORM_DB.prepare(
|
|
172
|
+
`SELECT composite_score, rubric_sdk, rubric_observability, rubric_cost_protection, rubric_security
|
|
173
|
+
FROM audit_results
|
|
174
|
+
WHERE project = ? AND scan_type = 'full' AND ai_judge_score IS NOT NULL
|
|
175
|
+
ORDER BY created_at DESC LIMIT 1`
|
|
176
|
+
).bind(project).first<{
|
|
177
|
+
composite_score: number;
|
|
178
|
+
rubric_sdk: number | null;
|
|
179
|
+
rubric_observability: number | null;
|
|
180
|
+
rubric_cost_protection: number | null;
|
|
181
|
+
rubric_security: number | null;
|
|
182
|
+
}>();
|
|
183
|
+
|
|
184
|
+
if (!latest) {
|
|
185
|
+
return { dimensions: ['sdk', 'observability', 'cost', 'security'], previousScore: 0 };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const weak: AuditDimension[] = [];
|
|
189
|
+
if ((latest.rubric_sdk ?? 0) < 4) weak.push('sdk');
|
|
190
|
+
if ((latest.rubric_observability ?? 0) < 4) weak.push('observability');
|
|
191
|
+
if ((latest.rubric_cost_protection ?? 0) < 4) weak.push('cost');
|
|
192
|
+
if ((latest.rubric_security ?? 0) < 4) weak.push('security');
|
|
193
|
+
|
|
194
|
+
if (weak.length === 0 && latest.composite_score < threshold) {
|
|
195
|
+
return { dimensions: ['sdk', 'observability', 'cost', 'security'], previousScore: latest.composite_score };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { dimensions: weak, previousScore: latest.composite_score };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
log.warn('Failed to get weak dimensions, defaulting to full scan', error, { project });
|
|
201
|
+
return { dimensions: ['sdk', 'observability', 'cost', 'security'], previousScore: 0 };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// CHECK FUNCTIONS
|
|
207
|
+
// =============================================================================
|
|
208
|
+
|
|
209
|
+
/** Layer 1: Config checks (wrangler bindings) */
|
|
210
|
+
function checkConfig(config: WranglerConfig | null): ConfigCheckResult {
|
|
211
|
+
if (!config) {
|
|
212
|
+
return {
|
|
213
|
+
hasPlatformCache: false, hasPlatformTelemetry: false,
|
|
214
|
+
observabilityEnabled: false, logsEnabled: false,
|
|
215
|
+
tracesEnabled: false, traceSamplingRate: null,
|
|
216
|
+
logSamplingRate: null, invocationLogsEnabled: false,
|
|
217
|
+
sourceMapsEnabled: false, issues: ['Wrangler config not found or invalid'],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const kvBindings = (config.kv_namespaces ?? []).map((kv) => kv.binding);
|
|
222
|
+
const queueBindings = (config.queues?.producers ?? []).map((q) => q.binding);
|
|
223
|
+
const obs = config.observability;
|
|
224
|
+
const issues: string[] = [];
|
|
225
|
+
|
|
226
|
+
const hasPlatformCache = kvBindings.includes('PLATFORM_CACHE');
|
|
227
|
+
const hasPlatformTelemetry = queueBindings.includes('PLATFORM_TELEMETRY');
|
|
228
|
+
const observabilityEnabled = obs?.enabled ?? false;
|
|
229
|
+
const logsEnabled = obs?.logs?.enabled ?? false;
|
|
230
|
+
const tracesEnabled = obs?.traces?.enabled ?? false;
|
|
231
|
+
|
|
232
|
+
if (!hasPlatformCache) issues.push('Missing PLATFORM_CACHE KV binding');
|
|
233
|
+
if (!hasPlatformTelemetry) issues.push('Missing PLATFORM_TELEMETRY queue binding');
|
|
234
|
+
if (!observabilityEnabled) issues.push('Observability not enabled');
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
hasPlatformCache, hasPlatformTelemetry,
|
|
238
|
+
observabilityEnabled, logsEnabled,
|
|
239
|
+
tracesEnabled,
|
|
240
|
+
traceSamplingRate: obs?.traces?.head_sampling_rate ?? null,
|
|
241
|
+
logSamplingRate: obs?.logs?.sampling_rate ?? null,
|
|
242
|
+
invocationLogsEnabled: obs?.logs?.invocation_logs ?? false,
|
|
243
|
+
sourceMapsEnabled: config.upload_source_maps ?? false,
|
|
244
|
+
issues,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Layer 2: Code smell checks (SDK pattern detection) */
|
|
249
|
+
function checkCodeSmells(
|
|
250
|
+
content: string | null,
|
|
251
|
+
patterns: string[] = DEFAULT_SDK_PATTERNS
|
|
252
|
+
): CodeSmellResult {
|
|
253
|
+
if (!content) {
|
|
254
|
+
return {
|
|
255
|
+
hasSdkFolder: false, hasWithFeatureBudget: false,
|
|
256
|
+
hasTrackedEnv: false, hasCircuitBreakerError: false,
|
|
257
|
+
hasErrorLogging: false, patternCounts: {},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const patternCounts: Record<string, number> = {};
|
|
262
|
+
for (const pattern of patterns) {
|
|
263
|
+
const matches = content.match(new RegExp(pattern, 'g'));
|
|
264
|
+
patternCounts[pattern] = matches?.length ?? 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
hasSdkFolder: content.includes('platform-sdk') || content.includes('@littlebearapps/platform'),
|
|
269
|
+
hasWithFeatureBudget: (patternCounts['withFeatureBudget'] ?? 0) > 0,
|
|
270
|
+
hasTrackedEnv: (patternCounts['trackedEnv'] ?? 0) > 0,
|
|
271
|
+
hasCircuitBreakerError: (patternCounts['CircuitBreakerError'] ?? 0) > 0,
|
|
272
|
+
hasErrorLogging: content.includes('log.error') || content.includes('console.error'),
|
|
273
|
+
patternCounts,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Layer 3: Runtime checks (heartbeat freshness) */
|
|
278
|
+
async function checkRuntime(
|
|
279
|
+
project: string,
|
|
280
|
+
freshnessHours: number,
|
|
281
|
+
env: AuditorEnv,
|
|
282
|
+
log: Logger
|
|
283
|
+
): Promise<RuntimeCheckResult> {
|
|
284
|
+
try {
|
|
285
|
+
const result = await env.PLATFORM_DB.prepare(
|
|
286
|
+
`SELECT MAX(last_heartbeat) as latest
|
|
287
|
+
FROM system_health_checks
|
|
288
|
+
WHERE project_id = ?`
|
|
289
|
+
).bind(project).first<{ latest: number | null }>();
|
|
290
|
+
|
|
291
|
+
if (!result?.latest) {
|
|
292
|
+
return { hasRecentHeartbeat: false, hoursSinceHeartbeat: null, lastHeartbeatTime: null };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const hoursSince = (Date.now() / 1000 - result.latest) / 3600;
|
|
296
|
+
return {
|
|
297
|
+
hasRecentHeartbeat: hoursSince < freshnessHours,
|
|
298
|
+
hoursSinceHeartbeat: Math.round(hoursSince),
|
|
299
|
+
lastHeartbeatTime: new Date(result.latest * 1000).toISOString(),
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
log.error('Runtime check failed', error, { project });
|
|
303
|
+
return { hasRecentHeartbeat: false, hoursSinceHeartbeat: null, lastHeartbeatTime: null };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// =============================================================================
|
|
308
|
+
// AI JUDGE
|
|
309
|
+
// =============================================================================
|
|
310
|
+
|
|
311
|
+
/** Layer 4: AI Judge analysis via Gemini AI Gateway */
|
|
312
|
+
async function auditWithGemini(
|
|
313
|
+
project: string,
|
|
314
|
+
entryPointContent: string,
|
|
315
|
+
wranglerContent: string | null,
|
|
316
|
+
staticChecks: Record<string, boolean>,
|
|
317
|
+
supplementaryContent: string,
|
|
318
|
+
config: AuditTargetsConfig,
|
|
319
|
+
env: AuditorEnv,
|
|
320
|
+
log: Logger
|
|
321
|
+
): Promise<AIJudgeResult | null> {
|
|
322
|
+
// Check content hash cache
|
|
323
|
+
const contentHash = await hashContent(`${project}:${entryPointContent}:${supplementaryContent}`);
|
|
324
|
+
const cacheKey = `AUDIT:AI:${contentHash}`;
|
|
325
|
+
|
|
326
|
+
const cached = await env.PLATFORM_CACHE.get(cacheKey);
|
|
327
|
+
if (cached) {
|
|
328
|
+
log.debug('AI Judge cache hit', { project, cacheKey });
|
|
329
|
+
try {
|
|
330
|
+
return JSON.parse(cached) as AIJudgeResult;
|
|
331
|
+
} catch { /* cache corrupt, proceed with fresh analysis */ }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const prompt = buildAIPrompt(project, entryPointContent, wranglerContent, staticChecks, supplementaryContent);
|
|
335
|
+
const weights = config.rubricWeights ?? DEFAULT_RUBRIC_WEIGHTS;
|
|
336
|
+
|
|
337
|
+
for (let attempt = 0; attempt <= AI_MAX_RETRIES; attempt++) {
|
|
338
|
+
try {
|
|
339
|
+
const response = await callGemini(prompt, env, log);
|
|
340
|
+
if (!response) return null;
|
|
341
|
+
|
|
342
|
+
const parsed = JSON.parse(response);
|
|
343
|
+
const validation = validateAIJudgeResponse(parsed);
|
|
344
|
+
|
|
345
|
+
if (validation.success) {
|
|
346
|
+
const data = validation.data;
|
|
347
|
+
const rubricScores = data.rubricScores;
|
|
348
|
+
const compositeScore = calculateCompositeScore(rubricScores, weights);
|
|
349
|
+
|
|
350
|
+
const result: AIJudgeResult = {
|
|
351
|
+
score: compositeScore,
|
|
352
|
+
summary: data.summary,
|
|
353
|
+
issues: JSON.stringify(data.issues),
|
|
354
|
+
categorisedIssues: JSON.stringify(data.issues),
|
|
355
|
+
reasoning: data.reasoning,
|
|
356
|
+
rubricScores: {
|
|
357
|
+
sdk: rubricScores.sdk.score,
|
|
358
|
+
observability: rubricScores.observability.score,
|
|
359
|
+
costProtection: rubricScores.costProtection.score,
|
|
360
|
+
security: rubricScores.security.score,
|
|
361
|
+
},
|
|
362
|
+
rubricEvidence: JSON.stringify({
|
|
363
|
+
sdk: rubricScores.sdk.evidence,
|
|
364
|
+
observability: rubricScores.observability.evidence,
|
|
365
|
+
costProtection: rubricScores.costProtection.evidence,
|
|
366
|
+
security: rubricScores.security.evidence,
|
|
367
|
+
}),
|
|
368
|
+
validationRetries: attempt,
|
|
369
|
+
cachedAt: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Cache result
|
|
373
|
+
await env.PLATFORM_CACHE.put(cacheKey, JSON.stringify(result), {
|
|
374
|
+
expirationTtl: config.aiCacheTtlSeconds ?? DEFAULT_AI_CACHE_TTL_SECONDS,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
log.info('AI Judge complete', { project, score: compositeScore, attempt });
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Validation failed — retry with error feedback
|
|
382
|
+
log.warn('AI Judge validation failed', { project, attempt, errors: validation.errors });
|
|
383
|
+
if (attempt < AI_MAX_RETRIES) {
|
|
384
|
+
prompt.concat('\n\n' + formatValidationErrors(validation.errors));
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
log.error('AI Judge attempt failed', error, { project, attempt });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Build AI Judge prompt */
|
|
395
|
+
function buildAIPrompt(
|
|
396
|
+
project: string,
|
|
397
|
+
entryPoint: string,
|
|
398
|
+
wrangler: string | null,
|
|
399
|
+
staticChecks: Record<string, boolean>,
|
|
400
|
+
supplementary: string
|
|
401
|
+
): string {
|
|
402
|
+
return `You are an expert Cloudflare Workers SDK integration auditor.
|
|
403
|
+
|
|
404
|
+
Analyse the following project "${project}" and score it on 4 dimensions (1-5 scale):
|
|
405
|
+
- sdk: SDK integration patterns (withFeatureBudget, completeTracking, circuit breakers)
|
|
406
|
+
- observability: Logging, tracing, metrics, error reporting
|
|
407
|
+
- costProtection: Circuit breakers, budgets, DLQ handling, rate limiting
|
|
408
|
+
- security: Authentication, input validation, secrets handling, CORS
|
|
409
|
+
|
|
410
|
+
## Entry Point:
|
|
411
|
+
\`\`\`typescript
|
|
412
|
+
${entryPoint.slice(0, DEEP_SCAN_FILE_LIMIT)}
|
|
413
|
+
\`\`\`
|
|
414
|
+
|
|
415
|
+
${wrangler ? `## Wrangler Config:\n\`\`\`jsonc\n${wrangler.slice(0, 5000)}\n\`\`\`` : ''}
|
|
416
|
+
|
|
417
|
+
## Static Check Results:
|
|
418
|
+
${JSON.stringify(staticChecks, null, 2)}
|
|
419
|
+
|
|
420
|
+
${supplementary ? `## Additional Source Files:\n${supplementary}` : ''}
|
|
421
|
+
|
|
422
|
+
Respond with ONLY valid JSON matching this schema:
|
|
423
|
+
{
|
|
424
|
+
"reasoning": "Chain-of-thought analysis...",
|
|
425
|
+
"rubricScores": {
|
|
426
|
+
"sdk": { "score": 1-5, "evidence": ["..."] },
|
|
427
|
+
"observability": { "score": 1-5, "evidence": ["..."] },
|
|
428
|
+
"costProtection": { "score": 1-5, "evidence": ["..."] },
|
|
429
|
+
"security": { "score": 1-5, "evidence": ["..."] }
|
|
430
|
+
},
|
|
431
|
+
"issues": [{ "category": "sdk|observability|cost|security", "severity": "critical|high|medium|low", "description": "...", "file": "...", "line": 123 }],
|
|
432
|
+
"summary": "Brief summary..."
|
|
433
|
+
}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Call Gemini via AI Gateway */
|
|
437
|
+
async function callGemini(
|
|
438
|
+
prompt: string,
|
|
439
|
+
env: AuditorEnv,
|
|
440
|
+
log: Logger
|
|
441
|
+
): Promise<string | null> {
|
|
442
|
+
const gatewayUrl = `${AI_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/google-ai-studio/v1beta/models/gemini-2.0-flash-lite:generateContent`;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const response = await fetch(gatewayUrl, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: {
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
|
|
450
|
+
'x-goog-api-key': env.GEMINI_API_KEY,
|
|
451
|
+
},
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
454
|
+
generationConfig: {
|
|
455
|
+
temperature: 0.1,
|
|
456
|
+
maxOutputTokens: 4096,
|
|
457
|
+
responseMimeType: 'application/json',
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
const errorBody = await response.text();
|
|
464
|
+
log.error('Gemini API error', new Error(`HTTP ${response.status}`), {
|
|
465
|
+
status: response.status,
|
|
466
|
+
errorBody: errorBody.substring(0, 500),
|
|
467
|
+
});
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const data = await response.json() as {
|
|
472
|
+
candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? null;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
log.error('Gemini fetch failed', error);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// =============================================================================
|
|
483
|
+
// STATUS DETERMINATION
|
|
484
|
+
// =============================================================================
|
|
485
|
+
|
|
486
|
+
function determineStatus(
|
|
487
|
+
config: ConfigCheckResult,
|
|
488
|
+
code: CodeSmellResult,
|
|
489
|
+
runtime: RuntimeCheckResult
|
|
490
|
+
): AuditStatus {
|
|
491
|
+
const hasConfig = config.hasPlatformCache && config.hasPlatformTelemetry;
|
|
492
|
+
const hasCode = code.hasWithFeatureBudget || code.hasTrackedEnv;
|
|
493
|
+
const hasRuntime = runtime.hasRecentHeartbeat;
|
|
494
|
+
|
|
495
|
+
if (hasConfig && hasCode && hasRuntime) return 'HEALTHY';
|
|
496
|
+
if (hasConfig && hasCode && !hasRuntime) return 'ZOMBIE';
|
|
497
|
+
if (hasConfig && !hasCode) return 'BROKEN';
|
|
498
|
+
if (!hasConfig && hasCode) return 'BROKEN';
|
|
499
|
+
if (!hasConfig && !hasCode && hasRuntime) return 'UNTRACKED';
|
|
500
|
+
return 'NOT_INTEGRATED';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function generateStatusMessage(
|
|
504
|
+
status: AuditStatus,
|
|
505
|
+
config: ConfigCheckResult,
|
|
506
|
+
code: CodeSmellResult,
|
|
507
|
+
runtime: RuntimeCheckResult
|
|
508
|
+
): string {
|
|
509
|
+
switch (status) {
|
|
510
|
+
case 'HEALTHY': return 'SDK fully integrated and active';
|
|
511
|
+
case 'ZOMBIE': return `SDK integrated but no heartbeat in ${runtime.hoursSinceHeartbeat ?? '?'}h`;
|
|
512
|
+
case 'BROKEN': return `SDK misconfigured: ${config.issues.join(', ')}`;
|
|
513
|
+
case 'UNTRACKED': return 'Runtime activity detected but no SDK integration';
|
|
514
|
+
case 'NOT_INTEGRATED': return 'No SDK integration detected';
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// =============================================================================
|
|
519
|
+
// BEHAVIORAL ANALYSIS
|
|
520
|
+
// =============================================================================
|
|
521
|
+
|
|
522
|
+
async function runBehavioralAnalysis(
|
|
523
|
+
project: string,
|
|
524
|
+
repo: string,
|
|
525
|
+
sdkPatterns: string[],
|
|
526
|
+
config: AuditTargetsConfig,
|
|
527
|
+
env: AuditorEnv,
|
|
528
|
+
log: Logger
|
|
529
|
+
): Promise<BehavioralResult> {
|
|
530
|
+
const days = config.behavioralAnalysisDays ?? DEFAULT_BEHAVIORAL_ANALYSIS_DAYS;
|
|
531
|
+
const minCommits = config.hotspotMinCommits ?? DEFAULT_HOTSPOT_MIN_COMMITS;
|
|
532
|
+
|
|
533
|
+
const commits = await fetchCommitHistory(repo, env.GITHUB_TOKEN, days, log);
|
|
534
|
+
if (commits.length === 0) {
|
|
535
|
+
return { hotspots: [], regressions: [], analysisWindow: days };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Fetch details for recent commits (limit to 20 for performance)
|
|
539
|
+
const recentCommits = commits.slice(0, 20);
|
|
540
|
+
const commitDetails = new Map<string, GitHubCommit>();
|
|
541
|
+
for (const commit of recentCommits) {
|
|
542
|
+
const details = await fetchCommitDetails(repo, commit.sha, env.GITHUB_TOKEN, log);
|
|
543
|
+
if (details) commitDetails.set(commit.sha, details);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Analyze hotspots
|
|
547
|
+
const fileChanges = new Map<string, { count: number; hasSdkPatterns: boolean }>();
|
|
548
|
+
for (const commit of commits) {
|
|
549
|
+
const details = commitDetails.get(commit.sha);
|
|
550
|
+
if (!details?.files) continue;
|
|
551
|
+
for (const file of details.files) {
|
|
552
|
+
if (!file.filename.match(/\.(ts|tsx|js|jsx)$/)) continue;
|
|
553
|
+
const existing = fileChanges.get(file.filename);
|
|
554
|
+
if (existing) { existing.count++; }
|
|
555
|
+
else {
|
|
556
|
+
const hasSdk = file.filename.includes('platform-sdk') || file.filename.includes('circuit-breaker');
|
|
557
|
+
fileChanges.set(file.filename, { count: 1, hasSdkPatterns: hasSdk });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const hotspots = [...fileChanges.entries()]
|
|
563
|
+
.filter(([, data]) => data.count >= minCommits)
|
|
564
|
+
.map(([filePath, data]) => ({
|
|
565
|
+
filePath,
|
|
566
|
+
changeCount: data.count,
|
|
567
|
+
hasSdkPatterns: data.hasSdkPatterns,
|
|
568
|
+
hotspotScore: data.count * (data.hasSdkPatterns ? 1 : 3),
|
|
569
|
+
}))
|
|
570
|
+
.sort((a, b) => b.hotspotScore - a.hotspotScore)
|
|
571
|
+
.slice(0, 10);
|
|
572
|
+
|
|
573
|
+
// Detect regressions
|
|
574
|
+
const regressions: BehavioralResult['regressions'] = [];
|
|
575
|
+
for (const commit of commits) {
|
|
576
|
+
const details = commitDetails.get(commit.sha);
|
|
577
|
+
if (!details?.files) continue;
|
|
578
|
+
|
|
579
|
+
const allAdded: string[] = [];
|
|
580
|
+
for (const f of details.files) {
|
|
581
|
+
if (!f.patch) continue;
|
|
582
|
+
for (const line of f.patch.split('\n')) {
|
|
583
|
+
if (line.startsWith('+') && !line.startsWith('+++')) allAdded.push(line);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const file of details.files) {
|
|
588
|
+
if (!file.patch || file.status === 'removed') continue;
|
|
589
|
+
const removed = file.patch.split('\n').filter((l) => l.startsWith('-') && !l.startsWith('---'));
|
|
590
|
+
const added = file.patch.split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++'));
|
|
591
|
+
|
|
592
|
+
for (const pattern of sdkPatterns) {
|
|
593
|
+
const removedCount = removed.filter((l) => l.includes(pattern)).length;
|
|
594
|
+
const addedCount = added.filter((l) => l.includes(pattern)).length;
|
|
595
|
+
if (removedCount > addedCount) {
|
|
596
|
+
const addedElsewhere = allAdded.some((l) => l.includes(pattern));
|
|
597
|
+
if (!addedElsewhere) {
|
|
598
|
+
regressions.push({
|
|
599
|
+
commitSha: commit.sha,
|
|
600
|
+
commitMessage: commit.commit.message.split('\n')[0].substring(0, 100),
|
|
601
|
+
removedPattern: pattern,
|
|
602
|
+
filePath: file.filename,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
log.info('Behavioral analysis complete', {
|
|
611
|
+
project, commitsAnalyzed: commits.length,
|
|
612
|
+
hotspotsFound: hotspots.length, regressionsFound: regressions.length,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
return { hotspots, regressions, analysisWindow: days };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// =============================================================================
|
|
619
|
+
// GITHUB HELPERS
|
|
620
|
+
// =============================================================================
|
|
621
|
+
|
|
622
|
+
async function fetchGitHubFile(
|
|
623
|
+
repo: string, path: string, token: string, log: Logger
|
|
624
|
+
): Promise<string | null> {
|
|
625
|
+
try {
|
|
626
|
+
const response = await fetch(`${GITHUB_API_BASE}/repos/${repo}/contents/${path}`, {
|
|
627
|
+
headers: {
|
|
628
|
+
Authorization: `Bearer ${token}`,
|
|
629
|
+
Accept: 'application/vnd.github.v3.raw',
|
|
630
|
+
'User-Agent': 'platform-auditor/1.0',
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
if (response.status !== 404) {
|
|
635
|
+
const body = await response.text();
|
|
636
|
+
log.error('GitHub API error', new Error(`HTTP ${response.status}`), { repo, path, body: body.substring(0, 500) });
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
return await response.text();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
log.error('GitHub fetch failed', error, { repo, path });
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function fetchDeepScanFiles(
|
|
648
|
+
target: AuditTarget, dimensions: AuditDimension[],
|
|
649
|
+
env: AuditorEnv, log: Logger
|
|
650
|
+
): Promise<string> {
|
|
651
|
+
const files = new Set<string>();
|
|
652
|
+
for (const dim of dimensions) {
|
|
653
|
+
for (const f of target.deepScanFiles[dim] || []) files.add(f);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let content = '';
|
|
657
|
+
for (const filePath of files) {
|
|
658
|
+
const fileContent = await fetchGitHubFile(target.repo, filePath, env.GITHUB_TOKEN, log);
|
|
659
|
+
if (fileContent) {
|
|
660
|
+
content += `\n\n## ${filePath}:\n\`\`\`typescript\n${fileContent.slice(0, DEEP_SCAN_FILE_LIMIT)}\n\`\`\``;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (content.length > SUPPLEMENTARY_CONTENT_WARN_LIMIT) {
|
|
665
|
+
log.warn('Deep scan content exceeds safety threshold', {
|
|
666
|
+
repo: target.repo, contentLength: content.length,
|
|
667
|
+
estimatedTokens: Math.round(content.length / 4),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return content;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function fetchCommitHistory(
|
|
675
|
+
repo: string, token: string, days: number, log: Logger
|
|
676
|
+
): Promise<GitHubCommit[]> {
|
|
677
|
+
const since = new Date();
|
|
678
|
+
since.setDate(since.getDate() - days);
|
|
679
|
+
try {
|
|
680
|
+
const response = await fetch(
|
|
681
|
+
`${GITHUB_API_BASE}/repos/${repo}/commits?since=${since.toISOString()}&per_page=100`,
|
|
682
|
+
{ headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'platform-auditor/1.0' } }
|
|
683
|
+
);
|
|
684
|
+
if (!response.ok) { await response.text(); return []; }
|
|
685
|
+
return (await response.json()) as GitHubCommit[];
|
|
686
|
+
} catch { return []; }
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function fetchCommitDetails(
|
|
690
|
+
repo: string, sha: string, token: string, log: Logger
|
|
691
|
+
): Promise<GitHubCommit | null> {
|
|
692
|
+
try {
|
|
693
|
+
const response = await fetch(`${GITHUB_API_BASE}/repos/${repo}/commits/${sha}`, {
|
|
694
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'platform-auditor/1.0' },
|
|
695
|
+
});
|
|
696
|
+
if (!response.ok) { await response.text(); return null; }
|
|
697
|
+
return (await response.json()) as GitHubCommit;
|
|
698
|
+
} catch { return null; }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// =============================================================================
|
|
702
|
+
// UTILITIES
|
|
703
|
+
// =============================================================================
|
|
704
|
+
|
|
705
|
+
function generateAuditId(): string {
|
|
706
|
+
return `audit-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function hashContent(content: string): Promise<string> {
|
|
710
|
+
const encoder = new TextEncoder();
|
|
711
|
+
const data = encoder.encode(content);
|
|
712
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
713
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
714
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function parseJsonc(content: string): WranglerConfig | null {
|
|
718
|
+
try {
|
|
719
|
+
let result = '';
|
|
720
|
+
let i = 0;
|
|
721
|
+
while (i < content.length) {
|
|
722
|
+
if (content[i] === '"') {
|
|
723
|
+
result += content[i++];
|
|
724
|
+
while (i < content.length && content[i] !== '"') {
|
|
725
|
+
if (content[i] === '\\' && i + 1 < content.length) result += content[i++];
|
|
726
|
+
result += content[i++];
|
|
727
|
+
}
|
|
728
|
+
if (i < content.length) result += content[i++];
|
|
729
|
+
} else if (content[i] === '/' && content[i + 1] === '/') {
|
|
730
|
+
while (i < content.length && content[i] !== '\n') i++;
|
|
731
|
+
} else if (content[i] === '/' && content[i + 1] === '*') {
|
|
732
|
+
i += 2;
|
|
733
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++;
|
|
734
|
+
i += 2;
|
|
735
|
+
} else {
|
|
736
|
+
result += content[i++];
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return JSON.parse(result) as WranglerConfig;
|
|
740
|
+
} catch {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// =============================================================================
|
|
746
|
+
// D1 STORAGE
|
|
747
|
+
// =============================================================================
|
|
748
|
+
|
|
749
|
+
async function storeAuditResult(
|
|
750
|
+
auditId: string,
|
|
751
|
+
result: ProjectAuditResult,
|
|
752
|
+
env: AuditorEnv,
|
|
753
|
+
log: Logger
|
|
754
|
+
): Promise<void> {
|
|
755
|
+
try {
|
|
756
|
+
await env.PLATFORM_DB.prepare(
|
|
757
|
+
`INSERT INTO audit_results (
|
|
758
|
+
audit_id, project, status, status_message,
|
|
759
|
+
has_platform_cache, has_platform_telemetry, observability_enabled, logs_enabled, config_issues,
|
|
760
|
+
has_sdk_folder, has_with_feature_budget, has_tracked_env, has_circuit_breaker_error, has_error_logging,
|
|
761
|
+
has_recent_heartbeat, hours_since_heartbeat,
|
|
762
|
+
ai_judge_score, ai_judge_summary, ai_judge_issues, ai_cached_at,
|
|
763
|
+
traces_enabled, trace_sampling_rate, log_sampling_rate, invocation_logs_enabled, source_maps_enabled,
|
|
764
|
+
rubric_sdk, rubric_observability, rubric_cost_protection, rubric_security,
|
|
765
|
+
rubric_evidence, ai_reasoning, ai_categorised_issues, ai_validation_retries,
|
|
766
|
+
scan_type, focused_dimensions, composite_score
|
|
767
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
768
|
+
).bind(
|
|
769
|
+
auditId, result.project, result.status, result.statusMessage,
|
|
770
|
+
result.config.hasPlatformCache ? 1 : 0,
|
|
771
|
+
result.config.hasPlatformTelemetry ? 1 : 0,
|
|
772
|
+
result.config.observabilityEnabled ? 1 : 0,
|
|
773
|
+
result.config.logsEnabled ? 1 : 0,
|
|
774
|
+
JSON.stringify(result.config.issues),
|
|
775
|
+
result.codeSmells.hasSdkFolder ? 1 : 0,
|
|
776
|
+
result.codeSmells.hasWithFeatureBudget ? 1 : 0,
|
|
777
|
+
result.codeSmells.hasTrackedEnv ? 1 : 0,
|
|
778
|
+
result.codeSmells.hasCircuitBreakerError ? 1 : 0,
|
|
779
|
+
result.codeSmells.hasErrorLogging ? 1 : 0,
|
|
780
|
+
result.runtime.hasRecentHeartbeat ? 1 : 0,
|
|
781
|
+
result.runtime.hoursSinceHeartbeat,
|
|
782
|
+
result.aiJudge?.score ?? null,
|
|
783
|
+
result.aiJudge?.summary ?? null,
|
|
784
|
+
result.aiJudge?.issues ?? null,
|
|
785
|
+
result.aiJudge?.cachedAt ?? null,
|
|
786
|
+
result.config.tracesEnabled ? 1 : 0,
|
|
787
|
+
result.config.traceSamplingRate,
|
|
788
|
+
result.config.logSamplingRate,
|
|
789
|
+
result.config.invocationLogsEnabled ? 1 : 0,
|
|
790
|
+
result.config.sourceMapsEnabled ? 1 : 0,
|
|
791
|
+
result.aiJudge?.rubricScores.sdk ?? null,
|
|
792
|
+
result.aiJudge?.rubricScores.observability ?? null,
|
|
793
|
+
result.aiJudge?.rubricScores.costProtection ?? null,
|
|
794
|
+
result.aiJudge?.rubricScores.security ?? null,
|
|
795
|
+
result.aiJudge?.rubricEvidence ?? null,
|
|
796
|
+
result.aiJudge?.reasoning ?? null,
|
|
797
|
+
result.aiJudge?.categorisedIssues ?? null,
|
|
798
|
+
result.aiJudge?.validationRetries ?? 0,
|
|
799
|
+
result.scanType,
|
|
800
|
+
result.focusedDimensions ? JSON.stringify(result.focusedDimensions) : null,
|
|
801
|
+
result.compositeScore
|
|
802
|
+
).run();
|
|
803
|
+
|
|
804
|
+
log.debug('Stored audit result', { project: result.project, auditId });
|
|
805
|
+
} catch (error) {
|
|
806
|
+
log.error('Failed to store audit result', error, { project: result.project });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function storeBehavioralResults(
|
|
811
|
+
project: string,
|
|
812
|
+
result: BehavioralResult,
|
|
813
|
+
env: AuditorEnv,
|
|
814
|
+
log: Logger
|
|
815
|
+
): Promise<void> {
|
|
816
|
+
const auditDate = new Date().toISOString().split('T')[0];
|
|
817
|
+
try {
|
|
818
|
+
for (const hotspot of result.hotspots) {
|
|
819
|
+
await env.PLATFORM_DB.prepare(
|
|
820
|
+
`INSERT OR REPLACE INTO audit_file_hotspots (
|
|
821
|
+
project, file_path, change_count, has_sdk_patterns, hotspot_score, audit_date
|
|
822
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
823
|
+
).bind(project, hotspot.filePath, hotspot.changeCount, hotspot.hasSdkPatterns ? 1 : 0, hotspot.hotspotScore, auditDate).run();
|
|
824
|
+
}
|
|
825
|
+
for (const reg of result.regressions) {
|
|
826
|
+
await env.PLATFORM_DB.prepare(
|
|
827
|
+
`INSERT OR IGNORE INTO audit_sdk_regressions (
|
|
828
|
+
project, commit_sha, commit_message, file_path, regression_type, audit_date
|
|
829
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
830
|
+
).bind(project, reg.commitSha, reg.commitMessage, reg.filePath, `${reg.removedPattern}_removed`, auditDate).run();
|
|
831
|
+
}
|
|
832
|
+
} catch (error) {
|
|
833
|
+
log.error('Failed to store behavioral results', error, { project });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// =============================================================================
|
|
838
|
+
// ALERT SENDING
|
|
839
|
+
// =============================================================================
|
|
840
|
+
|
|
841
|
+
async function sendAlerts(
|
|
842
|
+
auditId: string,
|
|
843
|
+
results: ProjectAuditResult[],
|
|
844
|
+
env: AuditorEnv,
|
|
845
|
+
log: Logger
|
|
846
|
+
): Promise<void> {
|
|
847
|
+
const unhealthy = results.filter((r) => r.status !== 'HEALTHY');
|
|
848
|
+
if (unhealthy.length === 0) return;
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
await env.ALERT_ROUTER.fetch('https://internal/alerts', {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
headers: { 'Content-Type': 'application/json' },
|
|
854
|
+
body: JSON.stringify({
|
|
855
|
+
source: 'platform-auditor',
|
|
856
|
+
auditId,
|
|
857
|
+
alerts: unhealthy.map((r) => ({
|
|
858
|
+
project: r.project,
|
|
859
|
+
status: r.status,
|
|
860
|
+
message: r.statusMessage,
|
|
861
|
+
score: r.compositeScore,
|
|
862
|
+
})),
|
|
863
|
+
}),
|
|
864
|
+
});
|
|
865
|
+
} catch (error) {
|
|
866
|
+
log.warn('Failed to send alerts (non-fatal)', error);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// =============================================================================
|
|
871
|
+
// MAIN WORKER
|
|
872
|
+
// =============================================================================
|
|
873
|
+
|
|
874
|
+
export default {
|
|
875
|
+
async scheduled(event: ScheduledEvent, env: AuditorEnv, ctx: ExecutionContext): Promise<void> {
|
|
876
|
+
const log = createLogger({ worker: 'platform-auditor', featureId: FEATURE_AUDITOR });
|
|
877
|
+
const scanType = determineScanType(event.scheduledTime);
|
|
878
|
+
log.info('Audit triggered', { scan_type: scanType });
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
const trackedEnv = withCronBudget(env, FEATURE_AUDITOR, {
|
|
882
|
+
ctx, cronExpression: '0 0 * * 0,3',
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const config = await loadAuditConfig(env, log);
|
|
886
|
+
if (Object.keys(config.targets).length === 0) {
|
|
887
|
+
log.warn('No audit targets configured — add targets to config/audit-targets.yaml and sync');
|
|
888
|
+
await completeTracking(trackedEnv);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const auditId = generateAuditId();
|
|
893
|
+
const results: ProjectAuditResult[] = [];
|
|
894
|
+
|
|
895
|
+
for (const [project, target] of Object.entries(config.targets)) {
|
|
896
|
+
let focusedDimensions: AuditDimension[] | null = null;
|
|
897
|
+
if (scanType === 'focused') {
|
|
898
|
+
const { dimensions, previousScore } = await getWeakDimensions(
|
|
899
|
+
project, config.focusedScanThreshold, env, log
|
|
900
|
+
);
|
|
901
|
+
if (previousScore >= config.focusedScanThreshold && dimensions.length === 0) {
|
|
902
|
+
log.info('Skipping project (above threshold)', { project, previousScore });
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
focusedDimensions = dimensions;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
// Layer 1: Config
|
|
910
|
+
const wranglerContent = await fetchGitHubFile(target.repo, target.wranglerPath, env.GITHUB_TOKEN, log);
|
|
911
|
+
const wranglerConfig = wranglerContent ? parseJsonc(wranglerContent) : null;
|
|
912
|
+
const configResult = checkConfig(wranglerConfig);
|
|
913
|
+
|
|
914
|
+
// Layer 2: Code smells
|
|
915
|
+
let entryPointContent = await fetchGitHubFile(target.repo, target.entryPoint, env.GITHUB_TOKEN, log);
|
|
916
|
+
if (!entryPointContent && target.entryPointFallback) {
|
|
917
|
+
entryPointContent = await fetchGitHubFile(target.repo, target.entryPointFallback, env.GITHUB_TOKEN, log);
|
|
918
|
+
}
|
|
919
|
+
const codeResult = checkCodeSmells(entryPointContent, config.sdkPatterns);
|
|
920
|
+
|
|
921
|
+
// Layer 3: Runtime
|
|
922
|
+
const runtimeResult = await checkRuntime(project, config.heartbeatFreshnessHours, env, log);
|
|
923
|
+
|
|
924
|
+
// Layer 4: AI Judge
|
|
925
|
+
let aiJudge: AIJudgeResult | null = null;
|
|
926
|
+
if (env.PLATFORM_AI_GATEWAY_KEY && entryPointContent) {
|
|
927
|
+
const staticChecks = {
|
|
928
|
+
hasPlatformCache: configResult.hasPlatformCache,
|
|
929
|
+
hasPlatformTelemetry: configResult.hasPlatformTelemetry,
|
|
930
|
+
observabilityEnabled: configResult.observabilityEnabled,
|
|
931
|
+
hasFeatureBudget: codeResult.hasWithFeatureBudget,
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
let supplementary = '';
|
|
935
|
+
const dims = scanType === 'full'
|
|
936
|
+
? (['sdk', 'observability', 'cost', 'security'] as AuditDimension[])
|
|
937
|
+
: (focusedDimensions ?? []);
|
|
938
|
+
if (dims.length > 0) {
|
|
939
|
+
supplementary = await fetchDeepScanFiles(target, dims, env, log);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
aiJudge = await auditWithGemini(
|
|
943
|
+
project, entryPointContent, wranglerContent, staticChecks, supplementary, config, env, log
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Determine status
|
|
948
|
+
const status = determineStatus(configResult, codeResult, runtimeResult);
|
|
949
|
+
const statusMessage = generateStatusMessage(status, configResult, codeResult, runtimeResult);
|
|
950
|
+
const compositeScore = aiJudge?.score ?? 0;
|
|
951
|
+
|
|
952
|
+
// Behavioral analysis
|
|
953
|
+
let behavioral: BehavioralResult | null = null;
|
|
954
|
+
try {
|
|
955
|
+
behavioral = await runBehavioralAnalysis(project, target.repo, config.sdkPatterns, config, env, log);
|
|
956
|
+
await storeBehavioralResults(project, behavioral, env, log);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
log.warn('Behavioral analysis failed (non-fatal)', error, { project });
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const auditResult: ProjectAuditResult = {
|
|
962
|
+
project, auditId, status, statusMessage, scanType,
|
|
963
|
+
focusedDimensions, config: configResult, codeSmells: codeResult,
|
|
964
|
+
runtime: runtimeResult, aiJudge: aiJudge, behavioral,
|
|
965
|
+
compositeScore, timestamp: new Date().toISOString(),
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
results.push(auditResult);
|
|
969
|
+
await storeAuditResult(auditId, auditResult, env, log);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
log.error('Project audit failed', error, { project });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Store latest in KV
|
|
976
|
+
await env.PLATFORM_CACHE.put('AUDIT:LATEST', JSON.stringify({
|
|
977
|
+
auditId, results, scanType, timestamp: new Date().toISOString(),
|
|
978
|
+
}), { expirationTtl: 30 * 24 * 60 * 60 });
|
|
979
|
+
|
|
980
|
+
// Feature coverage + comprehensive report (full scans only)
|
|
981
|
+
if (scanType === 'full') {
|
|
982
|
+
let featureCoverage: FeatureCoverageReport | null = null;
|
|
983
|
+
try {
|
|
984
|
+
featureCoverage = await runFeatureCoverageAudit(env, log);
|
|
985
|
+
await storeFeatureCoverageAudit(env, featureCoverage, log);
|
|
986
|
+
} catch (error) { log.warn('Feature coverage audit failed (non-fatal)', error); }
|
|
987
|
+
|
|
988
|
+
try {
|
|
989
|
+
const report = await generateComprehensiveReport(env, featureCoverage, log);
|
|
990
|
+
await storeComprehensiveReport(env, report, log);
|
|
991
|
+
} catch (error) { log.warn('Comprehensive report failed (non-fatal)', error); }
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
await sendAlerts(auditId, results, env, log);
|
|
995
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
996
|
+
await health(FEATURE_HEARTBEAT, env.PLATFORM_CACHE as any, env.PLATFORM_TELEMETRY, ctx);
|
|
997
|
+
await completeTracking(trackedEnv);
|
|
998
|
+
pingHeartbeat(ctx, env.GATUS_HEARTBEAT_URL, env.GATUS_TOKEN, true);
|
|
999
|
+
|
|
1000
|
+
log.info('Audit complete', {
|
|
1001
|
+
auditId, scanType, totalProjects: results.length,
|
|
1002
|
+
healthy: results.filter((r) => r.status === 'HEALTHY').length,
|
|
1003
|
+
});
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if (error instanceof CircuitBreakerError) {
|
|
1006
|
+
log.warn('Circuit breaker STOP', error);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
pingHeartbeat(ctx, env.GATUS_HEARTBEAT_URL, env.GATUS_TOKEN, false);
|
|
1010
|
+
log.error('Audit failed', error);
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
async fetch(request: Request, env: AuditorEnv, ctx: ExecutionContext): Promise<Response> {
|
|
1015
|
+
const url = new URL(request.url);
|
|
1016
|
+
|
|
1017
|
+
if (url.pathname === '/health') {
|
|
1018
|
+
return Response.json({ status: 'ok', service: 'platform-auditor', timestamp: new Date().toISOString() });
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const traceContext = createTraceContext(request, env);
|
|
1022
|
+
const log = createLoggerFromRequest(request, env, 'platform-auditor', FEATURE_AUDITOR);
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
const trackedEnv = withFeatureBudget(env, FEATURE_AUDITOR, { ctx });
|
|
1026
|
+
|
|
1027
|
+
if (url.pathname === '/audit' && request.method === 'GET') {
|
|
1028
|
+
const requestedType = url.searchParams.get('type') || 'full';
|
|
1029
|
+
const fakeDay = requestedType === 'focused' ? 3 : 0;
|
|
1030
|
+
const fakeDate = new Date();
|
|
1031
|
+
fakeDate.setUTCDate(fakeDate.getUTCDate() + ((fakeDay - fakeDate.getUTCDay() + 7) % 7));
|
|
1032
|
+
await this.scheduled({ scheduledTime: fakeDate.getTime(), cron: '0 0 * * 0,3', noRetry: () => {} } as unknown as ScheduledEvent, env, ctx);
|
|
1033
|
+
await completeTracking(trackedEnv);
|
|
1034
|
+
return Response.json({ status: 'audit_triggered', scan_type: requestedType });
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (url.pathname === '/audit/latest') {
|
|
1038
|
+
const latest = await env.PLATFORM_CACHE.get('AUDIT:LATEST');
|
|
1039
|
+
await completeTracking(trackedEnv);
|
|
1040
|
+
if (!latest) return Response.json({ error: 'No audit results found' }, { status: 404 });
|
|
1041
|
+
return Response.json(JSON.parse(latest));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (url.pathname.startsWith('/audit/latest/')) {
|
|
1045
|
+
const project = url.pathname.split('/').pop();
|
|
1046
|
+
const latest = await env.PLATFORM_CACHE.get('AUDIT:LATEST');
|
|
1047
|
+
await completeTracking(trackedEnv);
|
|
1048
|
+
if (!latest) return Response.json({ error: 'No audit results' }, { status: 404 });
|
|
1049
|
+
const parsed = JSON.parse(latest) as { auditId: string; results: ProjectAuditResult[] };
|
|
1050
|
+
const result = parsed.results.find((r) => r.project === project);
|
|
1051
|
+
if (!result) return Response.json({ error: `Project ${project} not found` }, { status: 404 });
|
|
1052
|
+
return Response.json({ auditId: parsed.auditId, result });
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
await completeTracking(trackedEnv);
|
|
1056
|
+
return Response.json({
|
|
1057
|
+
service: 'platform-auditor',
|
|
1058
|
+
endpoints: [
|
|
1059
|
+
'GET /health', 'GET /audit?type=full|focused',
|
|
1060
|
+
'GET /audit/latest', 'GET /audit/latest/:project',
|
|
1061
|
+
],
|
|
1062
|
+
});
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
if (error instanceof CircuitBreakerError) {
|
|
1065
|
+
return Response.json({ error: 'Service temporarily unavailable', code: 'CIRCUIT_BREAKER' }, { status: 503, headers: { 'Retry-After': '60' } });
|
|
1066
|
+
}
|
|
1067
|
+
log.error('Request failed', error, { path: url.pathname, traceId: traceContext.traceId });
|
|
1068
|
+
return Response.json({ error: 'Internal server error', traceId: traceContext.traceId }, { status: 500 });
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
};
|