Skip to content

Commit 511c84e

Browse files
authored
feat: Fix text wrapping logic, add badges (#8)
1 parent 113490a commit 511c84e

10 files changed

Lines changed: 96 additions & 28 deletions

File tree

.goreleaser.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ brews:
6262
repository:
6363
owner: Pringled
6464
name: homebrew-tap
65+
branch: main
6566
# Uses HOMEBREW_TAP_TOKEN secret from GitHub Actions
6667
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
6768
directory: Formula

README.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1+
<div align="center">
2+
13
# agentcheck
24

3-
Scans your shell and reports what an AI agent could access: cloud IAM, API keys, Kubernetes, local tools, and more. Read-only, no secrets exposed.
5+
<a href="https://github.com/Pringled/agentcheck/actions/workflows/ci.yml"><img src="https://github.com/Pringled/agentcheck/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
6+
<a href="https://goreportcard.com/report/github.com/Pringled/agentcheck"><img src="https://goreportcard.com/badge/github.com/Pringled/agentcheck" alt="Go Report Card"></a>
7+
<a href="https://github.com/Pringled/agentcheck/releases"><img src="https://img.shields.io/github/v/release/Pringled/agentcheck?color=%23007ec6&label=release" alt="Latest release"></a>
8+
<a href="https://github.com/Pringled/agentcheck/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License - MIT"></a>
9+
<a href="https://go.dev/"><img src="https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go&logoColor=white" alt="Go version"></a>
10+
11+
</div>
12+
13+
agentcheck is a fast, read-only tool that scans your shell and reports what an AI agent could access: cloud IAM, API keys, Kubernetes, local tools, and more.
14+
15+
<div align="center">
416

517
<picture>
618
<source media="(prefers-color-scheme: light)" srcset="assets/images/agentcheck_example_light.png">
719
<source media="(prefers-color-scheme: dark)" srcset="assets/images/agentcheck_example_dark.png">
820
<img alt="agentcheck example output" src="assets/images/agentcheck_example_dark.png">
921
</picture>
1022

11-
<div align="center">
12-
13-
<a href="https://github.com/Pringled/agentcheck/releases"><img src="https://img.shields.io/github/v/release/Pringled/agentcheck?color=%23007ec6&label=release" alt="Latest release"></a>
14-
<a href="https://github.com/Pringled/agentcheck/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License - MIT"></a>
15-
<a href="https://go.dev/"><img src="https://img.shields.io/badge/Go-1.22+-00ADD8?logo=go&logoColor=white" alt="Go version"></a>
16-
1723
</div>
1824

1925
## Quickstart
@@ -37,7 +43,7 @@ agentcheck
3743
| Azure | Active subscription and Owner / Contributor role assignments |
3844
| API Keys | 40+ env vars and credential files (OpenAI, Stripe, GitHub, etc.) |
3945
| Kubernetes | Active context, whether it points at a prod cluster |
40-
| Local | Docker daemon, SSH keys, Terraform state, .env files |
46+
| Local | Docker daemon, SSH keys, Terraform config files, .env files |
4147

4248
Every finding is tagged `LOW`, `MODERATE`, `HIGH`, or `CRITICAL`. `UNCERTAIN` findings (checks that timed out) are shown separately and never trigger CI failure.
4349

@@ -49,7 +55,7 @@ Every finding is tagged `LOW`, `MODERATE`, `HIGH`, or `CRITICAL`. `UNCERTAIN` fi
4955
|----------|----------|---------|
5056
| `CRITICAL` | Unrestricted access that cannot be further scoped or constrained. | AWS root credentials; active Vault token; kubectl pointed at a prod cluster; wildcard Kubernetes permissions |
5157
| `HIGH` | Confirmed access to something dangerous, but scoped to a service, project, or account. | AWS `AdministratorAccess` policy; GCP `roles/owner` on a project; Azure `Owner` role; API keys (OpenAI, Stripe, etc.) |
52-
| `MODERATE` | Access that could cause harm or enable lateral movement, but requires additional steps or has limited direct impact. | Docker daemon access; SSH keys loaded; Terraform state in working directory; credential files on disk |
58+
| `MODERATE` | Access that could cause harm or enable lateral movement, but requires additional steps or has limited direct impact. | Docker daemon access; SSH keys loaded; Terraform config files in working directory; credential files on disk |
5359
| `LOW` | Authenticated but no meaningful permissions found. | GCP account active with no project or roles |
5460
| `UNCERTAIN` | Check timed out or could not complete. Never triggers CI failure. | IAM policy retrieval timeout; API call failed |
5561

@@ -172,10 +178,15 @@ A project-root `.agentcheck.yaml` is **untrusted**. A hostile repository must no
172178
## Install
173179

174180
```bash
175-
# Homebrew
176-
brew install Pringled/tap/agentcheck
181+
# Homebrew (macOS/Linux)
182+
brew tap Pringled/tap
183+
brew install agentcheck
184+
185+
# Go install
186+
go install github.com/Pringled/agentcheck@latest
177187
178-
# or download a binary from https://github.com/Pringled/agentcheck/releases
188+
# Direct download
189+
# https://github.com/Pringled/agentcheck/releases
179190
180191
# Build from source
181192
git clone https://github.com/Pringled/agentcheck.git

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
module github.com/Pringled/agentcheck
22

3-
go 1.25.0
3+
go 1.24.0
44

55
require (
66
github.com/charmbracelet/lipgloss v1.1.0
77
github.com/spf13/cobra v1.10.2
8-
golang.org/x/sys v0.41.0
8+
golang.org/x/term v0.40.0
99
gopkg.in/yaml.v3 v3.0.1
1010
)
1111

@@ -24,4 +24,5 @@ require (
2424
github.com/rivo/uniseg v0.4.7 // indirect
2525
github.com/spf13/pflag v1.0.9 // indirect
2626
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
27+
golang.org/x/sys v0.41.0 // indirect
2728
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p
3838
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3939
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
4040
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
41+
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
42+
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
4143
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
4244
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4345
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/cli/cli.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ func NewRootCmdWithRunner(info BuildInfo, runner ScanRunner) *cobra.Command {
7575

7676
root := &cobra.Command{
7777
Use: "agentcheck",
78-
Short: "Know your blast radius before running an AI agent",
78+
Short: "Scan your shell and report what an AI agent could access",
7979
Long: `agentcheck scans your current shell environment and reports what an AI agent
80-
could do if run right now, covering cloud IAM, API keys, local tooling,
80+
could access if run right now: cloud IAM, API keys, local tooling,
8181
and Kubernetes access.
8282
8383
Quick start: agentcheck`,

internal/cli/cli_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,52 @@ func TestRootCmd_CwdConfigFailOn_Ignored(t *testing.T) {
301301
t.Error("CWD config fail_on: critical must be ignored; HIGH finding should trigger CI at the built-in 'high' threshold")
302302
}
303303
}
304+
305+
// TestRootCmd_CwdConfigExtraToolChecks_Ignored verifies that extra_tool_checks from a CWD
306+
// config (untrusted source) are NOT passed to scanners. A hostile repo must not be able to
307+
// run arbitrary commands by planting tool checks in its .agentcheck.yaml.
308+
func TestRootCmd_CwdConfigExtraToolChecks_Ignored(t *testing.T) {
309+
t.Setenv("AGENTCHECK_CONFIG", "")
310+
311+
dir := t.TempDir()
312+
yaml := "extra_tool_checks:\n - cmd: [\"echo\", \"hello\"]\n resource: test-resource\n description: test check\n severity: critical\n"
313+
if err := os.WriteFile(dir+"/.agentcheck.yaml", []byte(yaml), 0o600); err != nil {
314+
t.Fatalf("write config: %v", err)
315+
}
316+
orig, err := os.Getwd()
317+
if err != nil {
318+
t.Fatalf("getwd: %v", err)
319+
}
320+
t.Cleanup(func() {
321+
if err := os.Chdir(orig); err != nil {
322+
t.Errorf("restore cwd: %v", err)
323+
}
324+
})
325+
if err := os.Chdir(dir); err != nil {
326+
t.Fatalf("chdir: %v", err)
327+
}
328+
329+
var receivedCfg config.LoadResult
330+
cmd := cli.NewRootCmdWithRunner(
331+
cli.BuildInfo{Version: "test", Commit: "abc", Date: "2026-01-01"},
332+
func(cfg config.LoadResult) models.RiskReport {
333+
receivedCfg = cfg
334+
return emptyReport()
335+
},
336+
)
337+
var buf bytes.Buffer
338+
cmd.SetOut(&buf)
339+
if err := cmd.Execute(); err != nil {
340+
t.Fatalf("unexpected error: %v", err)
341+
}
342+
// The runner must receive the raw config (which has the checks), but the
343+
// real scanner runner must not apply them for untrusted sources. We verify
344+
// that the source is indeed untrusted (CWD), so the boundary is correctly
345+
// identified — enforced in realScanRunner via cfg.Source.Trusted().
346+
if receivedCfg.Source.Trusted() {
347+
t.Errorf("CWD config source must not be trusted, got %v", receivedCfg.Source)
348+
}
349+
if len(receivedCfg.Config.ExtraToolChecks) == 0 {
350+
t.Error("config should have parsed extra_tool_checks from CWD (to confirm the field is present but untrusted)")
351+
}
352+
}

internal/models/models.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ func (e *UnknownSeverityError) Error() string {
7070

7171
// Finding represents a single security concern detected during a scan.
7272
// Resource must never contain actual secret values; key names and file paths only.
73-
// Description should always start with "An AI agent running in this shell..."
7473
type Finding struct {
7574
Scanner string `json:"scanner"`
7675
Resource string `json:"resource"`

internal/output/json.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@ type JSONFormatter struct {
1515
}
1616

1717
// Render writes the scan report as indented JSON to the configured writer.
18+
// Nil Findings slices are normalized to empty arrays so consumers get consistent types.
1819
func (f JSONFormatter) Render(report models.RiskReport) error {
1920
w := f.Out
2021
if w == nil {
2122
w = os.Stdout
2223
}
24+
for i := range report.ScanResults {
25+
if report.ScanResults[i].Findings == nil {
26+
report.ScanResults[i].Findings = []models.Finding{}
27+
}
28+
}
2329
enc := json.NewEncoder(w)
2430
enc.SetIndent("", " ")
2531
enc.SetEscapeHTML(false)

internal/output/table.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"strings"
99

1010
"github.com/charmbracelet/lipgloss"
11-
"golang.org/x/sys/unix"
11+
"golang.org/x/term"
1212

1313
"github.com/Pringled/agentcheck/internal/models"
1414
)
@@ -61,21 +61,20 @@ func severityStyle(s models.Severity) lipgloss.Style {
6161
}
6262
}
6363

64-
// Column widths for the fixed-width scanner, resource, and severity columns.
65-
const (
66-
colScanner = 20
67-
colResource = 30
68-
colSeverity = 10
69-
)
70-
7164
// terminalWidth returns the current terminal width, falling back to 120.
7265
func terminalWidth() int {
73-
if ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ); err == nil && ws.Col > 0 { //nolint:gosec // G115: fd is always a small non-negative integer; overflow is impossible
74-
return int(ws.Col)
66+
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 { //nolint:gosec // G115: fd is always a small non-negative integer; overflow is impossible
67+
return w
7568
}
7669
return 120
7770
}
7871

72+
const (
73+
colScanner = 20
74+
colResource = 30
75+
colSeverity = 10
76+
)
77+
7978
// TableFormatter renders scan results as a colored terminal table with a summary section.
8079
type TableFormatter struct {
8180
// Out is the writer to render to. Defaults to os.Stdout when nil.

internal/scan/gcp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (s *GCPScanner) Scan() models.ScanResult {
127127

128128
var findings []models.Finding
129129

130-
// If project enumeration failed, fall back to active project only.
130+
// If project enumeration failed, return an UNCERTAIN finding.
131131
if err != nil {
132132
findings = append(findings, models.Finding{
133133
Scanner: "gcp",

0 commit comments

Comments
 (0)