| import type { ProjectScanResult } from './project-scan.js'; | ||
| export interface ShellwardPolicy { | ||
| /** 命中即失败:可填类别(secret/pii/overseas/env-perm) 或 严重度(critical/high/medium) */ | ||
| failOn?: string[]; | ||
| /** 总发现数上限(含被 allowOverseas 豁免后的) */ | ||
| maxFindings?: number; | ||
| /** 允许的境外大模型厂商(provider 名,命中这些的 overseas 发现被豁免) */ | ||
| allowOverseas?: string[]; | ||
| } | ||
| export interface PolicyResult { | ||
| pass: boolean; | ||
| source: 'file' | 'default'; | ||
| violations: string[]; | ||
| policy: ShellwardPolicy; | ||
| } | ||
| /** 读取项目根的 .shellward.json;无/坏则返回默认策略(有 critical 即失败) */ | ||
| export declare function loadPolicy(root: string): { | ||
| policy: ShellwardPolicy; | ||
| source: 'file' | 'default'; | ||
| }; | ||
| /** 根据策略评估扫描结果,返回是否通过 + 违规说明 */ | ||
| export declare function evaluatePolicy(scan: ProjectScanResult, policy: ShellwardPolicy): PolicyResult; | ||
| /** 加载 + 评估的便捷封装 */ | ||
| export declare function checkPolicy(scan: ProjectScanResult, root: string): PolicyResult; |
| // src/compliance/policy.ts — policy-as-code 门禁(响应 GitHub issue #2) | ||
| // | ||
| // 在 Git/CI 边界用声明式策略约束扫描结果:项目根放 `.shellward.json`, | ||
| // CI(shellward scan --ci)据此判定通过/失败。把"策略在 push 时声明 → 运行时执行" | ||
| // 的纵深防御补上 push 这一端。无策略文件时回退到默认(有 critical 即失败)。 | ||
| // | ||
| // 示例 .shellward.json: | ||
| // { | ||
| // "failOn": ["secret", "pii"], // 命中这些"类别"或"严重度"即失败 | ||
| // "maxFindings": 0, // 总发现数上限 | ||
| // "allowOverseas": ["OpenAI"] // 允许的境外厂商(不计入失败) | ||
| // } | ||
| import { readFileSync } from 'fs'; | ||
| import { join } from 'path'; | ||
| const KINDS = ['overseas', 'secret', 'pii', 'env-perm']; | ||
| const SEVERITIES = ['critical', 'high', 'medium']; | ||
| /** 读取项目根的 .shellward.json;无/坏则返回默认策略(有 critical 即失败) */ | ||
| export function loadPolicy(root) { | ||
| try { | ||
| const raw = readFileSync(join(root, '.shellward.json'), 'utf-8'); | ||
| const p = JSON.parse(raw); | ||
| if (p && typeof p === 'object') | ||
| return { policy: sanitize(p), source: 'file' }; | ||
| } | ||
| catch { /* 无策略文件或解析失败 → 默认 */ } | ||
| return { policy: { failOn: ['critical'] }, source: 'default' }; | ||
| } | ||
| function sanitize(p) { | ||
| const out = {}; | ||
| if (Array.isArray(p.failOn)) | ||
| out.failOn = p.failOn.filter((x) => typeof x === 'string'); | ||
| if (typeof p.maxFindings === 'number' && p.maxFindings >= 0) | ||
| out.maxFindings = Math.floor(p.maxFindings); | ||
| if (Array.isArray(p.allowOverseas)) | ||
| out.allowOverseas = p.allowOverseas.filter((x) => typeof x === 'string'); | ||
| return out; | ||
| } | ||
| /** 把被 allowOverseas 豁免的 overseas 发现去掉 */ | ||
| function effective(findings, allow) { | ||
| if (!allow.length) | ||
| return findings; | ||
| const allowLower = new Set(allow.map(a => a.toLowerCase())); | ||
| return findings.filter(f => { | ||
| if (f.kind !== 'overseas') | ||
| return true; | ||
| const prov = (f.provider_en || f.provider_zh || '').toLowerCase(); | ||
| return !allowLower.has(prov); | ||
| }); | ||
| } | ||
| /** 根据策略评估扫描结果,返回是否通过 + 违规说明 */ | ||
| export function evaluatePolicy(scan, policy) { | ||
| const allow = policy.allowOverseas || []; | ||
| const findings = effective(scan.findings, allow); | ||
| const violations = []; | ||
| const failOn = policy.failOn || []; | ||
| for (const token of failOn) { | ||
| if (KINDS.includes(token)) { | ||
| const n = findings.filter(f => f.kind === token).length; | ||
| if (n > 0) | ||
| violations.push(`failOn "${token}": 命中 ${n} 项`); | ||
| } | ||
| else if (SEVERITIES.includes(token)) { | ||
| const n = findings.filter(f => f.severity === token).length; | ||
| if (n > 0) | ||
| violations.push(`failOn 严重度 "${token}": 命中 ${n} 项`); | ||
| } | ||
| } | ||
| if (typeof policy.maxFindings === 'number' && findings.length > policy.maxFindings) { | ||
| violations.push(`发现数 ${findings.length} 超过上限 maxFindings=${policy.maxFindings}`); | ||
| } | ||
| return { pass: violations.length === 0, source: 'default', violations, policy }; | ||
| } | ||
| /** 加载 + 评估的便捷封装 */ | ||
| export function checkPolicy(scan, root) { | ||
| const { policy, source } = loadPolicy(root); | ||
| const r = evaluatePolicy(scan, policy); | ||
| r.source = source; | ||
| return r; | ||
| } |
| // src/compliance/policy.ts — policy-as-code 门禁(响应 GitHub issue #2) | ||
| // | ||
| // 在 Git/CI 边界用声明式策略约束扫描结果:项目根放 `.shellward.json`, | ||
| // CI(shellward scan --ci)据此判定通过/失败。把"策略在 push 时声明 → 运行时执行" | ||
| // 的纵深防御补上 push 这一端。无策略文件时回退到默认(有 critical 即失败)。 | ||
| // | ||
| // 示例 .shellward.json: | ||
| // { | ||
| // "failOn": ["secret", "pii"], // 命中这些"类别"或"严重度"即失败 | ||
| // "maxFindings": 0, // 总发现数上限 | ||
| // "allowOverseas": ["OpenAI"] // 允许的境外厂商(不计入失败) | ||
| // } | ||
| import { readFileSync } from 'fs' | ||
| import { join } from 'path' | ||
| import type { ProjectScanResult, ProjectFinding, FindingKind } from './project-scan.js' | ||
| export interface ShellwardPolicy { | ||
| /** 命中即失败:可填类别(secret/pii/overseas/env-perm) 或 严重度(critical/high/medium) */ | ||
| failOn?: string[] | ||
| /** 总发现数上限(含被 allowOverseas 豁免后的) */ | ||
| maxFindings?: number | ||
| /** 允许的境外大模型厂商(provider 名,命中这些的 overseas 发现被豁免) */ | ||
| allowOverseas?: string[] | ||
| } | ||
| export interface PolicyResult { | ||
| pass: boolean | ||
| source: 'file' | 'default' | ||
| violations: string[] | ||
| policy: ShellwardPolicy | ||
| } | ||
| const KINDS: FindingKind[] = ['overseas', 'secret', 'pii', 'env-perm'] | ||
| const SEVERITIES = ['critical', 'high', 'medium'] | ||
| /** 读取项目根的 .shellward.json;无/坏则返回默认策略(有 critical 即失败) */ | ||
| export function loadPolicy(root: string): { policy: ShellwardPolicy; source: 'file' | 'default' } { | ||
| try { | ||
| const raw = readFileSync(join(root, '.shellward.json'), 'utf-8') | ||
| const p = JSON.parse(raw) | ||
| if (p && typeof p === 'object') return { policy: sanitize(p), source: 'file' } | ||
| } catch { /* 无策略文件或解析失败 → 默认 */ } | ||
| return { policy: { failOn: ['critical'] }, source: 'default' } | ||
| } | ||
| function sanitize(p: any): ShellwardPolicy { | ||
| const out: ShellwardPolicy = {} | ||
| if (Array.isArray(p.failOn)) out.failOn = p.failOn.filter((x: any) => typeof x === 'string') | ||
| if (typeof p.maxFindings === 'number' && p.maxFindings >= 0) out.maxFindings = Math.floor(p.maxFindings) | ||
| if (Array.isArray(p.allowOverseas)) out.allowOverseas = p.allowOverseas.filter((x: any) => typeof x === 'string') | ||
| return out | ||
| } | ||
| /** 把被 allowOverseas 豁免的 overseas 发现去掉 */ | ||
| function effective(findings: ProjectFinding[], allow: string[]): ProjectFinding[] { | ||
| if (!allow.length) return findings | ||
| const allowLower = new Set(allow.map(a => a.toLowerCase())) | ||
| return findings.filter(f => { | ||
| if (f.kind !== 'overseas') return true | ||
| const prov = (f.provider_en || f.provider_zh || '').toLowerCase() | ||
| return !allowLower.has(prov) | ||
| }) | ||
| } | ||
| /** 根据策略评估扫描结果,返回是否通过 + 违规说明 */ | ||
| export function evaluatePolicy(scan: ProjectScanResult, policy: ShellwardPolicy): PolicyResult { | ||
| const allow = policy.allowOverseas || [] | ||
| const findings = effective(scan.findings, allow) | ||
| const violations: string[] = [] | ||
| const failOn = policy.failOn || [] | ||
| for (const token of failOn) { | ||
| if (KINDS.includes(token as FindingKind)) { | ||
| const n = findings.filter(f => f.kind === token).length | ||
| if (n > 0) violations.push(`failOn "${token}": 命中 ${n} 项`) | ||
| } else if (SEVERITIES.includes(token)) { | ||
| const n = findings.filter(f => f.severity === token).length | ||
| if (n > 0) violations.push(`failOn 严重度 "${token}": 命中 ${n} 项`) | ||
| } | ||
| } | ||
| if (typeof policy.maxFindings === 'number' && findings.length > policy.maxFindings) { | ||
| violations.push(`发现数 ${findings.length} 超过上限 maxFindings=${policy.maxFindings}`) | ||
| } | ||
| return { pass: violations.length === 0, source: 'default', violations, policy } | ||
| } | ||
| /** 加载 + 评估的便捷封装 */ | ||
| export function checkPolicy(scan: ProjectScanResult, root: string): PolicyResult { | ||
| const { policy, source } = loadPolicy(root) | ||
| const r = evaluatePolicy(scan, policy) | ||
| r.source = source | ||
| return r | ||
| } |
+11
-3
@@ -20,2 +20,3 @@ #!/usr/bin/env node | ||
| import { renderHtmlReport } from './compliance/html-report.js'; | ||
| import { checkPolicy } from './compliance/policy.js'; | ||
| import { runInit } from './init.js'; | ||
@@ -153,6 +154,13 @@ import { resolveLocale } from './types.js'; | ||
| } | ||
| // CI 模式:有 critical 项目发现则非零退出 | ||
| // CI 模式:按 .shellward.json 策略门禁(无策略文件则默认"有 critical 即失败") | ||
| if (ci) { | ||
| const criticals = scan.findings.filter(f => f.severity === 'critical').length; | ||
| if (criticals > 0) | ||
| const pol = checkPolicy(scan, root); | ||
| if (!json) { | ||
| process.stdout.write(zh | ||
| ? `\n🔒 策略门禁(${pol.source === 'file' ? '.shellward.json' : '默认'}):${pol.pass ? '✅ 通过' : '❌ 未通过'}\n` | ||
| : `\n🔒 Policy gate (${pol.source === 'file' ? '.shellward.json' : 'default'}): ${pol.pass ? '✅ pass' : '❌ fail'}\n`); | ||
| for (const v of pol.violations) | ||
| process.stdout.write(` - ${v}\n`); | ||
| } | ||
| if (!pol.pass) | ||
| process.exit(1); | ||
@@ -159,0 +167,0 @@ } |
+1
-1
| { | ||
| "name": "shellward", | ||
| "version": "0.7.20", | ||
| "version": "0.7.21", | ||
| "mcpName": "io.github.jnMetaCode/shellward", | ||
@@ -5,0 +5,0 @@ "description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.", |
+19
-1
@@ -11,3 +11,3 @@ <p align="center"> | ||
| [](./LICENSE) | ||
| [](#performance) | ||
| [](#performance) | ||
| [](#performance) | ||
@@ -225,2 +225,20 @@ | ||
| ### Policy-as-code (`.shellward.json`) | ||
| 声明式 CI 门禁([issue #2](https://github.com/jnMetaCode/shellward/issues/2))— put a `.shellward.json` in your repo root: | ||
| ```json | ||
| { | ||
| "failOn": ["secret", "pii"], | ||
| "maxFindings": 0, | ||
| "allowOverseas": ["OpenAI"] | ||
| } | ||
| ``` | ||
| - `failOn` — fail CI if any finding matches these **kinds** (`secret`/`pii`/`overseas`/`env-perm`) or **severities** (`critical`/`high`/`medium`) | ||
| - `maxFindings` — max total findings allowed | ||
| - `allowOverseas` — overseas providers explicitly permitted (exempt from failure) | ||
| `shellward scan --ci` reads it; without the file it defaults to "fail on any critical". 实现「策略在 Git push 时声明 → 运行时执行」的纵深防御。 | ||
| ## 8-Layer Defense | ||
@@ -227,0 +245,0 @@ |
+10
-3
@@ -21,2 +21,3 @@ #!/usr/bin/env node | ||
| import { renderHtmlReport } from './compliance/html-report.js' | ||
| import { checkPolicy } from './compliance/policy.js' | ||
| import { runInit } from './init.js' | ||
@@ -166,6 +167,12 @@ import { resolveLocale } from './types.js' | ||
| // CI 模式:有 critical 项目发现则非零退出 | ||
| // CI 模式:按 .shellward.json 策略门禁(无策略文件则默认"有 critical 即失败") | ||
| if (ci) { | ||
| const criticals = scan.findings.filter(f => f.severity === 'critical').length | ||
| if (criticals > 0) process.exit(1) | ||
| const pol = checkPolicy(scan, root) | ||
| if (!json) { | ||
| process.stdout.write(zh | ||
| ? `\n🔒 策略门禁(${pol.source === 'file' ? '.shellward.json' : '默认'}):${pol.pass ? '✅ 通过' : '❌ 未通过'}\n` | ||
| : `\n🔒 Policy gate (${pol.source === 'file' ? '.shellward.json' : 'default'}): ${pol.pass ? '✅ pass' : '❌ fail'}\n`) | ||
| for (const v of pol.violations) process.stdout.write(` - ${v}\n`) | ||
| } | ||
| if (!pol.pass) process.exit(1) | ||
| } | ||
@@ -172,0 +179,0 @@ } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
724168
1.45%141
2.17%14544
1.41%606
3.06%86
2.38%