Skip to content

Commit c08aaaa

Browse files
committed
sync: update from internal repo (2026-02-15 20:25)
1 parent 5bbf287 commit c08aaaa

12 files changed

Lines changed: 7439 additions & 35 deletions

File tree

.pre-commit-hooks.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- id: agent-security-scan
2+
name: Agent Security Scan
3+
description: Scan for AI agent security vulnerabilities
4+
entry: npx --yes @empowered-humanity/agent-security scan
5+
language: system
6+
pass_filenames: false
7+
always_run: true

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
[![npm version](https://img.shields.io/npm/v/@empowered-humanity/agent-security)](https://www.npmjs.com/package/@empowered-humanity/agent-security)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-gold.svg)](https://opensource.org/licenses/MIT)
66
[![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue.svg)](https://www.typescriptlang.org/)
7-
[![Tests](https://img.shields.io/badge/Tests-116%20passing-brightgreen.svg)]()
8-
[![Patterns](https://img.shields.io/badge/Patterns-176-navy.svg)]()
7+
[![Tests](https://img.shields.io/badge/Tests-123%20passing-brightgreen.svg)]()
8+
[![Patterns](https://img.shields.io/badge/Patterns-190-navy.svg)]()
99

1010
Security scanner for AI agent architectures. Detects prompt injection, credential exposure, code injection, and agent-specific attack patterns.
1111

1212
## What It Detects
1313

14-
**176 detection patterns** across 5 scanner categories:
14+
**190 detection patterns** across 5 scanner categories:
1515

1616
### 1. Prompt Injection (34 patterns)
1717
- Instruction override attempts
@@ -61,16 +61,16 @@ The scanner implements detection for all 10 OWASP Agentic Security Issues:
6161

6262
| OWASP ASI | Category | Patterns | Description |
6363
|-----------|----------|----------|-------------|
64-
| **ASI01** | Goal Hijacking | 2 | Malicious objectives override primary goals |
65-
| **ASI02** | Tool Misuse | 1 | Unauthorized tool access or API abuse |
66-
| **ASI03** | Privilege Abuse | 2 | Escalation beyond granted permissions |
67-
| **ASI04** | Supply Chain | 1 | Compromised dependencies or data sources |
68-
| **ASI05** | Remote Code Execution | 1 | Command injection, arbitrary code execution |
69-
| **ASI06** | Memory Poisoning | 2 | RAG corruption, persistent instruction injection |
70-
| **ASI07** | Insecure Communications | 1 | Unencrypted channels, data exfiltration |
71-
| **ASI08** | Cascading Failures | 2 | Error amplification, chain-reaction exploits |
72-
| **ASI09** | Trust Exploitation | 2 | Impersonation, false credentials |
73-
| **ASI10** | Rogue Agents | 2 | Self-replication, unauthorized spawning |
64+
| **ASI01** | Goal Hijacking | 6 | Malicious objectives override primary goals |
65+
| **ASI02** | Tool Misuse | 5 | Unauthorized tool access or API abuse |
66+
| **ASI03** | Privilege Abuse | 4 | Escalation beyond granted permissions |
67+
| **ASI04** | Supply Chain | 3 | Compromised dependencies or data sources |
68+
| **ASI05** | Remote Code Execution | 3 | Command injection, arbitrary code execution |
69+
| **ASI06** | Memory Poisoning | 10 | RAG corruption, persistent instruction injection, unicode hidden, embedding drift |
70+
| **ASI07** | Insecure Communications | 9 | Unencrypted channels, data exfiltration, message replay |
71+
| **ASI08** | Cascading Failures | 9 | Error amplification, chain-reaction exploits, circuit breaker bypass |
72+
| **ASI09** | Trust Exploitation | 8 | Impersonation, false credentials, YMYL decision override |
73+
| **ASI10** | Rogue Agents | 8 | Self-replication, unauthorized spawning, behavioral drift, silent approval |
7474

7575
## Installation
7676

@@ -183,7 +183,7 @@ security_scan:
183183

184184
## Pattern Categories
185185

186-
The 176 patterns are organized into these categories:
186+
The 190 patterns are organized into these categories:
187187

188188
| Category | Count | Severity |
189189
|----------|-------|----------|

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ When using agent-security in your projects:
5959

6060
## Security Features
6161

62-
- **Pattern-based detection**: 176 security patterns with 4 intelligence layers
62+
- **Pattern-based detection**: 190 security patterns with 4 intelligence layers
6363
- **OWASP ASI coverage**: All 10 OWASP Agentic Security Issues
6464
- **No network calls**: All scanning happens locally
6565
- **No data collection**: Your code never leaves your machine

action.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: 'Agent Security Scan'
2+
description: 'Scan for AI agent security vulnerabilities with 176+ detection patterns'
3+
author: 'Empowered Humanity'
4+
5+
inputs:
6+
path:
7+
description: 'Path to scan'
8+
required: false
9+
default: '.'
10+
severity:
11+
description: 'Minimum severity to report (critical, high, medium, low)'
12+
required: false
13+
default: 'medium'
14+
format:
15+
description: 'Output format (console, json, sarif)'
16+
required: false
17+
default: 'sarif'
18+
fail-on-findings:
19+
description: 'Fail if findings at or above this severity (critical, high, medium, low)'
20+
required: false
21+
default: 'high'
22+
upload-sarif:
23+
description: 'Upload SARIF results to GitHub Code Scanning'
24+
required: false
25+
default: 'true'
26+
27+
outputs:
28+
findings-count:
29+
description: 'Total number of findings'
30+
value: ${{ steps.scan.outputs.findings-count }}
31+
risk-level:
32+
description: 'Overall risk level'
33+
value: ${{ steps.scan.outputs.risk-level }}
34+
sarif-file:
35+
description: 'Path to SARIF output file'
36+
value: ${{ steps.scan.outputs.sarif-file }}
37+
38+
runs:
39+
using: 'composite'
40+
steps:
41+
- name: Setup Node.js
42+
uses: actions/setup-node@v4
43+
with:
44+
node-version: '20'
45+
46+
- name: Run Agent Security Scan
47+
id: scan
48+
shell: bash
49+
run: |
50+
SARIF_FILE="${{ runner.temp }}/agent-security-results.sarif"
51+
52+
npx --yes @empowered-humanity/agent-security@latest scan \
53+
${{ inputs.path }} \
54+
--severity ${{ inputs.severity }} \
55+
--format ${{ inputs.format }} \
56+
--fail-on ${{ inputs.fail-on-findings }} \
57+
--output "$SARIF_FILE" \
58+
|| SCAN_EXIT=$?
59+
60+
if [ -f "$SARIF_FILE" ]; then
61+
FINDINGS=$(cat "$SARIF_FILE" | node -e "
62+
const fs = require('fs');
63+
const data = JSON.parse(fs.readFileSync(0, 'utf8'));
64+
console.log(data.runs[0].results.length);
65+
")
66+
echo "findings-count=$FINDINGS" >> $GITHUB_OUTPUT
67+
echo "sarif-file=$SARIF_FILE" >> $GITHUB_OUTPUT
68+
else
69+
echo "findings-count=0" >> $GITHUB_OUTPUT
70+
echo "sarif-file=" >> $GITHUB_OUTPUT
71+
fi
72+
73+
echo "risk-level=${SCAN_EXIT:+failed}" >> $GITHUB_OUTPUT
74+
exit ${SCAN_EXIT:-0}
75+
76+
- name: Upload SARIF to GitHub Code Scanning
77+
if: ${{ inputs.upload-sarif == 'true' && always() && steps.scan.outputs.sarif-file != '' }}
78+
uses: github/codeql-action/upload-sarif@v3
79+
with:
80+
sarif_file: ${{ steps.scan.outputs.sarif-file }}
81+
category: agent-security
82+
83+
branding:
84+
icon: 'shield'
85+
color: 'blue'

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@empowered-humanity/agent-security",
3-
"version": "1.1.0",
4-
"description": "Security scanner for AI agent architectures - 176 detection patterns for prompt injection, credential exposure, MCP security, and OWASP ASI vulnerabilities",
3+
"version": "1.2.0",
4+
"description": "Security scanner for AI agent architectures - 190 detection patterns for prompt injection, credential exposure, MCP security, and OWASP ASI vulnerabilities",
55
"type": "module",
66
"main": "dist/index.js",
77
"types": "dist/index.d.ts",
@@ -32,7 +32,9 @@
3232
"README.md",
3333
"LICENSE",
3434
"SECURITY.md",
35-
"sbom.json"
35+
"sbom.json",
36+
"action.yml",
37+
".pre-commit-hooks.yaml"
3638
],
3739
"scripts": {
3840
"build": "tsc",

src/index.ts

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ import { resolve } from 'path';
2020
import { scanDirectory, scanFile, scanContent } from './scanner/index.js';
2121
import { printScanResult, formatScanResult } from './reporters/console.js';
2222
import { 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

2829
program
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

Comments
 (0)