Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ That's it. No config files, no setup, no flags needed.

```
┌──────────────────────────────────────────────────┐
│ HackMyAgent v0.9.9 — Security Scanner
│ HackMyAgent v0.10.0 — Security Scanner │
│ Found: 3 critical · 5 high · 12 medium │
│ │
│ CRED-001 critical Hardcoded API key in .env │
Expand Down Expand Up @@ -108,6 +108,8 @@ hackmyagent secure --fix --dry-run # preview fixes before applying
hackmyagent secure --ignore CRED-001,GIT-002 # skip specific checks
hackmyagent secure --json # JSON output for CI/CD
hackmyagent secure --verbose # show all checks including passed
hackmyagent secure --publish # push results to OpenA2A Registry
hackmyagent secure --publish --registry-url https://registry.example.com # custom registry
```

<details>
Expand Down Expand Up @@ -183,6 +185,7 @@ hackmyagent attack --local --intensity aggressive # full payload su
hackmyagent attack --local -f sarif -o results.sarif # SARIF output
hackmyagent attack https://api.example.com --fail-on-vulnerable medium # CI gate
hackmyagent attack https://api.example.com --api-format anthropic # Anthropic API format
hackmyagent attack --local --publish # push red-team results to OpenA2A Registry
```

| Category | Payloads | Description |
Expand Down Expand Up @@ -289,6 +292,7 @@ hackmyagent scan-soul # scan current directory
hackmyagent scan-soul --tier MULTI-AGENT # override tier detection
hackmyagent scan-soul --deep # LLM semantic analysis (requires ANTHROPIC_API_KEY)
hackmyagent scan-soul --fail-below 60 # CI gate
hackmyagent scan-soul --publish # push governance results to OpenA2A Registry
```

Auto-detects governance file: `SOUL.md` > `system-prompt.md` > `CLAUDE.md` > `.cursorrules` > `agent-config.yaml`.
Expand Down Expand Up @@ -436,6 +440,20 @@ All platforms are scanned automatically — no flags needed.

---

## Registry Integration

The `--publish` flag pushes scan results to the [OpenA2A Registry](https://registry.opena2a.org), building a shared trust database for AI agent security. Available on `secure`, `attack`, and `scan-soul` commands.

```bash
hackmyagent secure ./my-agent --publish
```

When signing keys are configured (via `opena2a claim`), results are published at full weight. Without signing keys, results are accepted as community contributions at 0.5x weight. The CLI shows guidance on how to claim your agent for full-weight publishing.

Use `--registry-url` to publish to a custom registry endpoint (e.g., a private organizational registry).

---

## CI/CD Integration

All commands support `--json` and `--ci` flags.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hackmyagent",
"version": "0.9.9",
"version": "0.10.0",
"description": "Find it. Break it. Fix it. The hacker's toolkit for AI agents.",
"bin": {
"hackmyagent": "dist/cli.js"
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Unified security toolkit for AI agents.
*/

export const VERSION = '0.9.9';
export const VERSION = '0.10.0';

// Checker module
export {
Expand Down
53 changes: 44 additions & 9 deletions src/scanner/external-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,40 @@ function calculateGrade(score: number): string {
return 'F';
}

function isPrivateOrReserved(hostname: string): boolean {
if (hostname === '169.254.169.254' || hostname === 'metadata.google.internal') return true;
if (net.isIPv4(hostname)) {
const parts = hostname.split('.').map(Number);
if (parts[0] === 10) return true;
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
if (parts[0] === 192 && parts[1] === 168) return true;
if (parts[0] === 0) return true;
}
return false;
}

function validateTarget(target: string): void {
// Validate protocol if a full URL was provided
if (target.includes('://')) {
const protocol = target.split('://')[0].toLowerCase();
if (protocol !== 'http' && protocol !== 'https') {
throw new Error(`Unsupported protocol "${protocol}". Only http and https are allowed.`);
}
}
}

export class ExternalScanner {
async scan(target: string, options?: ScannerOptions): Promise<ExternalScanResult> {
// Validate protocol (block file://, gopher://, etc.)
validateTarget(target);

// Extract hostname for private IP warning
const hostname = target.replace(/^https?:\/\//, '').split(/[:/]/)[0];
if (isPrivateOrReserved(hostname)) {
// Log warning but allow — scanning local services is a core use case for security testing
console.warn(`[HMA] Warning: scanning private/reserved address "${hostname}". Ensure you have authorization.`);
}

const startTime = Date.now();
const timeout = options?.timeout ?? 5000;
const ports = options?.ports ?? DEFAULT_PORTS;
Expand All @@ -74,8 +106,9 @@ export class ExternalScanner {
// Run security checks on open ports
const findings: ExternalFinding[] = [];

const insecure = options?.insecure === true;
for (const port of openPorts) {
const portFindings = await this.checkPort(target, port, timeout);
const portFindings = await this.checkPort(target, port, timeout, insecure);
findings.push(...portFindings);
}

Expand Down Expand Up @@ -148,15 +181,16 @@ export class ExternalScanner {
private async checkPort(
target: string,
port: number,
timeout: number
timeout: number,
insecure = false
): Promise<ExternalFinding[]> {
const findings: ExternalFinding[] = [];
const useHttps = port === 443;
const baseUrl = `http${useHttps ? 's' : ''}://${target}:${port}`;

// Check MCP SSE endpoints
for (const path of MCP_SSE_PATHS) {
const result = await this.httpProbe(baseUrl + path, timeout);
const result = await this.httpProbe(baseUrl + path, timeout, insecure);
if (result && result.contentType?.includes('text/event-stream')) {
findings.push({
id: generateId(),
Expand All @@ -176,7 +210,7 @@ export class ExternalScanner {

// Check MCP tools endpoints
for (const path of MCP_TOOLS_PATHS) {
const result = await this.httpProbe(baseUrl + path, timeout);
const result = await this.httpProbe(baseUrl + path, timeout, insecure);
if (result && result.status === 200 && result.body?.includes('tools')) {
findings.push({
id: generateId(),
Expand All @@ -196,7 +230,7 @@ export class ExternalScanner {

// Check config files
for (const path of CONFIG_PATHS) {
const result = await this.httpProbe(baseUrl + path, timeout);
const result = await this.httpProbe(baseUrl + path, timeout, insecure);
if (result && result.status === 200 && result.body) {
// Check if it looks like JSON config
if (
Expand All @@ -221,7 +255,7 @@ export class ExternalScanner {

// Check CLAUDE.md
for (const path of CLAUDE_MD_PATHS) {
const result = await this.httpProbe(baseUrl + path, timeout);
const result = await this.httpProbe(baseUrl + path, timeout, insecure);
if (result && result.status === 200 && result.body) {
findings.push({
id: generateId(),
Expand All @@ -240,7 +274,7 @@ export class ExternalScanner {
}

// Check root path for API keys in responses
const rootResult = await this.httpProbe(baseUrl + '/', timeout);
const rootResult = await this.httpProbe(baseUrl + '/', timeout, insecure);
if (rootResult && rootResult.body) {
for (const { name, pattern } of API_KEY_PATTERNS) {
if (pattern.test(rootResult.body)) {
Expand All @@ -266,7 +300,8 @@ export class ExternalScanner {

private httpProbe(
url: string,
timeout: number
timeout: number,
insecure = false
): Promise<{ status: number; contentType?: string; body?: string } | null> {
return new Promise((resolve) => {
const isHttps = url.startsWith('https://');
Expand All @@ -280,7 +315,7 @@ export class ExternalScanner {
'User-Agent': 'HackMyAgent-Scanner/1.0',
'ngrok-skip-browser-warning': 'true',
},
rejectUnauthorized: false,
rejectUnauthorized: !insecure,
},
(res) => {
let body = '';
Expand Down
1 change: 1 addition & 0 deletions src/scanner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export interface ScannerOptions {
timeout?: number;
ports?: number[];
skipPortScan?: boolean;
insecure?: boolean;
}
5 changes: 3 additions & 2 deletions src/soul/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { execSync, execFileSync } from 'child_process';
import { DOMAIN_TEMPLATES } from './templates';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -559,7 +559,8 @@ export class SoulScanner {
const tmpFile = path.join(require('os').tmpdir(), `soul-deep-${Date.now()}.txt`);
fs.writeFileSync(tmpFile, prompt, 'utf-8');
try {
const result = execSync(`${claudePath} --print "$(cat ${tmpFile})"`, {
const promptContent = fs.readFileSync(tmpFile, 'utf-8');
const result = execFileSync(claudePath, ['--print', promptContent], {
encoding: 'utf-8',
timeout: 15000,
stdio: ['pipe', 'pipe', 'ignore'],
Expand Down
Loading