aura-security 0.5.0 → 0.5.2

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.
@@ -38,6 +38,11 @@ export declare class AuraServer {
38
38
  private handleGetNotifications;
39
39
  private handleTestNotification;
40
40
  private handleSendNotification;
41
+ private handleGetScore;
42
+ private handleGetScoreHistory;
43
+ private handleGetScoreTrend;
44
+ private handleGetBadge;
45
+ private handleGetBadgeForTarget;
41
46
  private readBody;
42
47
  start(): Promise<void>;
43
48
  stop(): Promise<void>;
@@ -3,6 +3,7 @@
3
3
  import { createServer } from 'http';
4
4
  import { getDatabase } from '../database/index.js';
5
5
  import { NotificationService, createNotificationFromAudit } from '../integrations/notifications.js';
6
+ import { generateScoreBadge } from '../scoring/index.js';
6
7
  export class AuraServer {
7
8
  server = null;
8
9
  tools = new Map();
@@ -98,6 +99,26 @@ export class AuraServer {
98
99
  else if (path === '/notifications/send' && req.method === 'POST') {
99
100
  await this.handleSendNotification(req, res);
100
101
  }
102
+ // Score endpoints
103
+ else if (path === '/score' && req.method === 'GET') {
104
+ await this.handleGetScore(url, res);
105
+ }
106
+ else if (path.match(/^\/score\/(.+)\/history$/) && req.method === 'GET') {
107
+ const target = decodeURIComponent(path.slice(7, -8));
108
+ await this.handleGetScoreHistory(target, url, res);
109
+ }
110
+ else if (path.match(/^\/score\/(.+)\/trend$/) && req.method === 'GET') {
111
+ const target = decodeURIComponent(path.slice(7, -6));
112
+ await this.handleGetScoreTrend(target, url, res);
113
+ }
114
+ // Badge endpoints
115
+ else if (path === '/badge/score' && req.method === 'GET') {
116
+ await this.handleGetBadge(res);
117
+ }
118
+ else if (path.startsWith('/badge/') && req.method === 'GET') {
119
+ const target = decodeURIComponent(path.slice(7));
120
+ await this.handleGetBadgeForTarget(target, res);
121
+ }
101
122
  else {
102
123
  res.statusCode = 404;
103
124
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -118,8 +139,8 @@ export class AuraServer {
118
139
  res.statusCode = 200;
119
140
  res.end(JSON.stringify({
120
141
  name: 'aura-security',
121
- version: '0.2.0',
122
- endpoints: ['/info', '/tools', '/memory', '/settings', '/audits', '/stats', '/notifications'],
142
+ version: '0.5.2',
143
+ endpoints: ['/info', '/tools', '/memory', '/settings', '/audits', '/stats', '/notifications', '/score', '/badge'],
123
144
  tools: Array.from(this.tools.keys()),
124
145
  database: true
125
146
  }));
@@ -305,6 +326,93 @@ export class AuraServer {
305
326
  res.statusCode = 200;
306
327
  res.end(JSON.stringify(result));
307
328
  }
329
+ // ============ SCORE ENDPOINTS ============
330
+ async handleGetScore(url, res) {
331
+ const target = url.searchParams.get('target') || undefined;
332
+ if (target) {
333
+ // Get score for specific target
334
+ const latest = this.db.getLatestScore(target);
335
+ const trend = this.db.getScoreTrend(target, 10);
336
+ if (!latest) {
337
+ res.statusCode = 404;
338
+ res.end(JSON.stringify({ error: 'No score history for target' }));
339
+ return;
340
+ }
341
+ res.statusCode = 200;
342
+ res.end(JSON.stringify({
343
+ score: latest.score,
344
+ grade: latest.grade,
345
+ target: latest.target,
346
+ breakdown: {
347
+ critical: latest.critical,
348
+ high: latest.high,
349
+ medium: latest.medium,
350
+ low: latest.low
351
+ },
352
+ trend,
353
+ lastUpdated: latest.timestamp
354
+ }));
355
+ }
356
+ else {
357
+ // Get aggregate score
358
+ const aggregate = this.db.getAggregateScore();
359
+ res.statusCode = 200;
360
+ res.end(JSON.stringify({
361
+ score: aggregate.score,
362
+ grade: aggregate.grade,
363
+ gradeColor: aggregate.gradeColor,
364
+ breakdown: aggregate.breakdown,
365
+ trend: aggregate.trend,
366
+ lastUpdated: new Date().toISOString()
367
+ }));
368
+ }
369
+ }
370
+ async handleGetScoreHistory(target, url, res) {
371
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
372
+ const history = this.db.getScoreHistory(target, limit);
373
+ res.statusCode = 200;
374
+ res.end(JSON.stringify({ target, history }));
375
+ }
376
+ async handleGetScoreTrend(target, url, res) {
377
+ const limit = parseInt(url.searchParams.get('limit') || '10', 10);
378
+ const trend = this.db.getScoreTrend(target, limit);
379
+ res.statusCode = 200;
380
+ res.end(JSON.stringify({ target, ...trend }));
381
+ }
382
+ // ============ BADGE ENDPOINTS ============
383
+ async handleGetBadge(res) {
384
+ const aggregate = this.db.getAggregateScore();
385
+ const svg = generateScoreBadge(aggregate.score, aggregate.grade, aggregate.gradeColor);
386
+ res.setHeader('Content-Type', 'image/svg+xml');
387
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
388
+ res.statusCode = 200;
389
+ res.end(svg);
390
+ }
391
+ async handleGetBadgeForTarget(target, res) {
392
+ const latest = this.db.getLatestScore(target);
393
+ if (!latest) {
394
+ // Return a "no data" badge
395
+ const svg = generateScoreBadge(0, '?', '#6e7681');
396
+ res.setHeader('Content-Type', 'image/svg+xml');
397
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
398
+ res.statusCode = 200;
399
+ res.end(svg);
400
+ return;
401
+ }
402
+ // Get grade color based on score
403
+ let gradeColor = '#f85149'; // F - red
404
+ if (latest.score >= 90)
405
+ gradeColor = '#3fb950'; // A - green
406
+ else if (latest.score >= 70)
407
+ gradeColor = '#58a6ff'; // B - blue
408
+ else if (latest.score >= 50)
409
+ gradeColor = '#d29922'; // C - yellow
410
+ const svg = generateScoreBadge(latest.score, latest.grade, gradeColor);
411
+ res.setHeader('Content-Type', 'image/svg+xml');
412
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
413
+ res.statusCode = 200;
414
+ res.end(svg);
415
+ }
308
416
  readBody(req) {
309
417
  return new Promise((resolve, reject) => {
310
418
  const chunks = [];
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import { existsSync, writeFileSync, mkdirSync } from 'fs';
24
24
  import { join, resolve, basename } from 'path';
25
25
  import { spawnSync } from 'child_process';
26
26
  const AURA_URL = process.env.AURA_URL ?? 'http://127.0.0.1:3000';
27
- const VERSION = '0.5.0';
27
+ const VERSION = '0.5.1';
28
28
  // ANSI colors for terminal output
29
29
  const colors = {
30
30
  reset: '\x1b[0m',
@@ -10,6 +10,7 @@
10
10
  import type { AuditorOutput } from '../types/events.js';
11
11
  import type { LocalScanResult } from '../integrations/local-scanner.js';
12
12
  import type { AWSScanResult } from '../integrations/aws-scanner.js';
13
+ import { type SecurityScore, type ScoreHistoryEntry, type ScoreTrend } from '../scoring/index.js';
13
14
  export interface AuditRecord {
14
15
  id: string;
15
16
  type: 'code' | 'aws' | 'audit';
@@ -69,6 +70,18 @@ export declare class AuditorDatabase {
69
70
  low: number;
70
71
  };
71
72
  };
73
+ saveScore(target: string, auditId: string, counts: {
74
+ critical: number;
75
+ high: number;
76
+ medium: number;
77
+ low: number;
78
+ }): SecurityScore;
79
+ getScoreHistory(target?: string, limit?: number): ScoreHistoryEntry[];
80
+ getLatestScore(target?: string): ScoreHistoryEntry | null;
81
+ getScoreTrend(target?: string, limit?: number): ScoreTrend;
82
+ getAggregateScore(): SecurityScore & {
83
+ trend: ScoreTrend;
84
+ };
72
85
  close(): void;
73
86
  vacuum(): void;
74
87
  deleteOldAudits(daysToKeep?: number): number;
@@ -10,6 +10,7 @@
10
10
  import Database from 'better-sqlite3';
11
11
  import { join } from 'path';
12
12
  import { existsSync, mkdirSync } from 'fs';
13
+ import { calculateSecurityScore } from '../scoring/index.js';
13
14
  // ============ DEFAULT SETTINGS ============
14
15
  const DEFAULT_SETTINGS = {
15
16
  // AWS Settings
@@ -105,12 +106,30 @@ export class AuditorDatabase {
105
106
  error TEXT,
106
107
  FOREIGN KEY (audit_id) REFERENCES audits(id)
107
108
  )
109
+ `);
110
+ // Score history table
111
+ this.db.exec(`
112
+ CREATE TABLE IF NOT EXISTS score_history (
113
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ target TEXT NOT NULL,
115
+ audit_id TEXT NOT NULL,
116
+ score REAL NOT NULL,
117
+ grade TEXT NOT NULL,
118
+ critical INTEGER DEFAULT 0,
119
+ high INTEGER DEFAULT 0,
120
+ medium INTEGER DEFAULT 0,
121
+ low INTEGER DEFAULT 0,
122
+ timestamp TEXT NOT NULL,
123
+ FOREIGN KEY (audit_id) REFERENCES audits(id)
124
+ )
108
125
  `);
109
126
  // Create indexes
110
127
  this.db.exec(`
111
128
  CREATE INDEX IF NOT EXISTS idx_audits_timestamp ON audits(timestamp DESC);
112
129
  CREATE INDEX IF NOT EXISTS idx_audits_type ON audits(type);
113
130
  CREATE INDEX IF NOT EXISTS idx_notifications_audit ON notifications(audit_id);
131
+ CREATE INDEX IF NOT EXISTS idx_score_history_target ON score_history(target);
132
+ CREATE INDEX IF NOT EXISTS idx_score_history_timestamp ON score_history(timestamp DESC);
114
133
  `);
115
134
  // Initialize default settings
116
135
  const insertSetting = this.db.prepare(`
@@ -364,6 +383,121 @@ export class AuditorDatabase {
364
383
  },
365
384
  };
366
385
  }
386
+ // ============ SCORE METHODS ============
387
+ saveScore(target, auditId, counts) {
388
+ const score = calculateSecurityScore(counts);
389
+ const timestamp = new Date().toISOString();
390
+ const stmt = this.db.prepare(`
391
+ INSERT INTO score_history (target, audit_id, score, grade, critical, high, medium, low, timestamp)
392
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
393
+ `);
394
+ stmt.run(target, auditId, score.score, score.grade, counts.critical, counts.high, counts.medium, counts.low, timestamp);
395
+ console.log(`[DB] Saved score: ${score.score} (${score.grade}) for ${target}`);
396
+ return score;
397
+ }
398
+ getScoreHistory(target, limit = 50) {
399
+ let query = `
400
+ SELECT id, target, audit_id, score, grade, critical, high, medium, low, timestamp
401
+ FROM score_history
402
+ `;
403
+ const params = [];
404
+ if (target) {
405
+ query += ' WHERE target = ?';
406
+ params.push(target);
407
+ }
408
+ query += ' ORDER BY timestamp DESC LIMIT ?';
409
+ params.push(limit);
410
+ const stmt = this.db.prepare(query);
411
+ const rows = stmt.all(...params);
412
+ return rows.map(row => ({
413
+ id: row.id,
414
+ target: row.target,
415
+ auditId: row.audit_id,
416
+ score: row.score,
417
+ grade: row.grade,
418
+ critical: row.critical,
419
+ high: row.high,
420
+ medium: row.medium,
421
+ low: row.low,
422
+ timestamp: row.timestamp,
423
+ }));
424
+ }
425
+ getLatestScore(target) {
426
+ let query = `
427
+ SELECT id, target, audit_id, score, grade, critical, high, medium, low, timestamp
428
+ FROM score_history
429
+ `;
430
+ const params = [];
431
+ if (target) {
432
+ query += ' WHERE target = ?';
433
+ params.push(target);
434
+ }
435
+ query += ' ORDER BY timestamp DESC LIMIT 1';
436
+ const stmt = this.db.prepare(query);
437
+ const row = stmt.get(...params);
438
+ if (!row)
439
+ return null;
440
+ return {
441
+ id: row.id,
442
+ target: row.target,
443
+ auditId: row.audit_id,
444
+ score: row.score,
445
+ grade: row.grade,
446
+ critical: row.critical,
447
+ high: row.high,
448
+ medium: row.medium,
449
+ low: row.low,
450
+ timestamp: row.timestamp,
451
+ };
452
+ }
453
+ getScoreTrend(target, limit = 10) {
454
+ const history = this.getScoreHistory(target, limit);
455
+ if (history.length === 0) {
456
+ return {
457
+ currentScore: 100,
458
+ previousScore: null,
459
+ change: 0,
460
+ direction: 'same',
461
+ history: []
462
+ };
463
+ }
464
+ const current = history[0];
465
+ const previous = history.length > 1 ? history[1] : null;
466
+ const change = previous ? current.score - previous.score : 0;
467
+ let direction = 'same';
468
+ if (change > 0)
469
+ direction = 'up';
470
+ else if (change < 0)
471
+ direction = 'down';
472
+ return {
473
+ currentScore: current.score,
474
+ previousScore: previous?.score || null,
475
+ change: Math.abs(change),
476
+ direction,
477
+ history: history.map(h => ({ timestamp: h.timestamp, score: h.score }))
478
+ };
479
+ }
480
+ getAggregateScore() {
481
+ // Get totals from all audits
482
+ const stmt = this.db.prepare(`
483
+ SELECT
484
+ SUM(critical) as critical,
485
+ SUM(high) as high,
486
+ SUM(medium) as medium,
487
+ SUM(low) as low
488
+ FROM audits
489
+ `);
490
+ const row = stmt.get();
491
+ const counts = {
492
+ critical: row.critical || 0,
493
+ high: row.high || 0,
494
+ medium: row.medium || 0,
495
+ low: row.low || 0
496
+ };
497
+ const score = calculateSecurityScore(counts);
498
+ const trend = this.getScoreTrend(undefined, 10);
499
+ return { ...score, trend };
500
+ }
367
501
  // ============ CLEANUP ============
368
502
  close() {
369
503
  this.db.close();
package/dist/index.js CHANGED
@@ -498,6 +498,17 @@ async function main() {
498
498
  const db = server.getDatabase();
499
499
  auditId = db.saveAudit('code', scanResult.path, scanResult);
500
500
  console.log(`[AURA] Scan result saved to database: ${auditId}`);
501
+ // Calculate and save security score
502
+ const scoreCounts = {
503
+ critical: scanResult.secrets?.filter((s) => s.severity === 'critical').length || 0,
504
+ high: scanResult.secrets?.filter((s) => s.severity === 'high').length || 0,
505
+ medium: (scanResult.packages?.filter((p) => p.severity === 'medium').length || 0) +
506
+ (scanResult.sastFindings?.length || 0),
507
+ low: (scanResult.packages?.filter((p) => p.severity === 'low').length || 0) +
508
+ (scanResult.envFiles?.length || 0)
509
+ };
510
+ const score = db.saveScore(scanResult.path, auditId, scoreCounts);
511
+ console.log(`[AURA] Security score: ${score.score} (${score.grade})`);
501
512
  }
502
513
  catch (dbErr) {
503
514
  console.error('[AURA] Failed to save to database:', dbErr);
@@ -618,6 +629,15 @@ async function main() {
618
629
  agents: result.aura.agents
619
630
  });
620
631
  console.log(`[AURA] Aura scan saved to database: ${auditId}`);
632
+ // Save security score
633
+ const scoreCounts = {
634
+ critical: result.aura.summary.bySeverity['critical'] || 0,
635
+ high: result.aura.summary.bySeverity['high'] || 0,
636
+ medium: result.aura.summary.bySeverity['medium'] || 0,
637
+ low: result.aura.summary.bySeverity['low'] || 0
638
+ };
639
+ const score = db.saveScore(targetPath, auditId, scoreCounts);
640
+ console.log(`[AURA] Security score: ${score.score} (${score.grade})`);
621
641
  }
622
642
  catch (dbErr) {
623
643
  console.error('[AURA] Database save error:', dbErr);
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Security Score Calculator
3
+ *
4
+ * Calculates a 0-100 security score based on findings severity.
5
+ * Uses a penalty-based diminishing returns formula to prevent negative scores.
6
+ */
7
+ export interface FindingCounts {
8
+ critical: number;
9
+ high: number;
10
+ medium: number;
11
+ low: number;
12
+ }
13
+ export interface SecurityScore {
14
+ score: number;
15
+ grade: string;
16
+ gradeColor: string;
17
+ breakdown: {
18
+ critical: {
19
+ count: number;
20
+ penalty: number;
21
+ };
22
+ high: {
23
+ count: number;
24
+ penalty: number;
25
+ };
26
+ medium: {
27
+ count: number;
28
+ penalty: number;
29
+ };
30
+ low: {
31
+ count: number;
32
+ penalty: number;
33
+ };
34
+ totalPenalty: number;
35
+ };
36
+ }
37
+ export interface ScoreHistoryEntry {
38
+ id: number;
39
+ target: string;
40
+ auditId: string;
41
+ score: number;
42
+ grade: string;
43
+ critical: number;
44
+ high: number;
45
+ medium: number;
46
+ low: number;
47
+ timestamp: string;
48
+ }
49
+ export interface ScoreTrend {
50
+ currentScore: number;
51
+ previousScore: number | null;
52
+ change: number;
53
+ direction: 'up' | 'down' | 'same';
54
+ history: Array<{
55
+ timestamp: string;
56
+ score: number;
57
+ }>;
58
+ }
59
+ /**
60
+ * Calculate security score from finding counts
61
+ */
62
+ export declare function calculateSecurityScore(counts: FindingCounts): SecurityScore;
63
+ /**
64
+ * Calculate trend from score history
65
+ */
66
+ export declare function calculateTrend(history: ScoreHistoryEntry[]): ScoreTrend;
67
+ /**
68
+ * Generate SVG badge for security score
69
+ */
70
+ export declare function generateScoreBadge(score: number, grade: string, gradeColor: string): string;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Security Score Calculator
3
+ *
4
+ * Calculates a 0-100 security score based on findings severity.
5
+ * Uses a penalty-based diminishing returns formula to prevent negative scores.
6
+ */
7
+ // Scoring weights
8
+ const WEIGHTS = {
9
+ critical: 15,
10
+ high: 8,
11
+ medium: 3,
12
+ low: 1
13
+ };
14
+ // Grade thresholds
15
+ const GRADES = [
16
+ { min: 90, grade: 'A', color: '#3fb950' },
17
+ { min: 70, grade: 'B', color: '#58a6ff' },
18
+ { min: 50, grade: 'C', color: '#d29922' },
19
+ { min: 0, grade: 'F', color: '#f85149' }
20
+ ];
21
+ /**
22
+ * Calculate security score from finding counts
23
+ */
24
+ export function calculateSecurityScore(counts) {
25
+ const critical = counts.critical || 0;
26
+ const high = counts.high || 0;
27
+ const medium = counts.medium || 0;
28
+ const low = counts.low || 0;
29
+ // Calculate penalties
30
+ const criticalPenalty = critical * WEIGHTS.critical;
31
+ const highPenalty = high * WEIGHTS.high;
32
+ const mediumPenalty = medium * WEIGHTS.medium;
33
+ const lowPenalty = low * WEIGHTS.low;
34
+ const totalPenalty = criticalPenalty + highPenalty + mediumPenalty + lowPenalty;
35
+ // Diminishing returns formula: score = 100 / (1 + penalty/100)
36
+ // This ensures score stays between 0-100 and degrades smoothly
37
+ const score = Math.round(100 / (1 + totalPenalty / 100));
38
+ // Get grade
39
+ const gradeInfo = GRADES.find(g => score >= g.min) || GRADES[GRADES.length - 1];
40
+ return {
41
+ score,
42
+ grade: gradeInfo.grade,
43
+ gradeColor: gradeInfo.color,
44
+ breakdown: {
45
+ critical: { count: critical, penalty: criticalPenalty },
46
+ high: { count: high, penalty: highPenalty },
47
+ medium: { count: medium, penalty: mediumPenalty },
48
+ low: { count: low, penalty: lowPenalty },
49
+ totalPenalty
50
+ }
51
+ };
52
+ }
53
+ /**
54
+ * Calculate trend from score history
55
+ */
56
+ export function calculateTrend(history) {
57
+ if (history.length === 0) {
58
+ return {
59
+ currentScore: 100,
60
+ previousScore: null,
61
+ change: 0,
62
+ direction: 'same',
63
+ history: []
64
+ };
65
+ }
66
+ const current = history[0];
67
+ const previous = history.length > 1 ? history[1] : null;
68
+ const change = previous ? current.score - previous.score : 0;
69
+ let direction = 'same';
70
+ if (change > 0)
71
+ direction = 'up';
72
+ else if (change < 0)
73
+ direction = 'down';
74
+ return {
75
+ currentScore: current.score,
76
+ previousScore: previous?.score || null,
77
+ change: Math.abs(change),
78
+ direction,
79
+ history: history.map(h => ({ timestamp: h.timestamp, score: h.score }))
80
+ };
81
+ }
82
+ /**
83
+ * Generate SVG badge for security score
84
+ */
85
+ export function generateScoreBadge(score, grade, gradeColor) {
86
+ const labelText = 'security';
87
+ const valueText = `${score} ${grade}`;
88
+ // Calculate widths (approximate character width)
89
+ const labelWidth = labelText.length * 7 + 10;
90
+ const valueWidth = valueText.length * 7 + 10;
91
+ const totalWidth = labelWidth + valueWidth;
92
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" viewBox="0 0 ${totalWidth} 20">
93
+ <linearGradient id="smooth" x2="0" y2="100%">
94
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
95
+ <stop offset="1" stop-opacity=".1"/>
96
+ </linearGradient>
97
+ <clipPath id="round">
98
+ <rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
99
+ </clipPath>
100
+ <g clip-path="url(#round)">
101
+ <rect width="${labelWidth}" height="20" fill="#555"/>
102
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${gradeColor}"/>
103
+ <rect width="${totalWidth}" height="20" fill="url(#smooth)"/>
104
+ </g>
105
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
106
+ <text x="${labelWidth / 2}" y="15" fill="#010101" fill-opacity=".3">${labelText}</text>
107
+ <text x="${labelWidth / 2}" y="14" fill="#fff">${labelText}</text>
108
+ <text x="${labelWidth + valueWidth / 2}" y="15" fill="#010101" fill-opacity=".3">${valueText}</text>
109
+ <text x="${labelWidth + valueWidth / 2}" y="14" fill="#fff">${valueText}</text>
110
+ </g>
111
+ </svg>`;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aura-security",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Deterministic security auditing engine with optional AI advisory layer. Run as CLI, CI step, or service. AI does not make enforcement decisions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -349,6 +349,103 @@
349
349
  margin: 0;
350
350
  }
351
351
 
352
+ /* Screenshot Comparison */
353
+ .screenshot-comparison {
354
+ display: grid;
355
+ grid-template-columns: 1fr 1fr;
356
+ gap: 2rem;
357
+ margin: 2rem 0;
358
+ }
359
+
360
+ .screenshot-card {
361
+ background: var(--bg-card);
362
+ border-radius: 16px;
363
+ overflow: hidden;
364
+ border: 1px solid var(--border);
365
+ }
366
+
367
+ .screenshot-label {
368
+ padding: 1rem 1.25rem;
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 0.75rem;
372
+ font-weight: 600;
373
+ font-size: 0.9375rem;
374
+ }
375
+
376
+ .screenshot-card.bad .screenshot-label {
377
+ background: rgba(239, 68, 68, 0.1);
378
+ border-bottom: 2px solid var(--critical);
379
+ color: var(--critical);
380
+ }
381
+
382
+ .screenshot-card.good .screenshot-label {
383
+ background: rgba(34, 197, 94, 0.1);
384
+ border-bottom: 2px solid var(--success);
385
+ color: var(--success);
386
+ }
387
+
388
+ .screenshot-icon {
389
+ font-size: 1.25rem;
390
+ }
391
+
392
+ .screenshot-container {
393
+ position: relative;
394
+ background: #0d1117;
395
+ min-height: 280px;
396
+ }
397
+
398
+ .screenshot-container img {
399
+ width: 100%;
400
+ height: auto;
401
+ display: block;
402
+ }
403
+
404
+ .screenshot-placeholder {
405
+ display: flex;
406
+ flex-direction: column;
407
+ align-items: center;
408
+ justify-content: center;
409
+ height: 280px;
410
+ text-align: center;
411
+ padding: 2rem;
412
+ }
413
+
414
+ .screenshot-placeholder.bad {
415
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(249, 115, 22, 0.1));
416
+ }
417
+
418
+ .screenshot-placeholder.good {
419
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(6, 182, 212, 0.1));
420
+ }
421
+
422
+ .placeholder-icon {
423
+ font-size: 4rem;
424
+ margin-bottom: 1rem;
425
+ opacity: 0.8;
426
+ }
427
+
428
+ .placeholder-text {
429
+ color: var(--text-secondary);
430
+ font-size: 1rem;
431
+ line-height: 1.5;
432
+ }
433
+
434
+ .placeholder-text small {
435
+ opacity: 0.7;
436
+ }
437
+
438
+ .screenshot-caption {
439
+ padding: 1rem 1.25rem;
440
+ font-size: 0.8125rem;
441
+ color: var(--text-secondary);
442
+ border-top: 1px solid var(--border);
443
+ }
444
+
445
+ .screenshot-caption strong {
446
+ color: var(--text);
447
+ }
448
+
352
449
  /* Comparison */
353
450
  .comparison {
354
451
  display: grid;
@@ -555,6 +652,9 @@
555
652
  .comparison {
556
653
  grid-template-columns: 1fr;
557
654
  }
655
+ .screenshot-comparison {
656
+ grid-template-columns: 1fr;
657
+ }
558
658
  .slop-grid {
559
659
  grid-template-columns: 1fr;
560
660
  }
@@ -797,6 +897,47 @@
797
897
  <h2>Good vs Bad: Real Examples</h2>
798
898
  <p>Here's what secure and insecure repositories look like when scanned with aurasecurity:</p>
799
899
 
900
+ <!-- 3D Visualizer Screenshots -->
901
+ <h3>3D Visualization Comparison</h3>
902
+ <p>See the difference at a glance - red means danger, green means safe:</p>
903
+
904
+ <div class="screenshot-comparison">
905
+ <div class="screenshot-card bad">
906
+ <div class="screenshot-label">
907
+ <span class="screenshot-icon bad">&#10060;</span>
908
+ <span>Vulnerable Repository</span>
909
+ </div>
910
+ <div class="screenshot-container">
911
+ <img src="https://app.aurasecurity.io/screenshots/bad-repo.png" alt="3D view of vulnerable repository with red nodes" onerror="this.parentElement.innerHTML='<div class=\'screenshot-placeholder bad\'><div class=\'placeholder-icon\'>&#128308;</div><div class=\'placeholder-text\'>juice-shop scan result<br><small>Red node = 9 secrets found</small></div></div>'">
912
+ </div>
913
+ <div class="screenshot-caption">
914
+ <strong>juice-shop</strong> - Multiple red severity indicators orbiting the node. Each red sphere represents a critical or high finding.
915
+ </div>
916
+ </div>
917
+
918
+ <div class="screenshot-card good">
919
+ <div class="screenshot-label">
920
+ <span class="screenshot-icon good">&#9989;</span>
921
+ <span>Clean Repository</span>
922
+ </div>
923
+ <div class="screenshot-container">
924
+ <img src="https://app.aurasecurity.io/screenshots/good-repo.png" alt="3D view of clean repository with green node" onerror="this.parentElement.innerHTML='<div class=\'screenshot-placeholder good\'><div class=\'placeholder-icon\'>&#128994;</div><div class=\'placeholder-text\'>aurasecurity scan result<br><small>Green node = 0 issues</small></div></div>'">
925
+ </div>
926
+ <div class="screenshot-caption">
927
+ <strong>aurasecurity</strong> - Clean green node with no severity indicators. This is your target state.
928
+ </div>
929
+ </div>
930
+ </div>
931
+
932
+ <div class="info-box">
933
+ <h5>&#127912; Reading the 3D View</h5>
934
+ <p><strong>Node Color:</strong> Red = critical issues, Orange = high, Yellow = medium, Green = clean<br>
935
+ <strong>Orbiting Shapes:</strong> Each shape around a node represents a finding category. More shapes = more issues.<br>
936
+ <strong>Click to Drill Down:</strong> Click any node to see severity breakdown, click severity to see individual findings.</p>
937
+ </div>
938
+
939
+ <!-- Stats Comparison Cards -->
940
+ <h3>Scan Statistics</h3>
800
941
  <div class="comparison">
801
942
  <div class="comparison-card bad">
802
943
  <div class="comparison-header">
@@ -815,7 +956,7 @@
815
956
  </div>
816
957
  <div class="comparison-stat">
817
958
  <span class="stat-label">High Findings</span>
818
- <span class="stat-value high">4 (API Keys)</span>
959
+ <span class="stat-value high">8 (API Keys)</span>
819
960
  </div>
820
961
  <div class="comparison-stat">
821
962
  <span class="stat-label">Medium Findings</span>
@@ -827,7 +968,7 @@
827
968
  </div>
828
969
  <div class="comparison-stat">
829
970
  <span class="stat-label">Total Issues</span>
830
- <span class="stat-value critical">20</span>
971
+ <span class="stat-value critical">24</span>
831
972
  </div>
832
973
  </div>
833
974
  </div>
@@ -835,13 +976,13 @@
835
976
  <div class="comparison-card good">
836
977
  <div class="comparison-header">
837
978
  <span>&#9989;</span>
838
- <span>get-shit-done (Clean)</span>
979
+ <span>aurasecurity (Clean)</span>
839
980
  </div>
840
981
  <div class="comparison-body">
841
- <p style="font-size: 0.875rem; margin-bottom: 1rem;">A simple productivity tool with no security findings.</p>
982
+ <p style="font-size: 0.875rem; margin-bottom: 1rem;">Our own repository - we practice what we preach.</p>
842
983
  <div class="comparison-stat">
843
984
  <span class="stat-label">Scan Time</span>
844
- <span class="stat-value">2.87s</span>
985
+ <span class="stat-value">4.10s</span>
845
986
  </div>
846
987
  <div class="comparison-stat">
847
988
  <span class="stat-label">Critical Findings</span>
@@ -198,16 +198,13 @@
198
198
  }
199
199
 
200
200
  .logo-icon {
201
- width: 28px;
202
- height: 28px;
203
- background: var(--accent);
204
- border-radius: 6px;
205
- display: flex;
206
- align-items: center;
207
- justify-content: center;
208
- font-size: 14px;
209
- font-weight: 600;
210
- color: white;
201
+ height: 24px;
202
+ width: auto;
203
+ }
204
+
205
+ .logo-icon img {
206
+ height: 100%;
207
+ width: auto;
211
208
  }
212
209
 
213
210
  .home-link {
@@ -289,6 +286,100 @@
289
286
  .stat-value.critical { color: var(--critical); }
290
287
  .stat-value.warning { color: var(--warning); }
291
288
 
289
+ /* Score Display */
290
+ .score-display {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 12px;
294
+ padding: 0 16px;
295
+ border-left: 1px solid var(--border);
296
+ margin-left: 8px;
297
+ }
298
+
299
+ .score-circle {
300
+ position: relative;
301
+ width: 40px;
302
+ height: 40px;
303
+ }
304
+
305
+ .score-circle svg {
306
+ transform: rotate(-90deg);
307
+ }
308
+
309
+ .score-circle .score-bg {
310
+ fill: none;
311
+ stroke: var(--bg-tertiary);
312
+ stroke-width: 3;
313
+ }
314
+
315
+ .score-circle .score-progress {
316
+ fill: none;
317
+ stroke: var(--success);
318
+ stroke-width: 3;
319
+ stroke-linecap: round;
320
+ transition: stroke-dashoffset 0.5s ease, stroke 0.3s;
321
+ }
322
+
323
+ .score-value-container {
324
+ position: absolute;
325
+ top: 50%;
326
+ left: 50%;
327
+ transform: translate(-50%, -50%);
328
+ text-align: center;
329
+ }
330
+
331
+ .score-number {
332
+ font-size: 12px;
333
+ font-weight: 700;
334
+ color: var(--text-primary);
335
+ line-height: 1;
336
+ }
337
+
338
+ .score-info {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 2px;
342
+ }
343
+
344
+ .score-label {
345
+ font-size: 11px;
346
+ color: var(--text-muted);
347
+ text-transform: uppercase;
348
+ letter-spacing: 0.5px;
349
+ }
350
+
351
+ .score-grade {
352
+ font-size: 18px;
353
+ font-weight: 700;
354
+ }
355
+
356
+ .score-grade.grade-a { color: var(--success); }
357
+ .score-grade.grade-b { color: var(--accent); }
358
+ .score-grade.grade-c { color: var(--warning); }
359
+ .score-grade.grade-f { color: var(--critical); }
360
+
361
+ .score-trend {
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 4px;
365
+ font-size: 11px;
366
+ }
367
+
368
+ .score-trend.up { color: var(--success); }
369
+ .score-trend.down { color: var(--critical); }
370
+ .score-trend.same { color: var(--text-muted); }
371
+
372
+ .trend-chart-mini {
373
+ width: 60px;
374
+ height: 24px;
375
+ }
376
+
377
+ .trend-chart-mini polyline {
378
+ fill: none;
379
+ stroke: var(--accent);
380
+ stroke-width: 1.5;
381
+ }
382
+
292
383
  /* Left Panel - Scanner */
293
384
  #scanner-panel {
294
385
  top: 72px;
@@ -795,7 +886,7 @@
795
886
  <!-- Header -->
796
887
  <header id="header" class="panel">
797
888
  <div class="logo">
798
- <div class="logo-icon">S</div>
889
+ <div class="logo-icon"><img src="/images/aura-logo.jpg" alt="AURA"></div>
799
890
  <span>aurasecurity</span>
800
891
  </div>
801
892
 
@@ -824,6 +915,28 @@
824
915
  <span class="stat-value" id="totalRepos">0</span>
825
916
  <span>Scanned</span>
826
917
  </div>
918
+
919
+ <!-- Security Score -->
920
+ <div class="score-display" id="scoreDisplay">
921
+ <div class="score-circle">
922
+ <svg viewBox="0 0 40 40">
923
+ <circle class="score-bg" cx="20" cy="20" r="17"/>
924
+ <circle class="score-progress" id="scoreProgress" cx="20" cy="20" r="17"
925
+ stroke-dasharray="106.8" stroke-dashoffset="106.8"/>
926
+ </svg>
927
+ <div class="score-value-container">
928
+ <span class="score-number" id="scoreNumber">--</span>
929
+ </div>
930
+ </div>
931
+ <div class="score-info">
932
+ <span class="score-label">Score</span>
933
+ <span class="score-grade grade-a" id="scoreGrade">-</span>
934
+ <span class="score-trend same" id="scoreTrend">--</span>
935
+ </div>
936
+ <svg class="trend-chart-mini" id="trendChartMini" viewBox="0 0 60 24">
937
+ <polyline id="trendLine" points=""/>
938
+ </svg>
939
+ </div>
827
940
  </div>
828
941
  </header>
829
942
 
@@ -1802,6 +1915,7 @@
1802
1915
  log(`Complete: ${repo.secrets} secrets, ${repo.vulns} vulnerabilities`, repo.secrets > 0 ? 'error' : 'success');
1803
1916
  }
1804
1917
  updateUI();
1918
+ refreshScore(); // Update security score after scan
1805
1919
  selectRepo(repo.id);
1806
1920
 
1807
1921
  } catch (err) {
@@ -2140,8 +2254,83 @@
2140
2254
  }
2141
2255
  window.clearAllReports = clearAllReports;
2142
2256
 
2257
+ // ========== SECURITY SCORE ==========
2258
+ async function fetchSecurityScore() {
2259
+ try {
2260
+ const res = await fetch(`${AURA_URL}/score`);
2261
+ if (!res.ok) return null;
2262
+ return await res.json();
2263
+ } catch (e) {
2264
+ console.log('Score fetch failed:', e);
2265
+ return null;
2266
+ }
2267
+ }
2268
+
2269
+ function updateScoreDisplay(scoreData) {
2270
+ if (!scoreData) {
2271
+ document.getElementById('scoreNumber').textContent = '--';
2272
+ document.getElementById('scoreGrade').textContent = '-';
2273
+ document.getElementById('scoreTrend').textContent = '--';
2274
+ return;
2275
+ }
2276
+
2277
+ const { score, grade, gradeColor, trend } = scoreData;
2278
+
2279
+ // Update score number
2280
+ document.getElementById('scoreNumber').textContent = score;
2281
+
2282
+ // Update grade with color
2283
+ const gradeEl = document.getElementById('scoreGrade');
2284
+ gradeEl.textContent = grade;
2285
+ gradeEl.className = 'score-grade';
2286
+ if (score >= 90) gradeEl.classList.add('grade-a');
2287
+ else if (score >= 70) gradeEl.classList.add('grade-b');
2288
+ else if (score >= 50) gradeEl.classList.add('grade-c');
2289
+ else gradeEl.classList.add('grade-f');
2290
+
2291
+ // Update progress ring (circumference = 2 * PI * 17 = 106.8)
2292
+ const circumference = 106.8;
2293
+ const offset = circumference - (score / 100) * circumference;
2294
+ const progressEl = document.getElementById('scoreProgress');
2295
+ progressEl.style.strokeDashoffset = offset;
2296
+ progressEl.style.stroke = gradeColor || (score >= 70 ? 'var(--success)' : score >= 50 ? 'var(--warning)' : 'var(--critical)');
2297
+
2298
+ // Update trend indicator
2299
+ const trendEl = document.getElementById('scoreTrend');
2300
+ if (trend && trend.previousScore !== null) {
2301
+ const arrow = trend.direction === 'up' ? '↑' : trend.direction === 'down' ? '↓' : '→';
2302
+ trendEl.textContent = `${arrow} ${trend.change}`;
2303
+ trendEl.className = `score-trend ${trend.direction}`;
2304
+ } else {
2305
+ trendEl.textContent = '—';
2306
+ trendEl.className = 'score-trend same';
2307
+ }
2308
+
2309
+ // Update mini trend chart
2310
+ if (trend && trend.history && trend.history.length > 1) {
2311
+ const points = trend.history.slice().reverse();
2312
+ const maxScore = Math.max(...points.map(p => p.score), 100);
2313
+ const minScore = Math.min(...points.map(p => p.score), 0);
2314
+ const range = maxScore - minScore || 1;
2315
+
2316
+ const polyPoints = points.map((p, i) => {
2317
+ const x = (i / (points.length - 1)) * 58 + 1;
2318
+ const y = 22 - ((p.score - minScore) / range) * 20;
2319
+ return `${x},${y}`;
2320
+ }).join(' ');
2321
+
2322
+ document.getElementById('trendLine').setAttribute('points', polyPoints);
2323
+ }
2324
+ }
2325
+
2326
+ async function refreshScore() {
2327
+ const scoreData = await fetchSecurityScore();
2328
+ updateScoreDisplay(scoreData);
2329
+ }
2330
+
2143
2331
  // ========== INIT ==========
2144
2332
  initScene();
2333
+ refreshScore(); // Fetch initial score
2145
2334
  log('Ready to scan');
2146
2335
  </script>
2147
2336
  </body>
@@ -110,14 +110,13 @@
110
110
  }
111
111
 
112
112
  .logo-mark {
113
- width: 36px;
114
- height: 36px;
115
- background: linear-gradient(135deg, var(--primary), var(--secondary));
116
- border-radius: 10px;
117
- display: flex;
118
- align-items: center;
119
- justify-content: center;
120
- font-size: 1.25rem;
113
+ height: 32px;
114
+ width: auto;
115
+ }
116
+
117
+ .logo-mark img {
118
+ height: 100%;
119
+ width: auto;
121
120
  }
122
121
 
123
122
  .nav-links {
@@ -1489,7 +1488,7 @@
1489
1488
  <nav>
1490
1489
  <div class="nav-inner">
1491
1490
  <a href="/" class="logo">
1492
- <div class="logo-mark">🛡️</div>
1491
+ <div class="logo-mark"><img src="/images/aura-logo.jpg" alt="AURA"></div>
1493
1492
  aurasecurity
1494
1493
  </a>
1495
1494
  <div class="nav-links">
@@ -1898,7 +1897,7 @@
1898
1897
  <div class="footer-container">
1899
1898
  <div class="footer-left">
1900
1899
  <a href="/" class="logo">
1901
- <div class="logo-mark">🛡️</div>
1900
+ <div class="logo-mark"><img src="/images/aura-logo.jpg" alt="AURA"></div>
1902
1901
  aurasecurity
1903
1902
  </a>
1904
1903
  <div class="footer-links">