-
Notifications
You must be signed in to change notification settings - Fork 0
opsec #1535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
opsec #1535
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||
| #!/usr/bin/env bash | ||||||||||
| # block-gh-settings.sh — Shared hook for Claude Code + Codex | ||||||||||
| # Blocks gh CLI commands that modify GitHub repository settings. | ||||||||||
| # Exit 2 = block (Codex), JSON decision output (Claude). | ||||||||||
| set -euo pipefail | ||||||||||
|
|
||||||||||
| # Read tool input from stdin | ||||||||||
| input=$(cat) | ||||||||||
|
|
||||||||||
| # Extract command | ||||||||||
| command=$(echo "$input" | jq -r '.tool_input.command // .command // empty' 2>/dev/null) | ||||||||||
| [[ -z $command ]] && exit 0 | ||||||||||
|
|
||||||||||
| # Block: gh repo <destructive-subcommand> | ||||||||||
| if echo "$command" | grep -qE 'gh\s+repo\s+(delete|rename|archive|transfer|edit)\b'; then | ||||||||||
| subcommand=$(echo "$command" | grep -oE 'gh\s+repo\s+(delete|rename|archive|transfer|edit)' | awk '{print $3}') | ||||||||||
| msg="'gh repo $subcommand' is blocked. Repo settings must be changed manually." | ||||||||||
| echo "BLOCKED by block-gh-settings.sh: $msg" >&2 | ||||||||||
| exit 2 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| # Block: gh api -X PATCH|DELETE|PUT targeting /repos/ | ||||||||||
| if echo "$command" | grep -qE 'gh\s+api'; then | ||||||||||
| if echo "$command" | grep -qE '\-X\s+(PATCH|DELETE|PUT)' && echo "$command" | grep -qE '/repos/'; then | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The mutation filter is bypassable because it only detects Prompt for AI agents |
||||||||||
| method=$(echo "$command" | grep -oE '\-X\s+(PATCH|DELETE|PUT)' | awk '{print $2}') | ||||||||||
|
Comment on lines
+24
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||
| msg="'gh api -X $method /repos/...' is blocked. Repo API mutations must be done manually." | ||||||||||
| echo "BLOCKED by block-gh-settings.sh: $msg" >&2 | ||||||||||
| exit 2 | ||||||||||
| fi | ||||||||||
| fi | ||||||||||
|
Comment on lines
+22
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, the gh CLI gh api command accepts both --method DELETE (or the short form -X DELETE) and paths without a leading slash like repos/owner/repo. The official documentation shows examples using -X for methods like GET and PATCH, confirming --method DELETE works similarly as it overrides the default method. A GitHub CLI maintainer example explicitly uses gh api -X DELETE repos/${username}/${reponame} for deleting a repository, demonstrating DELETE with a path starting repos/owner/repo without a leading slash. Multiple sources confirm gh api accepts paths both with and without a leading slash (e.g., /repos/... or repos/...), prepending the base API URL appropriately. Examples in the manual consistently use paths without leading slashes like repos/{owner}/{repo}/releases. While Git Bash on Windows may misinterpret leading slashes due to path expansion, this is a shell issue, not a limitation of gh CLI, and paths without leading slashes work universally. Citations:
Tighten regex patterns to close bypass vectors in The script blocks destructive mutations via
Both bypasses should be closed. Update the regex patterns to match:
Proposed fix # Block: gh api -X PATCH|DELETE|PUT targeting /repos/
if echo "$command" | grep -qE 'gh\s+api'; then
- if echo "$command" | grep -qE '\-X\s+(PATCH|DELETE|PUT)' && echo "$command" | grep -qE '/repos/'; then
- method=$(echo "$command" | grep -oE '\-X\s+(PATCH|DELETE|PUT)' | awk '{print $2}')
+ if echo "$command" | grep -qE '(-X|--method)[= ]+(PATCH|DELETE|PUT)\b' \
+ && echo "$command" | grep -qE '(^|[[:space:]=])/?repos/'; then
+ method=$(echo "$command" | grep -oE '(-X|--method)[= ]+(PATCH|DELETE|PUT)' | grep -oE '(PATCH|DELETE|PUT)')
msg="'gh api -X $method /repos/...' is blocked. Repo API mutations must be done manually."
echo "BLOCKED by block-gh-settings.sh: $msg" >&2
exit 2
fi
fiAdd test cases in It 'blocks gh api --method DELETE /repos/...'
Data '{"tool_input": {"command": "gh api --method DELETE /repos/owner/repo"}}'
When run bash "$SCRIPT"
The status should eq 2
The stderr should include 'BLOCKED'
End
It 'blocks gh api -X DELETE repos/... (no leading slash)'
Data '{"tool_input": {"command": "gh api -X DELETE repos/owner/repo"}}'
When run bash "$SCRIPT"
The status should eq 2
The stderr should include 'BLOCKED'
End🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| exit 0 | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -71,7 +71,34 @@ import ../../hosts/nixos { | |||||||
| ]; | ||||||||
|
|
||||||||
| # Networking | ||||||||
| networking.networkmanager.wifi.powersave = true; | ||||||||
| networking.networkmanager.wifi = { | ||||||||
| powersave = true; | ||||||||
| scanRandMacAddress = true; | ||||||||
| macAddress = "stable-ssid"; | ||||||||
| }; | ||||||||
|
|
||||||||
| networking.firewall = { | ||||||||
| enable = true; | ||||||||
| trustedInterfaces = [ "tailscale0" ]; | ||||||||
| allowedTCPPorts = [ ]; | ||||||||
| allowedUDPPorts = [ ]; | ||||||||
| logRefusedConnections = true; | ||||||||
| }; | ||||||||
|
Comment on lines
+80
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm whether tailscale is actually enabled on the matic host (directly or transitively).
rg -nP -C2 '\b(services\.tailscale|modules\.tailscale)\b' named-hosts/matic/ hosts/ modules/ 2>/dev/null
echo '---'
# Also check if any imported module pulls in tailscale for matic.
fd -t f . named-hosts/matic hosts | xargs rg -nP '\btailscale\b' 2>/dev/nullRepository: shunkakinoki/dotfiles Length of output: 70 Remove dead firewall config or enable Tailscale on this host.
Combined with 🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| # Audit logging | ||||||||
| security.auditd.enable = true; | ||||||||
| security.audit = { | ||||||||
| enable = true; | ||||||||
| rules = [ | ||||||||
| "-a exit,always -F arch=b64 -S execve -k exec" | ||||||||
| "-a exit,always -F arch=b32 -S execve -k exec" | ||||||||
| "-a exit,always -F arch=b64 -S setuid,setgid,setresuid,setresgid -k priv_esc" | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Missing Prompt for AI agents
Suggested change
|
||||||||
| "-w /etc/sudoers -p wa -k sudoers" | ||||||||
| "-w /etc/passwd -p wa -k identity" | ||||||||
| "-w /etc/shadow -p wa -k identity" | ||||||||
| "-w /etc/ssh -p wa -k ssh" | ||||||||
| ]; | ||||||||
|
Comment on lines
+92
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Also consider whether unconditional 🔒 Proposed fix: add b32 priv-esc rule rules = [
"-a exit,always -F arch=b64 -S execve -k exec"
"-a exit,always -F arch=b32 -S execve -k exec"
"-a exit,always -F arch=b64 -S setuid,setgid,setresuid,setresgid -k priv_esc"
+ "-a exit,always -F arch=b32 -S setuid,setgid,setresuid,setresgid -k priv_esc"
"-w /etc/sudoers -p wa -k sudoers"
"-w /etc/passwd -p wa -k identity"
"-w /etc/shadow -p wa -k identity"
"-w /etc/ssh -p wa -k ssh"
];🤖 Prompt for AI Agents |
||||||||
| }; | ||||||||
|
|
||||||||
| # Docker | ||||||||
| virtualisation.docker.enable = true; | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| #!/usr/bin/env bash | ||
| # shellcheck disable=SC2329 | ||
|
|
||
| Describe 'block-gh-settings.sh' | ||
| SCRIPT="$PWD/config/shared/hooks/block-gh-settings.sh" | ||
|
|
||
| Describe 'non-modifying commands' | ||
|
|
||
| It 'allows gh pr list' | ||
| Data '{"tool_input": {"command": "gh pr list"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| It 'allows gh repo view' | ||
| Data '{"tool_input": {"command": "gh repo view"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| It 'allows gh repo clone' | ||
| Data '{"tool_input": {"command": "gh repo clone owner/repo"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| It 'allows gh api GET' | ||
| Data '{"tool_input": {"command": "gh api /repos/owner/repo"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| It 'allows gh api -X POST to non-repo path' | ||
| Data '{"tool_input": {"command": "gh api -X POST /gists"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| End | ||
|
|
||
| Describe 'blocked gh repo subcommands' | ||
|
|
||
| It 'blocks gh repo delete' | ||
| Data '{"tool_input": {"command": "gh repo delete owner/repo"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| It 'blocks gh repo rename' | ||
| Data '{"tool_input": {"command": "gh repo rename new-name"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| It 'blocks gh repo archive' | ||
| Data '{"tool_input": {"command": "gh repo archive owner/repo"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| It 'blocks gh repo transfer' | ||
| Data '{"tool_input": {"command": "gh repo transfer owner/repo new-owner"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| It 'blocks gh repo edit' | ||
| Data '{"tool_input": {"command": "gh repo edit --description new-desc"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| End | ||
|
|
||
| Describe 'blocked gh api mutations on /repos/' | ||
|
|
||
| It 'blocks gh api -X PATCH /repos/...' | ||
| Data '{"tool_input": {"command": "gh api -X PATCH /repos/owner/repo"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| It 'blocks gh api -X DELETE /repos/...' | ||
| Data '{"tool_input": {"command": "gh api -X DELETE /repos/owner/repo/branches/main/protection"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| It 'blocks gh api -X PUT /repos/...' | ||
| Data '{"tool_input": {"command": "gh api -X PUT /repos/owner/repo/collaborators/user"}}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| End | ||
|
|
||
| Describe 'codex input format' | ||
|
|
||
| It 'blocks codex-style input with .command key' | ||
| Data '{"command": "gh repo delete owner/repo"}' | ||
| When run bash "$SCRIPT" | ||
| The status should eq 2 | ||
| The stderr should include 'BLOCKED' | ||
| End | ||
|
|
||
| End | ||
|
|
||
| Describe 'edge cases' | ||
|
|
||
| It 'passes with empty input' | ||
| Data '{}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| It 'passes with empty command' | ||
| Data '{"tool_input": {"command": ""}}' | ||
| When run bash "$SCRIPT" | ||
| The status should be success | ||
| End | ||
|
|
||
| End | ||
|
|
||
| End |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extraction of the subcommand can be simplified. Using
head -n1ensures that only the first match is captured if multiple keywords appear in the string (e.g., in comments), and it avoids the overhead of spawningawk.