chore: ARTEX 初始化
ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
commit
0ef8561411
287 files changed
+70178
No files matched your search
+188
@@ -0,0 +1,188 @@
|
||||
// Package guard implements the safety boundary layer (docs §11): side-effect
|
||||
// gating of destructive/exfil shell commands, an audit log, and Observer/G5
|
||||
// failure attribution. Every tool call passes through the PreToolUse hook before
|
||||
// executing. (The RoE authorization-scope mechanism was removed; a replacement
|
||||
// may be added later.)
|
||||
package guard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Autumn-27/norma/hook"
|
||||
"github.com/Autumn-27/artex/intercept"
|
||||
)
|
||||
|
||||
// AuditEntry records one gated tool call.
|
||||
type AuditEntry struct {
|
||||
TS int64 `json:"ts"`
|
||||
Tool string `json:"tool"`
|
||||
Action string `json:"action"` // allow|block
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
}
|
||||
|
||||
// Guard enforces the side-effect policy via agent-core hooks.
|
||||
type Guard struct {
|
||||
mu sync.Mutex
|
||||
audit []AuditEntry
|
||||
attrib map[string]int // failure attribution counts (Observer / G5)
|
||||
reg *hook.Registry
|
||||
interceptor *intercept.Interceptor // optional; nil disables user-configured rules
|
||||
}
|
||||
|
||||
// New creates a Guard without user-configured intercept rules (used for pentest
|
||||
// tasks where the Interceptor is not yet available).
|
||||
func New() *Guard { return newGuard(nil) }
|
||||
|
||||
// NewWithInterceptor creates a Guard with user-configured intercept rules.
|
||||
func NewWithInterceptor(ic *intercept.Interceptor) *Guard { return newGuard(ic) }
|
||||
|
||||
func newGuard(ic *intercept.Interceptor) *Guard {
|
||||
g := &Guard{attrib: map[string]int{}, interceptor: ic}
|
||||
g.reg = hook.NewRegistry().
|
||||
On(hook.PreToolUse, g.preToolUse).
|
||||
On(hook.PostToolUse, g.postToolUse)
|
||||
return g
|
||||
}
|
||||
|
||||
// Hooks returns the hook registry to attach to an agent session.
|
||||
func (g *Guard) Hooks() *hook.Registry { return g.reg }
|
||||
|
||||
var (
|
||||
reDestructive = regexp.MustCompile(`(?i)\b(rm\s+-rf\s+/|mkfs|dd\s+if=|:\(\)\s*\{|shutdown|reboot|>\s*/dev/sd)`)
|
||||
reExfil = regexp.MustCompile(`(?i)(curl|wget|nc|ncat)\b[^|]*\b(\|\s*(curl|wget|nc))`)
|
||||
)
|
||||
|
||||
func (g *Guard) preToolUse(ctx context.Context, ev hook.Event) hook.Result {
|
||||
// Gate the shell-command surface: Bash + the interactive-shell tools (shell_open's
|
||||
// command, shell_send's text). Same destructive/exfil rules — an interactive
|
||||
// session must not bypass the safety boundary. Other tools pass through.
|
||||
var cmd string
|
||||
switch ev.ToolName {
|
||||
case "Bash", "shell_open":
|
||||
var in struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
_ = json.Unmarshal(ev.Input, &in)
|
||||
cmd = in.Command
|
||||
case "shell_send":
|
||||
var in struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
_ = json.Unmarshal(ev.Input, &in)
|
||||
cmd = in.Text
|
||||
default:
|
||||
g.record(ev.ToolName, "allow", "", "")
|
||||
return g.applyIntercept(ctx, ev)
|
||||
}
|
||||
if strings.TrimSpace(cmd) == "" { // e.g. shell_send with only keys/hex → nothing to check
|
||||
g.record(ev.ToolName, "allow", "", "")
|
||||
return g.applyIntercept(ctx, ev)
|
||||
}
|
||||
|
||||
// destructive / exfil gating (§11 side-effect gating)
|
||||
if reDestructive.MatchString(cmd) {
|
||||
return g.block(ev.ToolName, "破坏性命令被安全边界拒绝(需人工批准)", cmd)
|
||||
}
|
||||
if reExfil.MatchString(cmd) {
|
||||
return g.block(ev.ToolName, "疑似数据外泄管道被拒绝", cmd)
|
||||
}
|
||||
|
||||
g.record(ev.ToolName, "allow", "", cmd)
|
||||
return g.applyIntercept(ctx, ev)
|
||||
}
|
||||
|
||||
// applyIntercept evaluates user-configured intercept rules against the tool call.
|
||||
// It is called after all built-in safety checks pass.
|
||||
func (g *Guard) applyIntercept(ctx context.Context, ev hook.Event) hook.Result {
|
||||
if g.interceptor == nil {
|
||||
return hook.Result{}
|
||||
}
|
||||
if !g.interceptor.IsToolEnabled(ev.ToolName) {
|
||||
return hook.Result{}
|
||||
}
|
||||
dec, matched := g.interceptor.Match(ev.ToolName, ev.Input)
|
||||
if !matched {
|
||||
return hook.Result{}
|
||||
}
|
||||
switch dec.Action {
|
||||
case "deny":
|
||||
return g.block(ev.ToolName, dec.Message, "")
|
||||
case "allow":
|
||||
return hook.Result{}
|
||||
case "ask":
|
||||
// If the worker context is already cancelled (task stopped / killed), block
|
||||
// immediately without creating a pending record — avoids orphaned DB entries
|
||||
// and makes execOne complete fast, reducing the race against drainSynthetic.
|
||||
if ctx.Err() != nil {
|
||||
return g.block(ev.ToolName, "工作已取消,拦截规则阻止执行", "")
|
||||
}
|
||||
convID := intercept.ConvIDFromContext(ctx)
|
||||
if !g.interceptor.HandleAsk(ctx, convID, dec, ev.ToolName, ev.Input) {
|
||||
return g.block(ev.ToolName, "用户拒绝或审批超时", "")
|
||||
}
|
||||
return hook.Result{}
|
||||
}
|
||||
return hook.Result{}
|
||||
}
|
||||
|
||||
var reBlocked = regexp.MustCompile(`(?i)\b(403|forbidden|waf|blocked|rate.?limit|429|captcha|denied)\b`)
|
||||
|
||||
// postToolUse is the Observer failure-attribution hook (G5): it classifies tool
|
||||
// results into blocked / error / ok so the planner can change strategy instead
|
||||
// of giving up at a WAF.
|
||||
func (g *Guard) postToolUse(_ context.Context, ev hook.Event) hook.Result {
|
||||
if ev.ToolName != "Bash" {
|
||||
return hook.Result{}
|
||||
}
|
||||
class := "ok"
|
||||
switch {
|
||||
case reBlocked.Match(ev.Result):
|
||||
class = "blocked"
|
||||
case ev.IsError:
|
||||
class = "error"
|
||||
}
|
||||
g.mu.Lock()
|
||||
g.attrib[class]++
|
||||
g.mu.Unlock()
|
||||
return hook.Result{}
|
||||
}
|
||||
|
||||
// Attributions returns failure-attribution counts (Observer / G5).
|
||||
func (g *Guard) Attributions() map[string]int {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
out := make(map[string]int, len(g.attrib))
|
||||
for k, v := range g.attrib {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (g *Guard) block(tool, reason, cmd string) hook.Result {
|
||||
g.record(tool, "block", reason, cmd)
|
||||
return hook.Result{Decision: "block", Message: reason}
|
||||
}
|
||||
|
||||
func (g *Guard) record(tool, action, reason, cmd string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.audit = append(g.audit, AuditEntry{TS: time.Now().Unix(), Tool: tool, Action: action, Reason: reason, Command: cmd})
|
||||
if len(g.audit) > 2000 {
|
||||
g.audit = g.audit[len(g.audit)-2000:]
|
||||
}
|
||||
}
|
||||
|
||||
// Audit returns a snapshot of recent gated calls (most recent last).
|
||||
func (g *Guard) Audit() []AuditEntry {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
out := make([]AuditEntry, len(g.audit))
|
||||
copy(out, g.audit)
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user