@@ -20,10 +20,11 @@ import { resolve } from 'path';
2020import { scanDirectory , scanFile , scanContent } from './scanner/index.js' ;
2121import { printScanResult , formatScanResult } from './reporters/console.js' ;
2222import { formatAsJson } from './reporters/json.js' ;
23- import { ALL_PATTERNS , getPatternStats , getPatternsByCategory } from './patterns/index.js' ;
24- import type { Severity } from './patterns/types.js' ;
23+ import { formatAsSarif } from './reporters/sarif.js' ;
24+ import { ALL_PATTERNS , getPatternStats , getPatternsByCategory , getPatternsByOwaspAsi , getPatternsMinSeverity } from './patterns/index.js' ;
25+ import type { Severity , DetectionPattern } from './patterns/types.js' ;
2526
26- const VERSION = '1.1 .0' ;
27+ const VERSION = '1.2 .0' ;
2728
2829program
2930 . name ( 'te-agent-security' )
@@ -37,23 +38,50 @@ program
3738 . option ( '-f, --file <file>' , 'Scan a single file' )
3839 . option ( '-s, --severity <level>' , 'Minimum severity (critical, high, medium, low)' , 'medium' )
3940 . option ( '-o, --output <file>' , 'Output file path' )
40- . option ( '--format <format>' , 'Output format (console, json)' , 'console' )
41+ . option ( '--format <format>' , 'Output format (console, json, sarif)' , 'console' )
42+ . option ( '--fail-on <severity>' , 'Exit with code 1 if findings at or above severity (critical, high, medium, low)' )
4143 . option ( '--context' , 'Show code context for findings' )
4244 . option ( '--group <by>' , 'Group findings by (severity, file, category, classification)' , 'severity' )
45+ . option ( '--asi <id>' , 'Filter by OWASP ASI category (e.g., ASI01, ASI06)' )
4346 . option ( '-v, --verbose' , 'Verbose output' )
4447 . option ( '-q, --quiet' , 'Quiet mode - only show errors' )
4548 . action ( async ( path , options ) => {
4649 const targetPath = options . file || path || process . cwd ( ) ;
4750 const resolvedPath = resolve ( targetPath ) ;
4851
49- const spinner = options . quiet ? null : ora ( 'Scanning for security issues...' ) . start ( ) ;
52+ // Build filtered pattern set if --asi is specified
53+ let filteredPatterns : DetectionPattern [ ] | undefined ;
54+ if ( options . asi ) {
55+ const asiId = options . asi . toUpperCase ( ) ;
56+ filteredPatterns = getPatternsByOwaspAsi ( asiId ) ;
57+ if ( filteredPatterns . length === 0 ) {
58+ console . error ( chalk . red ( `No patterns found for ASI category: ${ asiId } ` ) ) ;
59+ console . error ( chalk . gray ( 'Valid categories: ASI01-ASI10' ) ) ;
60+ process . exit ( 1 ) ;
61+ }
62+ if ( options . severity && options . severity !== 'medium' ) {
63+ const severityOrder : Severity [ ] = [ 'low' , 'medium' , 'high' , 'critical' ] ;
64+ const minIndex = severityOrder . indexOf ( options . severity as Severity ) ;
65+ filteredPatterns = filteredPatterns . filter (
66+ ( p ) => severityOrder . indexOf ( p . severity ) >= minIndex
67+ ) ;
68+ }
69+ }
70+
71+ const scanOptions = filteredPatterns
72+ ? { patterns : filteredPatterns }
73+ : { minSeverity : options . severity as Severity } ;
74+
75+ const spinner = options . quiet ? null : ora (
76+ filteredPatterns
77+ ? `Scanning for ${ options . asi . toUpperCase ( ) } patterns (${ filteredPatterns . length } rules)...`
78+ : 'Scanning for security issues...'
79+ ) . start ( ) ;
5080
5181 try {
5282 const result = options . file
5383 ? await ( async ( ) => {
54- const findings = await scanFile ( resolvedPath , {
55- minSeverity : options . severity as Severity ,
56- } ) ;
84+ const findings = await scanFile ( resolvedPath , scanOptions ) ;
5785 const criticalCount = findings . filter ( ( f ) => f . pattern . severity === 'critical' ) . length ;
5886 const highCount = findings . filter ( ( f ) => f . pattern . severity === 'high' ) . length ;
5987 const mediumCount = findings . filter ( ( f ) => f . pattern . severity === 'medium' ) . length ;
@@ -64,7 +92,7 @@ program
6492
6593 return {
6694 filesScanned : 1 ,
67- patternsChecked : ALL_PATTERNS . length ,
95+ patternsChecked : filteredPatterns ?. length ?? ALL_PATTERNS . length ,
6896 findings,
6997 riskScore : {
7098 total : 100 - findings . length * 10 ,
@@ -81,9 +109,7 @@ program
81109 timestamp : new Date ( ) ,
82110 } ;
83111 } ) ( )
84- : await scanDirectory ( resolvedPath , {
85- minSeverity : options . severity as Severity ,
86- } ) ;
112+ : await scanDirectory ( resolvedPath , scanOptions ) ;
87113
88114 spinner ?. stop ( ) ;
89115
@@ -96,6 +122,14 @@ program
96122 } else {
97123 console . log ( jsonOutput ) ;
98124 }
125+ } else if ( options . format === 'sarif' ) {
126+ const sarifOutput = formatAsSarif ( result ) ;
127+ if ( options . output ) {
128+ await writeFile ( options . output , sarifOutput ) ;
129+ console . log ( chalk . green ( `SARIF results written to ${ options . output } ` ) ) ;
130+ } else {
131+ console . log ( sarifOutput ) ;
132+ }
99133 } else {
100134 const consoleOutput = formatScanResult ( result , {
101135 showContext : options . context ,
@@ -113,8 +147,15 @@ program
113147 }
114148 }
115149
116- // Exit with error code if critical findings
117- if ( result . riskScore . counts . critical > 0 ) {
150+ // Exit with error code based on --fail-on threshold (default: critical)
151+ const severityOrder : Severity [ ] = [ 'low' , 'medium' , 'high' , 'critical' ] ;
152+ const failOn = options . failOn as Severity | undefined ;
153+ const threshold = failOn && severityOrder . includes ( failOn ) ? failOn : 'critical' ;
154+ const thresholdIndex = severityOrder . indexOf ( threshold ) ;
155+ const hasFailures = severityOrder . slice ( thresholdIndex ) . some (
156+ ( sev ) => result . riskScore . counts [ sev ] > 0
157+ ) ;
158+ if ( hasFailures ) {
118159 process . exit ( 1 ) ;
119160 }
120161 } catch ( error ) {
@@ -130,14 +171,20 @@ program
130171 . description ( 'List available detection patterns' )
131172 . option ( '-c, --category <category>' , 'Filter by category' )
132173 . option ( '-s, --severity <level>' , 'Filter by severity' )
174+ . option ( '--asi <id>' , 'Filter by OWASP ASI category (e.g., ASI01, ASI06)' )
133175 . option ( '--json' , 'Output as JSON' )
134176 . action ( ( options ) => {
135- let patterns = ALL_PATTERNS ;
177+ let patterns : DetectionPattern [ ] = ALL_PATTERNS ;
136178
137179 if ( options . category ) {
138180 patterns = getPatternsByCategory ( options . category ) ;
139181 }
140182
183+ if ( options . asi ) {
184+ const asiId = options . asi . toUpperCase ( ) ;
185+ patterns = patterns . filter ( ( p ) => p . owaspAsi === asiId ) ;
186+ }
187+
141188 if ( options . severity ) {
142189 patterns = patterns . filter ( ( p ) => p . severity === options . severity ) ;
143190 }
@@ -149,6 +196,7 @@ program
149196 name : p . name ,
150197 severity : p . severity ,
151198 category : p . category ,
199+ owaspAsi : p . owaspAsi || null ,
152200 description : p . description ,
153201 source : p . source ,
154202 } ) ) ,
@@ -175,6 +223,9 @@ program
175223 console . log ( `\n${ chalk . bold ( pattern . name ) } ` ) ;
176224 console . log ( ` Severity: ${ severityColor ( pattern . severity ) } ` ) ;
177225 console . log ( ` Category: ${ chalk . cyan ( pattern . category ) } ` ) ;
226+ if ( pattern . owaspAsi ) {
227+ console . log ( ` OWASP ASI: ${ chalk . magenta ( pattern . owaspAsi ) } ` ) ;
228+ }
178229 console . log ( ` Source: ${ chalk . gray ( pattern . source ) } ` ) ;
179230 console . log ( ` ${ pattern . description } ` ) ;
180231 if ( pattern . example ) {
@@ -210,6 +261,29 @@ program
210261 console . log ( ` Medium: ${ chalk . blue ( stats . bySeverity . medium ) } ` ) ;
211262 console . log ( ` Low: ${ chalk . gray ( stats . bySeverity . low ) } ` ) ;
212263
264+ console . log ( chalk . bold ( '\nBy OWASP ASI:' ) ) ;
265+ const asiLabels : Record < string , string > = {
266+ ASI01 : 'Agent Goal Hijack' ,
267+ ASI02 : 'Tool Misuse' ,
268+ ASI03 : 'Privilege Abuse' ,
269+ ASI04 : 'Supply Chain' ,
270+ ASI05 : 'Code Execution' ,
271+ ASI06 : 'Memory Poisoning' ,
272+ ASI07 : 'Insecure Comms' ,
273+ ASI08 : 'Cascading Failures' ,
274+ ASI09 : 'Trust Exploitation' ,
275+ ASI10 : 'Rogue Agents' ,
276+ } ;
277+ const asiIds = [ 'ASI01' , 'ASI02' , 'ASI03' , 'ASI04' , 'ASI05' , 'ASI06' , 'ASI07' , 'ASI08' , 'ASI09' , 'ASI10' ] ;
278+ let asiTotal = 0 ;
279+ for ( const asiId of asiIds ) {
280+ const count = stats . byOwaspAsi [ asiId ] || 0 ;
281+ asiTotal += count ;
282+ const bar = '\u2588' . repeat ( Math . min ( count , 20 ) ) ;
283+ console . log ( ` ${ asiId } ${ chalk . gray ( asiLabels [ asiId ] ?. padEnd ( 20 ) ?? '' ) } : ${ chalk . cyan ( String ( count ) . padStart ( 3 ) ) } ${ chalk . green ( bar ) } ` ) ;
284+ }
285+ console . log ( ` ${ chalk . gray ( 'ASI-tagged total' ) } : ${ chalk . cyan ( asiTotal ) } /${ stats . total } ` ) ;
286+
213287 console . log ( chalk . bold ( '\nBy Category:' ) ) ;
214288 const categories = Object . entries ( stats . byCategory ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) ;
215289 for ( const [ category , count ] of categories ) {
0 commit comments