Skip to content

Commit bb8411b

Browse files
committed
feat(cli): focus flags and severity triage; fix(SEC018): context-aware, pattern-first; chore: runtime minConfidence for human runs; docs: CLI and CHANGELOG 1.0.4; tests: SEC018 cases
1 parent 0145326 commit bb8411b

File tree

9 files changed

+278
-21
lines changed

9 files changed

+278
-21
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,19 @@ Initial stable release.
6161
### Notes
6262
- External link checks are timeout-guarded; internal crawling remains opt-in.
6363
- Heuristics aim to minimize noise; tune with confidence thresholding, rule enable/disable, and baselines.
64+
65+
## 1.0.4 — 2025-08-24
66+
67+
### Added
68+
- Human output triage header with severity-first summary (non-breaking; JSON/SARIF unchanged)
69+
- Focus filters for human output: `--focus-critical`, `--focus-security`, `--focus-new`, and `--detailed`
70+
71+
### Changed (non-disruptive)
72+
- SEC018 noise reduction: context/file-aware ignores (CSS/Tailwind/globs/data URIs/UUID), pattern-first detection for `sk-`/JWT/DB URLs/etc., higher entropy threshold
73+
- Runtime default minConfidence=0.8 for human runs (non-JSON) when not provided (does not change config or JSON/SARIF)
74+
75+
### Tests & Docs
76+
- Added tests covering SEC018 false positives and true positives
77+
- Updated CLI docs with new flags and examples
78+
79+
This patch focuses on triage-first UX and noise reduction without changing schema or defaults that would break existing workflows.

docs/CLI.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Options:
3535
--crawl-start-url <url> Starting URL for internal crawl
3636
--crawl-depth <n> Max crawl depth (default: 2)
3737
--crawl-timeout <ms> Per-page timeout in ms (default: 10000)
38+
--detailed Show all findings including lower-confidence/noisy ones
39+
--focus-critical Only show critical (high severity) issues
40+
--focus-security Only show security issues (hide a11y/links/etc)
41+
--focus-new Only show issues not in baseline
3842
```
3943

4044
Examples:
@@ -51,6 +55,12 @@ ubon scan --update-baseline
5155
5256
# SARIF report for GitHub code scanning
5357
ubon scan --sarif ubon.sarif
58+
59+
# Show only critical security issues (progressive disclosure)
60+
ubon scan --focus-critical --focus-security
61+
62+
# Show everything, including lower-confidence
63+
ubon scan --detailed
5464
```
5565

5666
#### ubon check

docs/ROADMAP.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
### Ubon Roadmap
2+
3+
This document tracks planned improvements for the next minor release.
4+
5+
## Next version: v1.1.0 (proposed)
6+
7+
### Output and UX
8+
- **Colorized, branded output**: Chalk theme with lotus (🪷), toggle via `--color auto|always|never` and `--format human|table|json`.
9+
- **Grouping/deduping**: `--group-by file|rule`, collapse repeated findings, `--min-severity warning|error`, `--max-issues N`.
10+
- **Actionable snippets**: 3–5 lines of code context and a short “why it matters”.
11+
12+
### Auto-fix and repair
13+
- **Expand safe fixes**:
14+
- Add `alt` attributes and input labels/aria-labels.
15+
- Add cookie flags: `HttpOnly; Secure; SameSite=Lax`.
16+
- Remove hardcoded env fallbacks and secret-logging statements.
17+
- Wrap `fetch` with AbortController timeout and basic retry/backoff.
18+
- **PR generator**: `--apply-fixes --create-pr` to branch, commit, and open a reviewable PR with a summary.
19+
- **Per-rule codemods**: Fixers shipped per `ruleId` so teams can opt in/out.
20+
21+
### Rule coverage (security + Next.js)
22+
- **JWT/cookies**: Detect tokens returned in JSON, weak/inline secrets, missing cookie flags.
23+
- **Next.js App Router**: Client/server boundary leaks, `NEXT_PUBLIC_*` misuse, `dangerouslySetInnerHTML`, open redirects, CORS in `middleware.ts`.
24+
- **Data exfil/logging**: Secrets/PII in logs, `JSON.stringify(req)` dumps, verbose prod errors.
25+
- **Network**: SSRF sinks (`fetch(userInput)`), unbounded axios/fetch, missing `signal`.
26+
- **Config**: Insecure `next.config.js` (e.g., permissive `images.domains`, `eslint.ignoreDuringBuilds`), risky webpack overrides.
27+
- **Backend**: String‑built SQL, `new Function`, shell exec, Prisma `.env` leaks.
28+
29+
### Signal/noise controls
30+
- **Inline suppressions**: `// ubon-disable-next-line RULEID` (optional reason), surfaced in JSON/SARIF.
31+
- **Tuning**: Per‑rule confidence thresholds in config; first‑class ignore globs for fixtures/mocks/stories.
32+
- **Taxonomy**: Add CWE/OWASP tags per rule and include in outputs.
33+
34+
### Performance and scale
35+
- **Caching**: Local OSV cache with TTL; memoize results by file hash for fast re‑runs.
36+
- **Parallelism**: Bounded concurrency and rate‑limited external link checks.
37+
- **Watch mode**: `ubon check --watch --fast` incremental scanning on file changes.
38+
39+
### CI and PR integration
40+
- **Review bot**: Optional GitHub reviewer that posts inline comments for new/changed issues.
41+
- **Gates**: “New issues only” gate using base SHA; budget mode (cap warnings/errors).
42+
- **SARIF polish**: Rich `helpUri` links to rule docs and remediation examples.
43+
44+
### IDE and developer ergonomics
45+
- **VS Code extension**: In‑editor diagnostics and quick‑fixes for autofixable rules.
46+
- **Init recipes**: `ubon init` can generate `.env.example` and a minimal security checklist tailored to the profile.
47+
48+
### Programmatic API and schema
49+
- **Stable JSON schema**: Publish `@ubon/schema` for typed consumers (versioned).
50+
- **Result fingerprints**: Stability across reformatting; document derivation.
51+
52+
### Scanner safety and privacy
53+
- **Default redaction**: Mask secret‑like strings in human output; keep stable fingerprints.
54+
- **Sandboxing**: Never execute user code; mock dynamic imports; document network usage and opt‑outs.
55+
56+
### Explore expansion to Rails?
57+
58+
Rails would be very doable with ubon's current architecture.
59+
60+
Here's why I think it would work well:
61+
62+
What's already there ✅
63+
64+
- Multi-language framework - ubon already handles JS, Python with profiles
65+
- Pattern-based detection - Ruby syntax is very scannable with regex patterns
66+
- CVE database integration - Should already include Ruby gems vulnerabilities
67+
- Environment scanning - Would work for Rails config/ files, database.yml, etc.
68+
- File structure awareness - Could easily learn Rails conventions
69+
70+
Rails-specific rules to add 🔨
71+
72+
High-impact vulnerabilities:
73+
# SQL injection patterns
74+
User.where("name = '#{params[:name]}'") # Should flag
75+
User.find_by_sql("SELECT * FROM users WHERE id = #{id}")
76+
77+
# Mass assignment
78+
User.create(params[:user]) # Without strong params
79+
80+
# Command injection
81+
system("ls #{params[:dir]}") # Shell injection
82+
`git log #{branch}` # Backtick injection
83+
84+
# Deserialization
85+
YAML.load(user_input) # vs YAML.safe_load
86+
Marshal.load(data)
87+
88+
# Template injection in ERB
89+
<%= params[:content].html_safe %> # XSS risk
90+
91+
Rails-specific files to scan:
92+
- Gemfile.lock - Gem vulnerabilities (like requirements.txt)
93+
- config/database.yml - Database credentials
94+
- config/secrets.yml - Hardcoded secrets
95+
- app/controllers/ - Strong params, authentication
96+
- app/views/ - XSS, template injection
97+
98+
Implementation approach 🛠️
99+
100+
Phase 1 (Easy wins):
101+
- Add --profile rails
102+
- Ruby syntax patterns for eval, system, YAML.load
103+
- Gemfile.lock vulnerability scanning (similar to requirements.txt)
104+
- Config file secret detection
105+
106+
Phase 2 (Rails-aware):
107+
- ActiveRecord query pattern analysis
108+
- Strong parameters validation
109+
- ERB template scanning
110+
- Rails security best practices
111+
112+
Phase 3 (Advanced):
113+
- Semantic analysis of Rails patterns
114+
- Route/controller flow analysis
115+
- Authentication/authorization checks
116+
117+
The beauty is ubon's pattern-based + profile system would translate perfectly to
118+
Rails. Most Ruby vulnerabilities follow predictable patterns that regex can catch
119+
effectively.
120+
121+
Biggest win: Rails has very established security patterns, so the rules would be
122+
highly accurate with fewer false positives than general-purpose scanners.
123+
124+
## v1.1.0 milestone priorities
125+
- **P1**: Output/UX polish (color/theme, grouping, context snippets); inline suppressions; OSV caching; cookie/JWT rules.
126+
- **P2**: Expanded autofixes and `--create-pr`; “new issues only” CI gate; watch mode.
127+
- **P3**: VS Code extension (MVP); SARIF help links; schema package draft.
128+
- **P4**: New profile 'Rails'. And include it in the relevant documentation, README, etc. (anywhere where it's mentioned that is for Next/React, Python and Vue)
129+
130+
## Success criteria
131+
- Human output: grouped by file/rule, colorized, with context snippets and < 120 ms added overhead in `--fast` mode on medium repos.
132+
- JSON/SARIF: include CWE/OWASP tags, suppressions, stable fingerprints; validated by sample CI run.
133+
- Autofixes: at least A11Y001/A11Y002, cookie flags, secret‑logging removal proven on sample repos.
134+
135+
## Notes
136+
- Maintain redaction by default in human output; never print full secrets.
137+
- Add docs for reducing false positives and enabling baselines.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ubon",
3-
"version": "1.0.3",
3+
"version": "1.0.4",
44
"description": "Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/__tests__/rules-misc.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ describe('Misc rules', () => {
2323
const res = await iac.scan({ directory: tmp });
2424
expect(res.some(r => r.ruleId === 'DOCKER004')).toBe(true);
2525
});
26+
27+
it('does not flag CSS/Tailwind noise for SEC018', async () => {
28+
const fp = join(tmp, 'styles.css');
29+
writeFileSync(fp, `.btn{color:#3b82f6} .bg{background:linear-gradient(#fff,#000)}`);
30+
const tw = join(tmp, 'tailwind.config.js');
31+
writeFileSync(tw, `module.exports={ theme:{ colors:{ primary:'#3b82f6' }}}`);
32+
const s = new SecurityScanner();
33+
const res = await s.scan({ directory: tmp });
34+
expect(res.some(r => r.ruleId === 'SEC018')).toBe(false);
35+
});
36+
37+
it('flags obvious secret patterns for SEC018', async () => {
38+
const fp = join(tmp, 'app.ts');
39+
writeFileSync(fp, `const k = 'sk-1234567890abcdefZZZZ'; const jwt = 'eyJhbGciOiJIUzI1NiIs.eyJzdWIiOiIx'.repeat(1);`);
40+
const s = new SecurityScanner();
41+
const res = await s.scan({ directory: tmp });
42+
expect(res.some(r => r.ruleId === 'SEC018')).toBe(true);
43+
});
2644
});
2745

2846

src/cli.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ program
8383
.option('--crawl-start-url <url>', 'Starting URL for internal crawl')
8484
.option('--crawl-depth <n>', 'Max crawl depth', '2')
8585
.option('--crawl-timeout <ms>', 'Per-page timeout in ms', '10000')
86+
.option('--detailed', 'Show all findings including lower-confidence/noisy ones')
87+
.option('--focus-critical', 'Only show critical (high severity) issues')
88+
.option('--focus-security', 'Only show security issues (hide a11y/links/etc)')
89+
.option('--focus-new', 'Only show issues not in baseline')
8690
.action(async (options) => {
8791
const scanner = new UbonScan(options.verbose, options.json);
8892

@@ -106,7 +110,11 @@ program
106110
crawlInternal: !!options.crawlInternal,
107111
crawlStartUrl: options.crawlStartUrl,
108112
crawlDepth: options.crawlDepth ? parseInt(options.crawlDepth) : undefined,
109-
crawlTimeoutMs: options.crawlTimeout ? parseInt(options.crawlTimeout) : undefined
113+
crawlTimeoutMs: options.crawlTimeout ? parseInt(options.crawlTimeout) : undefined,
114+
detailed: !!options.detailed,
115+
focusCritical: !!options.focusCritical,
116+
focusSecurity: !!options.focusSecurity,
117+
focusNew: !!options.focusNew
110118
};
111119
const scanOptions = mergeOptions(config, cliOptions);
112120

@@ -202,6 +210,10 @@ program
202210
.option('--crawl-start-url <url>', 'Starting URL for internal crawl')
203211
.option('--crawl-depth <n>', 'Max crawl depth', '2')
204212
.option('--crawl-timeout <ms>', 'Per-page timeout in ms', '10000')
213+
.option('--detailed', 'Show all findings including lower-confidence/noisy ones')
214+
.option('--focus-critical', 'Only show critical (high severity) issues')
215+
.option('--focus-security', 'Only show security issues (hide a11y/links/etc)')
216+
.option('--focus-new', 'Only show issues not in baseline')
205217
.action(async (options) => {
206218
const scanner = new UbonScan(options.verbose, options.json);
207219

@@ -224,7 +236,11 @@ program
224236
crawlInternal: !!options.crawlInternal,
225237
crawlStartUrl: options.crawlStartUrl,
226238
crawlDepth: options.crawlDepth ? parseInt(options.crawlDepth) : undefined,
227-
crawlTimeoutMs: options.crawlTimeout ? parseInt(options.crawlTimeout) : undefined
239+
crawlTimeoutMs: options.crawlTimeout ? parseInt(options.crawlTimeout) : undefined,
240+
detailed: !!options.detailed,
241+
focusCritical: !!options.focusCritical,
242+
focusSecurity: !!options.focusSecurity,
243+
focusNew: !!options.focusNew
228244
};
229245
const scanOptions = mergeOptions(config, cliOptions);
230246

src/index.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ export class UbonScan {
3737
}
3838
// Select scanners based on profile
3939
this.scanners = this.resolveScanners(profile as any, options.fast);
40+
41+
// Runtime, non-persistent defaults for human-friendly noise reduction
42+
if (!options.json && options.profile !== 'python') {
43+
// If user didn't set minConfidence, prefer a gentle default
44+
if (typeof options.minConfidence !== 'number') {
45+
(options as any).minConfidence = 0.8;
46+
}
47+
}
4048
const allResults: ScanResult[] = [];
4149

4250
// Run static file scanners
@@ -93,7 +101,8 @@ export class UbonScan {
93101

94102
const filtered = this.filterResults(allResults, options);
95103
const withFingerprints = filtered.map(r => ({ ...r, fingerprint: this.computeFingerprint(r) }));
96-
const finalResults = await this.applyBaseline(withFingerprints, options);
104+
const afterBaseline = await this.applyBaseline(withFingerprints, options);
105+
const finalResults = this.applyFocusFilters(afterBaseline, options);
97106
return this.sortResults(finalResults);
98107
}
99108

@@ -117,6 +126,13 @@ export class UbonScan {
117126
return;
118127
}
119128

129+
// Severity-first header
130+
const errorCount = results.filter(r => r.type === 'error').length;
131+
const warnCount = results.filter(r => r.type === 'warning').length;
132+
const criticalCount = results.filter(r => r.severity === 'high').length;
133+
const highText = criticalCount > 0 ? chalk.bgRed.white(` ${criticalCount} CRITICAL `) : '';
134+
console.log(`\n${chalk.hex('#c99cb3')('🪷')} ${chalk.bold('Triage')}: ${highText} ${chalk.red(errorCount + ' errors')}, ${chalk.yellow(warnCount + ' warnings')}`);
135+
120136
this.logger.separator();
121137
this.logger.title(`Found ${results.length} issues:`);
122138

@@ -205,6 +221,24 @@ export class UbonScan {
205221
return filtered;
206222
}
207223

224+
private applyFocusFilters(results: ScanResult[], options: ScanOptions): ScanResult[] {
225+
let out = results;
226+
if (options.focusNew) {
227+
// already applied baseline; no-op here since baseline removed old issues
228+
}
229+
if (options.focusSecurity) {
230+
out = out.filter(r => r.category === 'security');
231+
}
232+
if (options.focusCritical) {
233+
out = out.filter(r => r.severity === 'high');
234+
}
235+
if (!options.detailed && typeof options.minConfidence !== 'number') {
236+
// gentle noise reduction when not detailed: default minConfidence 0.8 for human runs
237+
out = out.filter(r => (r.confidence ?? 1) >= 0.8);
238+
}
239+
return out;
240+
}
241+
208242
private computeFingerprint(result: ScanResult): string {
209243
const hash = createHash('sha256');
210244
const normalizedPath = (result.file || '').replace(/\\/g, '/');

src/scanners/security-scanner.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -299,27 +299,45 @@ export class SecurityScanner implements Scanner {
299299
}
300300
}
301301
});
302-
// Entropy-based secret detection
302+
// Entropy-based secret detection (context-aware, reduced noise)
303303
lines.forEach((line, index) => {
304304
const toks = extractQuotedLiterals(line).filter(s => s.length >= 16);
305305
for (const tok of toks) {
306306
const ent = shannonEntropy(tok);
307-
if (ent >= 3.5 && /[A-Za-z0-9]/.test(tok)) {
308-
const meta = RULES.SEC018;
309-
results.push({
310-
type: meta.severity === 'high' ? 'error' : 'warning',
311-
category: meta.category,
312-
message: meta.message,
313-
file,
314-
line: index + 1,
315-
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
316-
severity: meta.severity,
317-
ruleId: meta.id,
318-
match: tok.slice(0, 200),
319-
confidence: 0.8,
320-
fix: meta.fix
321-
});
322-
}
307+
if (ent < 3.8 || !/[A-Za-z0-9]/.test(tok)) continue;
308+
309+
// File/context-based ignores: CSS/Tailwind, configs, globs
310+
const lowerFile = file.toLowerCase();
311+
const isCssContext = lowerFile.endsWith('.css') || lowerFile.endsWith('.scss') || lowerFile.endsWith('.sass') || lowerFile.endsWith('.less') || lowerFile.includes('tailwind.config');
312+
if (isCssContext) continue;
313+
314+
// Token-based ignores: hex colors, tailwind classes, data URIs, globs, UUIDs
315+
const isHexColor = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(tok);
316+
const isTailwind = /(bg|text|border|from|to|via)-[a-zA-Z]+-\d{2,3}/.test(tok);
317+
const isDataUri = /^data:image\//.test(tok);
318+
const isGlobLike = /\*\*?|\{.*\}|\*\.[a-zA-Z0-9]+/.test(tok);
319+
const isUuid = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/.test(tok);
320+
if (isHexColor || isTailwind || isDataUri || isGlobLike || isUuid) continue;
321+
322+
// Suspicious indicators increase confidence
323+
const looksLikeSecret = /\b(sk-|pk_live_|rk_(live|test)_|eyJ[A-Za-z0-9._-]{10,}|AKIA[0-9A-Z]{16}|password=|secret=|api_key=|token=|postgres(ql)?:\/\/|mongodb:\/\/)/.test(tok);
324+
const isDotEnvFile = /(^|\/)\.env(\.|$)/.test(lowerFile);
325+
if (!looksLikeSecret && !isDotEnvFile) continue;
326+
327+
const meta = RULES.SEC018;
328+
results.push({
329+
type: meta.severity === 'high' ? 'error' : 'warning',
330+
category: meta.category,
331+
message: meta.message,
332+
file,
333+
line: index + 1,
334+
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
335+
severity: meta.severity,
336+
ruleId: meta.id,
337+
match: tok.slice(0, 200),
338+
confidence: looksLikeSecret ? 0.9 : 0.8,
339+
fix: meta.fix
340+
});
323341
}
324342
});
325343

0 commit comments

Comments
 (0)