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
28 changes: 18 additions & 10 deletions internal/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ package audit

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
"time"
)

// ErrNotImplemented indicates that an audit check has not yet been implemented.
var ErrNotImplemented = errors.New("audit check not implemented")

// AuditTargetType represents a type of audit target.
type AuditTargetType int

Expand All @@ -28,8 +33,6 @@ const (
AuditTargetEndpoints
)

// AuditTarget represents a target to audit in the codebase.

// AuditTarget represents a target to audit in the codebase.
type AuditTarget struct {
Type AuditTargetType
Expand Down Expand Up @@ -270,31 +273,31 @@ func DefaultRules() []string {
// auditHooks performs audit on MCP hooks.
func auditHooks(ctx context.Context, target AuditTarget, scope *AuditScope, report *AuditReport) error {
// Placeholder: implement hook auditing
return nil
return ErrNotImplemented
}

// auditMCPServer performs audit on MCP server configurations.
func auditMCPServer(ctx context.Context, target AuditTarget, scope *AuditScope, report *AuditReport) error {
// Placeholder: implement MCP config auditing
return nil
return ErrNotImplemented
}

// auditPermissions performs audit on permission configurations.
func auditPermissions(ctx context.Context, target AuditTarget, scope *AuditScope, report *AuditReport) error {
// Placeholder: implement permission auditing
return nil
return ErrNotImplemented
}

// auditSecrets performs audit on secret storage and access.
func auditSecrets(ctx context.Context, target AuditTarget, scope *AuditScope, report *AuditReport) error {
// Placeholder: implement secret auditing
return nil
return ErrNotImplemented
}

// auditEndpoints performs audit on external endpoints and APIs.
func auditEndpoints(ctx context.Context, target AuditTarget, scope *AuditScope, report *AuditReport) error {
// Placeholder: implement endpoint auditing
return nil
return ErrNotImplemented
}

// ValidateRule checks if a rule is valid.
Expand Down Expand Up @@ -387,13 +390,18 @@ func (h *HTTPClient) Get(ctx context.Context, url string) (*HTTPResponse, error)
if err != nil {
return nil, err
}
defer resp.Body.Close()

return &HTTPResponse{StatusCode: resp.StatusCode, Body: resp.Body}, nil
bodyBytes, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
return nil, err
}

return &HTTPResponse{StatusCode: resp.StatusCode, Body: bodyBytes}, nil
}

// HTTPResponse represents an HTTP response.
type HTTPResponse struct {
StatusCode int
Body interface{ Read([]byte) (int, error) }
Body []byte
}
36 changes: 27 additions & 9 deletions internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,25 +169,43 @@ func (g *DependencyGraph) GetDirectDependents(uri string) []string {
return g.edges[uri]
}

// GetAllDependents returns all transitive dependents.
// MaxTraversalDepth is the maximum depth for graph traversal.
const MaxTraversalDepth = 64

// GetAllDependents returns all transitive dependents using iterative BFS.
func (g *DependencyGraph) GetAllDependents(uri string) []string {
g.mu.RLock()
defer g.mu.RUnlock()

visited := make(map[string]bool)
visited[uri] = true
var result []string
var traverse func(n string)
traverse = func(n string) {
if visited[n] {
return

// Iterative BFS with explicit queue and depth tracking.
type entry struct {
node string
depth int
}
queue := []entry{{node: uri, depth: 0}}
head := 0

for head < len(queue) {
cur := queue[head]
head++

if cur.depth >= MaxTraversalDepth {
continue
}
visited[n] = true
for _, child := range g.edges[n] {

for _, child := range g.edges[cur.node] {
if visited[child] {
continue
}
visited[child] = true
result = append(result, child)
traverse(child)
queue = append(queue, entry{node: child, depth: cur.depth + 1})
}
}
traverse(uri)
return result
}

Expand Down
70 changes: 66 additions & 4 deletions internal/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,58 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
)

// HookType represents the type of hook.
type HookType string

const (
HookBeforeReview HookType = "beforeReview"
HookAfterReview HookType = "afterReview"
HookSessionStart HookType = "sessionStart"
HookSessionEnd HookType = "sessionEnd"
)

// AllowedCommands is the set of permitted binaries for hook execution.
var AllowedCommands = map[string]bool{
"lint": true,
"staticcheck": true,
"golint": true,
"gofmt": true,
"goimports": true,
"go": true,
"git": true,
"make": true,
"npm": true,
"yarn": true,
"pnpm": true,
"pytest": true,
"python": true,
"cargo": true,
"cargo-clippy": true,
"shellcheck": true,
"eslint": true,
"prettier": true,
"test": true,
"bash": true,
"sh": true,
"cat": true,
"echo": true,
"printf": true,
"wc": true,
"grep": true,
"diff": true,
"sort": true,
"head": true,
"tail": true,
}

// hookArgPattern matches shell metacharacters that could enable injection.
var hookArgPattern = regexp.MustCompile("[;&|$`(){}\\[\\]<>!#*?\\n\\r\\x00]")

// HookType represents the type of hook.
type HookType string

// Hook represents a lifecycle hook.
type Hook struct {
Name string
Expand Down Expand Up @@ -73,7 +111,10 @@ func (d *Dispatcher) dispatch(hookType HookType, context string) error {
}

for _, hook := range hooks {
cmd := exec.Command(hook.Command, hook.Args...) // #nosec G204 -- hook.Command is user-configured via hook registration/config files, executing arbitrary commands is the intended feature
if err := validateHookCommand(hook); err != nil {
return fmt.Errorf("hook %s rejected: %w", hook.Name, err)
}
cmd := exec.Command(hook.Command, hook.Args...) // #nosec G204 — validated by AllowedCommands allowlist
cmd.Env = append(os.Environ(), fmt.Sprintf("HOOK_CONTEXT=%s", context))

output, err := cmd.CombinedOutput()
Expand Down Expand Up @@ -151,12 +192,33 @@ func (d *Dispatcher) extractCommand(data []byte) string {
if strings.HasPrefix(line, "command:") {
cmd := strings.TrimPrefix(line, "command:")
cmd = strings.TrimSpace(cmd)
base := filepath.Base(strings.Fields(cmd)[0])
if !AllowedCommands[base] {
return ""
}
if hookArgPattern.MatchString(cmd) {
return ""
}
return cmd
}
}
return ""
}

// validateHookCommand validates a hook's command and arguments against the allowlist.
func validateHookCommand(hook *Hook) error {
base := filepath.Base(hook.Command)
if !AllowedCommands[base] {
return fmt.Errorf("command %q not in allowlist", hook.Command)
}
for _, arg := range hook.Args {
if hookArgPattern.MatchString(arg) {
return fmt.Errorf("argument %q contains unsafe characters", arg)
}
}
return nil
}

// HookTypeFromString converts a string to HookType.
func HookTypeFromString(s string) HookType {
switch strings.ToLower(s) {
Expand Down
Loading