aura-security 0.4.0 → 0.4.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 slopsecurityadmin
3
+ Copyright (c) 2026 aurasecurity
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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.3.0';
27
+ const VERSION = '0.4.1';
28
28
  // ANSI colors for terminal output
29
29
  const colors = {
30
30
  reset: '\x1b[0m',
@@ -134,6 +134,15 @@ const SECURITY_TOOLS = [
134
134
  install: { gem: 'gem install bundler-audit' },
135
135
  license: 'GPLv3 (Free)',
136
136
  required: false
137
+ },
138
+ {
139
+ name: 'flawfinder',
140
+ command: 'flawfinder',
141
+ description: 'C/C++ source code security scanner',
142
+ category: 'sast',
143
+ install: { pip: 'pip install flawfinder', apt: 'apt install flawfinder' },
144
+ license: 'GPL-2.0 (Free)',
145
+ required: false
137
146
  }
138
147
  ];
139
148
  function isToolInstalled(command) {
@@ -640,22 +649,28 @@ function displayScanResults(result) {
640
649
  const dockerfileCount = result.dockerfileFindings.length;
641
650
  const serviceCount = result.discoveredServices.length;
642
651
  const moduleCount = result.discoveredModules.length;
652
+ // Helper to normalize severity (SAST findings use uppercase)
653
+ const normSev = (sev) => (sev || '').toLowerCase();
643
654
  const criticalCount = result.secrets.filter(s => s.severity === 'critical').length +
644
655
  result.packages.filter(p => p.severity === 'critical').length +
645
656
  result.iacFindings.filter(i => i.severity === 'critical').length +
646
- result.dockerfileFindings.filter(d => d.severity === 'critical').length;
657
+ result.dockerfileFindings.filter(d => d.severity === 'critical').length +
658
+ result.sastFindings.filter(s => normSev(s.severity) === 'critical').length;
647
659
  const highCount = result.secrets.filter(s => s.severity === 'high').length +
648
660
  result.packages.filter(p => p.severity === 'high').length +
649
661
  result.iacFindings.filter(i => i.severity === 'high').length +
650
- result.dockerfileFindings.filter(d => d.severity === 'high').length;
662
+ result.dockerfileFindings.filter(d => d.severity === 'high').length +
663
+ result.sastFindings.filter(s => normSev(s.severity) === 'high').length;
651
664
  const mediumCount = result.secrets.filter(s => s.severity === 'medium').length +
652
665
  result.packages.filter(p => p.severity === 'medium').length +
653
666
  result.iacFindings.filter(i => i.severity === 'medium').length +
654
- result.dockerfileFindings.filter(d => d.severity === 'medium').length;
667
+ result.dockerfileFindings.filter(d => d.severity === 'medium').length +
668
+ result.sastFindings.filter(s => normSev(s.severity) === 'medium').length;
655
669
  const lowCount = result.secrets.filter(s => s.severity === 'low').length +
656
670
  result.packages.filter(p => p.severity === 'low').length +
657
671
  result.iacFindings.filter(i => i.severity === 'low').length +
658
- result.dockerfileFindings.filter(d => d.severity === 'low').length;
672
+ result.dockerfileFindings.filter(d => d.severity === 'low').length +
673
+ result.sastFindings.filter(s => normSev(s.severity) === 'low').length;
659
674
  // Stats bar
660
675
  console.log('┌─────────────────────────────────────────────────────────────────┐');
661
676
  console.log(`│ ${c('bgRed', ` CRITICAL ${criticalCount} `)} ${c('red', `HIGH ${highCount}`)} ${c('yellow', `MEDIUM ${mediumCount}`)} ${c('dim', `LOW ${lowCount}`)} │`);
@@ -1343,8 +1358,8 @@ ${c('bold', 'SCAN OPTIONS:')}
1343
1358
  -j, --json Output results as JSON
1344
1359
  -f, --format Output format: console, json, sarif, junit, gitlab-sast, gitlab-deps, summary
1345
1360
  -o, --output Save results to file
1346
- -l, --languages Filter by languages: js,py,go,rust,ruby,php,java
1347
- -S, --scanners Use specific scanners: grype,trivy,gitleaks,semgrep,checkov,hadolint,pip-audit,govulncheck,cargo-audit,bundle-audit,composer
1361
+ -l, --languages Filter by languages: js,py,go,rust,ruby,php,java,c,csharp
1362
+ -S, --scanners Use specific scanners: grype,trivy,gitleaks,semgrep,checkov,hadolint,pip-audit,govulncheck,cargo-audit,bundle-audit,composer,flawfinder
1348
1363
  --fail-on Exit with error on severity: critical,high,medium (default: critical)
1349
1364
  --no-fail Never exit with error code
1350
1365
 
@@ -36,17 +36,51 @@ const SECRET_SCAN_EXCLUDE_FILES = [
36
36
  '.next/',
37
37
  'coverage/'
38
38
  ];
39
- // Directories that often contain false positives (test data, scripts, examples)
39
+ // Directories that often contain false positives (test data, vendored deps, examples)
40
40
  const FALSE_POSITIVE_DIRS = [
41
41
  'node_modules',
42
42
  '__tests__',
43
43
  '__mocks__',
44
- 'test/',
45
- 'tests/',
46
- 'spec/',
47
- 'fixtures/',
48
- 'examples/',
49
- 'scripts/'
44
+ '/test/',
45
+ '/tests/',
46
+ '/spec/',
47
+ '/fixtures/',
48
+ '/examples/',
49
+ '/example/',
50
+ '/sample/',
51
+ '/samples/',
52
+ '/demo/',
53
+ '/demos/',
54
+ '/mock/',
55
+ '/mocks/',
56
+ '/testdata/',
57
+ '/test-data/',
58
+ '/assets/test/',
59
+ '/testing/',
60
+ '/vendor/',
61
+ '/vendors/',
62
+ '/bundled/',
63
+ '/deps/',
64
+ '/third_party/',
65
+ '/third-party/',
66
+ '/thirdparty/',
67
+ '/external/',
68
+ '/dummy/',
69
+ '/fake/'
70
+ ];
71
+ // File name patterns that indicate test/example data (not real secrets)
72
+ const TEST_FILE_PATTERNS = [
73
+ /\.example$/i,
74
+ /\.sample$/i,
75
+ /\.test\./i,
76
+ /\.spec\./i,
77
+ /example\d*\.(key|pem|jwt|crt|p12|pfx)$/i,
78
+ /test\d*\.(key|pem|jwt|crt|p12|pfx)$/i,
79
+ /sample\d*\.(key|pem|jwt|crt|p12|pfx)$/i,
80
+ /fake[-_]?(key|secret|token|credential)/i,
81
+ /dummy[-_]?(key|secret|token|credential)/i,
82
+ /mock[-_]?(key|secret|token|credential)/i,
83
+ /temporary[-_]?key/i,
50
84
  ];
51
85
  // Rules to completely skip - too many false positives to be useful
52
86
  // aws-secret-access-key matches any 40-char base64 string which catches SHA hashes,
@@ -58,6 +92,14 @@ const SKIP_RULES_ENTIRELY = [
58
92
  const FALSE_POSITIVE_RULES = [
59
93
  'generic-api-key'
60
94
  ];
95
+ // Rules to skip in specific file types (too many false positives)
96
+ const SKIP_RULES_IN_FILE_TYPES = {
97
+ // generic-api-key matches C macros and function signatures in headers
98
+ '.h': ['generic-api-key'],
99
+ '.hpp': ['generic-api-key'],
100
+ '.c': ['generic-api-key', 'sourcegraph-access-token'],
101
+ '.cpp': ['generic-api-key'],
102
+ };
61
103
  // Run gitleaks for secrets detection
62
104
  function runGitleaks(targetPath) {
63
105
  const findings = [];
@@ -93,10 +135,17 @@ function runGitleaks(targetPath) {
93
135
  const isExcludedFile = SECRET_SCAN_EXCLUDE_FILES.some(pattern => fileName === pattern || filePath.includes(pattern.toLowerCase()));
94
136
  // Filter out findings from known false positive directories
95
137
  const isInFalsePositiveDir = FALSE_POSITIVE_DIRS.some(dir => filePath.includes(dir.toLowerCase()));
138
+ // Filter out test/example file names
139
+ const isTestFile = TEST_FILE_PATTERNS.some(pattern => pattern.test(fileName) || pattern.test(filePath));
96
140
  // Filter out known false positive rules in certain file types
97
141
  const isFalsePositiveRule = FALSE_POSITIVE_RULES.includes(ruleId) &&
98
142
  (isExcludedFile || isInFalsePositiveDir);
99
- if (isExcludedFile || isFalsePositiveRule) {
143
+ // Check if this rule should be skipped for this file type
144
+ const fileExt = extname(fileName).toLowerCase();
145
+ const skipRulesForExt = SKIP_RULES_IN_FILE_TYPES[fileExt] || [];
146
+ const isRuleSkippedForFileType = skipRulesForExt.includes(ruleId);
147
+ // Skip if any false positive indicator matches
148
+ if (isExcludedFile || isInFalsePositiveDir || isTestFile || isFalsePositiveRule || isRuleSkippedForFileType) {
100
149
  filteredCount++;
101
150
  continue;
102
151
  }
@@ -114,7 +163,7 @@ function runGitleaks(targetPath) {
114
163
  }
115
164
  catch { }
116
165
  if (filteredCount > 0) {
117
- console.log(`[SCANNER] Filtered ${filteredCount} false positives from lock/generated files`);
166
+ console.log(`[SCANNER] Filtered ${filteredCount} false positives (test files, examples, lock files)`);
118
167
  }
119
168
  }
120
169
  console.log(`[SCANNER] gitleaks found ${findings.length} secrets`);
@@ -533,6 +582,52 @@ function runComposerAudit(targetPath) {
533
582
  }
534
583
  return findings;
535
584
  }
585
+ function runFlawfinder(targetPath) {
586
+ const findings = [];
587
+ if (!isToolAvailable('flawfinder')) {
588
+ console.log('[SCANNER] flawfinder not available, skipping C/C++ SAST');
589
+ return findings;
590
+ }
591
+ try {
592
+ console.log('[SCANNER] Running flawfinder...');
593
+ const result = spawnSync('flawfinder', [
594
+ '--dataonly',
595
+ '--quiet',
596
+ '--csv',
597
+ targetPath
598
+ ], { encoding: 'utf-8', timeout: 180000, maxBuffer: 50 * 1024 * 1024 });
599
+ if (result.stdout) {
600
+ // Parse CSV output: File,Line,Column,Level,Category,Name,Warning
601
+ const lines = result.stdout.split('\n').filter(l => l.trim() && !l.startsWith('File,'));
602
+ for (const line of lines) {
603
+ const parts = line.split(',');
604
+ if (parts.length >= 7) {
605
+ const level = parseInt(parts[3], 10);
606
+ // Map flawfinder levels (0-5) to severity
607
+ let severity = 'low';
608
+ if (level >= 5)
609
+ severity = 'critical';
610
+ else if (level >= 4)
611
+ severity = 'high';
612
+ else if (level >= 2)
613
+ severity = 'medium';
614
+ findings.push({
615
+ file: parts[0],
616
+ line: parseInt(parts[1], 10),
617
+ rule: `${parts[4]}:${parts[5]}`,
618
+ message: parts.slice(6).join(',').replace(/"/g, ''),
619
+ severity
620
+ });
621
+ }
622
+ }
623
+ }
624
+ console.log(`[SCANNER] flawfinder found ${findings.length} issues`);
625
+ }
626
+ catch (err) {
627
+ console.error('[SCANNER] flawfinder error:', err);
628
+ }
629
+ return findings;
630
+ }
536
631
  function runCheckov(targetPath) {
537
632
  const findings = [];
538
633
  if (!isToolAvailable('checkov')) {
@@ -892,6 +987,13 @@ export class LocalScanner {
892
987
  else {
893
988
  console.log('[SCANNER] semgrep not available, skipping SAST');
894
989
  }
990
+ // C/C++ SAST with flawfinder
991
+ if (shouldScanLang('c') && shouldUsescanner('flawfinder') && isToolAvailable('flawfinder')) {
992
+ console.log('[SCANNER] Using flawfinder for C/C++ SAST analysis');
993
+ const flawfinderFindings = runFlawfinder(this.config.targetPath);
994
+ result.sastFindings.push(...flawfinderFindings);
995
+ toolsUsed.push('flawfinder');
996
+ }
895
997
  // ============ IAC SCANNING ============
896
998
  if (this.config.scanIaC !== false) {
897
999
  if (shouldUsescanner('checkov') && isToolAvailable('checkov')) {
@@ -948,8 +1050,16 @@ export class LocalScanner {
948
1050
  languages.add('php');
949
1051
  if (existsSync(join(dir, 'pom.xml')) || existsSync(join(dir, 'build.gradle')))
950
1052
  languages.add('java');
951
- if (existsSync(join(dir, '*.csproj')) || existsSync(join(dir, '*.sln')))
952
- languages.add('csharp');
1053
+ // C# - need to scan directory for .csproj/.sln files (existsSync doesn't support globs)
1054
+ try {
1055
+ const entries = readdirSync(dir);
1056
+ if (entries.some(f => f.endsWith('.csproj') || f.endsWith('.sln')))
1057
+ languages.add('csharp');
1058
+ // C/C++ detection
1059
+ if (entries.some(f => f === 'CMakeLists.txt' || f === 'Makefile' || f.endsWith('.c') || f.endsWith('.cpp') || f.endsWith('.h') || f.endsWith('.hpp')))
1060
+ languages.add('c');
1061
+ }
1062
+ catch { /* ignore */ }
953
1063
  return Array.from(languages);
954
1064
  }
955
1065
  // Merge package findings avoiding duplicates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aura-security",
3
- "version": "0.4.0",
3
+ "version": "0.4.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",
@@ -804,8 +804,21 @@
804
804
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
805
805
 
806
806
  // ========== CONFIGURATION ==========
807
- const AURA_URL = 'http://127.0.0.1:3000';
808
- const WS_URL = 'ws://127.0.0.1:3001';
807
+ // Auto-detect environment based on hostname
808
+ const hostname = window.location.hostname;
809
+ const isLocalDev = hostname === '127.0.0.1' || hostname === 'localhost';
810
+
811
+ // API URL: local dev uses localhost:3000, production uses same origin (nginx proxies to API)
812
+ const AURA_URL = isLocalDev ? 'http://127.0.0.1:3000' : '';
813
+
814
+ // WebSocket URL: local dev uses localhost:3001, production uses same origin /ws path
815
+ const WS_URL = isLocalDev
816
+ ? 'ws://127.0.0.1:3001'
817
+ : `wss://${hostname}/ws`;
818
+
819
+ console.log('[CONFIG] Environment:', isLocalDev ? 'local' : 'production');
820
+ console.log('[CONFIG] API URL:', AURA_URL || '(same origin)');
821
+ console.log('[CONFIG] WS URL:', WS_URL);
809
822
  const POLL_INTERVAL = 5000; // Slower polling as WebSocket handles real-time
810
823
  let wsConnected = false;
811
824
  let ws = null;
@@ -2383,7 +2396,19 @@
2383
2396
  })
2384
2397
  });
2385
2398
 
2386
- const data = await res.json();
2399
+ // Check if response is OK before parsing JSON
2400
+ if (!res.ok) {
2401
+ const errorText = await res.text();
2402
+ throw new Error(`Server error (${res.status}): ${errorText.substring(0, 100)}`);
2403
+ }
2404
+
2405
+ // Safely parse JSON with error handling
2406
+ let data;
2407
+ try {
2408
+ data = await res.json();
2409
+ } catch (parseErr) {
2410
+ throw new Error('Invalid response from server (not JSON)');
2411
+ }
2387
2412
  console.log('Git scan response:', data);
2388
2413
 
2389
2414
  if (data.result && data.result.scan_details) {
@@ -2391,8 +2416,11 @@
2391
2416
  } else if (data.error) {
2392
2417
  statusEl.innerHTML = `<span style="color: #ff4444;">Error: ${data.error}</span>`;
2393
2418
  showToast('Git scan failed', true);
2419
+ } else if (data.result && data.result.error) {
2420
+ statusEl.innerHTML = `<span style="color: #ff4444;">Error: ${data.result.error}</span>`;
2421
+ showToast('Git scan failed', true);
2394
2422
  } else {
2395
- statusEl.innerHTML = '<span style="color: #ff4444;">Scan failed</span>';
2423
+ statusEl.innerHTML = '<span style="color: #ff4444;">Scan failed - no results</span>';
2396
2424
  }
2397
2425
  } catch (err) {
2398
2426
  console.error('Git scan error:', err);