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.
- package/dist/aura/server.d.ts +5 -0
- package/dist/aura/server.js +110 -2
- package/dist/cli.js +1 -1
- package/dist/database/index.d.ts +13 -0
- package/dist/database/index.js +134 -0
- package/dist/index.js +20 -0
- package/dist/scoring/index.d.ts +70 -0
- package/dist/scoring/index.js +112 -0
- package/package.json +1 -1
- package/visualizer/docs.html +146 -5
- package/visualizer/index-minimal.html +200 -11
- package/visualizer/landing.html +9 -10
package/dist/aura/server.d.ts
CHANGED
|
@@ -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>;
|
package/dist/aura/server.js
CHANGED
|
@@ -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
|
|
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.
|
|
27
|
+
const VERSION = '0.5.1';
|
|
28
28
|
// ANSI colors for terminal output
|
|
29
29
|
const colors = {
|
|
30
30
|
reset: '\x1b[0m',
|
package/dist/database/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/database/index.js
CHANGED
|
@@ -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.
|
|
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",
|
package/visualizer/docs.html
CHANGED
|
@@ -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">❌</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\'>🔴</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">✅</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\'>🟢</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>🎨 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">
|
|
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">
|
|
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>✅</span>
|
|
838
|
-
<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;">
|
|
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">
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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"
|
|
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>
|
package/visualizer/landing.html
CHANGED
|
@@ -110,14 +110,13 @@
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
.logo-mark {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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"
|
|
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"
|
|
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">
|