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
29 changes: 29 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
#
# CodeRabbit configuration for nock.
# Reviews are scoped to the git-flow integration branches: pull requests INTO
# `develop` (feature -> develop) and INTO `main` (develop -> main). On those PRs a
# review can be (re)triggered any time by commenting `@coderabbitai review`.
language: en-US
early_access: false

reviews:
# CHILL keeps the signal high — fewer, higher-confidence findings — matching how
# this repo reviews (correctness + security over nitpicks).
profile: chill
request_changes_workflow: false
high_level_summary: true
poem: false
review_status: true

auto_review:
enabled: true
drafts: false
# Only PRs targeting these branches are reviewed: feature -> develop, and the
# develop -> main release PR. PRs to other bases are left alone.
base_branches:
- develop
- main

chat:
auto_reply: true
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CodeQL

on:
push:
branches: [main]
branches: [main, develop]
pull_request:
branches: [main]
branches: [main, develop] # gate both hops: feature -> develop and develop -> main
schedule:
- cron: "27 3 * * 1" # weekly, Monday 03:27 UTC

Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: gitleaks

# Secret scanning — mechanically enforces nock's hard rule that API keys never
# land in commits, logs, or history. Gitleaks is the de-facto OSS secret scanner
# and is free for public repositories.
#
# Scoped to both git-flow merge hops: PRs into develop (feature -> develop) and
# into main (develop -> main), plus a weekly sweep that catches a secret pattern
# disclosed after it was committed. Report-only for now: this check is intentionally
# NOT a required status check yet — let it run and annotate while false positives
# are tuned, then promote it to required in branch protection.

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: "13 6 * * 1" # weekly, Monday 06:13 UTC
workflow_dispatch:

permissions:
contents: read
pull-requests: write # post the findings summary on the PR
security-events: write # upload SARIF to the Security tab

jobs:
scan:
name: scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # full history so PR-range and push diffs are scanned
Comment thread
jessn-dev marked this conversation as resolved.
- name: gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 changes: 36 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,22 @@ Thanks for your interest in nock. This guide gets you productive fast.
```bash
make build # compile to bin/nock
make test # unit tests with the race detector
make check # full local gate: fmt, vet, lint, test — run before pushing
make check # quick local gate: fmt, vet, lint, test (host OS only)
make verify # full CI mirror: cross-platform build + lint, coverage, govulncheck
make run ARGS="search web directories"
```

`make check` is the fast pre-commit gate but only exercises your host OS. Before
pushing anything that touches platform-specific code (build-tagged files,
filesystem permissions, `os/exec`), run **`make verify`** — it cross-builds every
release target (`linux`, `darwin`, `windows`; `amd64`/`arm64`), cross-lints each
target OS (so `*_windows.go` and the like are checked, not just the host's), and
runs coverage plus `govulncheck`, catching build-tag and compile breakage the
host-only gate misses. It cannot run another OS's *test suite* (CI does that), but
it catches the compile/lint failures that otherwise only surface on the runner.
The script is [`scripts/dev-check.sh`](scripts/dev-check.sh); `FAST=1` skips the
cross-platform matrix.

The scaffold is stdlib-only and builds offline. Heavy dependencies (bubbletea,
the MCP SDK, the Anthropic SDK, the YAML parser) are introduced milestone by
milestone — see `ROADMAP.md`.
Expand Down Expand Up @@ -44,6 +56,29 @@ milestone — see `ROADMAP.md`.
- Keep PRs focused; add tests for behavior changes.
- Fill in the PR template checklist. CI must be green.

### Branching (git-flow)

Feature work branches off `develop`, never `main`: `feature -> develop`, and
`develop -> main` only at release. Open PRs against `develop`; the
`develop -> main` PR is the release cut.

### Automated review (CodeRabbit)

**Why, on a solo project:** nock is single-maintainer, so there is no second
human reviewer to catch what the author misses. nock also *launches commands* —
it has to be safe for public use — so a bug or a permissions gap is a security
issue, not just a defect. CodeRabbit is the cheap, always-on "second pair of
eyes" that fills that gap: it already caught a real secrets-at-rest hole (history
files created before a fix kept loose permissions). It is a reviewer, not a gate
— it never blocks a merge and the maintainer decides every change.

PRs into `develop` and `main` are reviewed automatically by
[CodeRabbit](https://coderabbit.ai), configured in [`.coderabbit.yaml`](.coderabbit.yaml)
(CHILL profile, scoped to those two base branches). To (re)trigger a pass after
pushing fixes, comment `@coderabbitai review` on the PR. Treat its findings as
review input: verify each against the code, fix the valid ones, and say why
you're skipping the rest.

## Licensing of contributions

By contributing, you agree your contributions are licensed under the project's
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ tidy:
.PHONY: check
check: fmt vet lint test

## verify: full CI mirror incl. cross-platform build/lint (scripts/dev-check.sh)
.PHONY: verify
verify:
./scripts/dev-check.sh

## clean: remove build artifacts
.PHONY: clean
clean:
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ rebuilt from scratch: engine-first, agent-native, vendor-neutral. See
> ⚠️ For **authorized** security testing only. You are responsible for having
> permission to run what you launch. See [`SECURITY.md`](SECURITY.md).

## The problem

Mid-engagement, you burn seconds you don't have recalling the exact flags — or
your cheatsheets rot in a scratch file you `grep` by hand and re-type. When recall
is slower than just remembering it, the tool has failed.

nock makes recall instant: fuzzy-search the command, set your target once, review
the fully-resolved line, fire. Same knowledge, two keystrokes instead of twenty.

## Why nock

- **Single static binary** (Go) — instant startup, trivial distribution, no
Expand All @@ -60,6 +69,20 @@ rebuilt from scratch: engine-first, agent-native, vendor-neutral. See
- **Graceful degradation** — fuzzy search needs no AI and no network. AI ranking
layers on only when configured.

## At a glance

| | raw notes | arsenal | nock |
| --------------------------------- | :-------: | :-----: | :--: |
| Set a variable once, fill it everywhere | 🔴 | 🟢 | 🟢 |
| Single static binary, no runtime drift | — | 🔴 | 🟢 |
| Works offline / air-gapped | 🟢 | 🟢 | 🟢 |
| Optional AI ranking | 🔴 | 🔴 | 🟢 |
| MCP / agent-native (query from Claude, Cursor…) | 🔴 | 🔴 | 🟢 |

[arsenal](https://github.com/Orange-Cyberdefense/arsenal) pioneered fast command
recall; nock keeps the idea and modernizes the architecture — engine-first,
agent-native, single Go binary.

## Status

Early. **Milestone 0 (scaffold)** is in place: the engine, fuzzy search, the variable
Expand Down Expand Up @@ -91,6 +114,23 @@ export NOCK_CHEATSHEETS=/path/to/your/cheatsheets
./bin/nock search smb
```

## What it feels like

The interactive TUI (Milestone 2) is the search → fill → fire loop end to end:

```
› nmap serv # fuzzy-search as you type
[recon] nmap-service-scan nmap -sV -sC -oA scans/<target> <target>

<target> = 10.0.0.5 # fill each variable once

✓ nmap -sV -sC -oA scans/10.0.0.5 10.0.0.5 # review the resolved line, then fire
```

Nothing runs until you confirm, and what's shown is exactly what's emitted — no
hidden expansion ([show-before-fire](SECURITY.md)). _An asciinema recording lands
with the TUI._

## Modes

| Command | Mode | Status |
Expand Down Expand Up @@ -138,6 +178,8 @@ Versioned, in-repo docs live in [`docs/`](docs/):
- [CLI reference](docs/cli.md) — every mode and flag.
- [Cheatsheet schema](docs/schema.md) — the `pkg/format` authoring contract.
- [MCP setup](docs/mcp.md) — exposing nock to AI agents (Milestone 3).
- [Contributing](CONTRIBUTING.md) — dev loop, git-flow branching, and the
CodeRabbit review workflow (`@coderabbitai review` on PRs to `develop`/`main`).

Narrative and community content (tutorials, FAQ, operator workflows) lives in the
[Wiki](https://github.com/jessn-dev/nock/wiki).
Expand Down
9 changes: 6 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,12 @@ type Provider interface {
to the operator before any execution/prefill. What is displayed must equal what runs —
no hidden expansion, no auto-execution. This is the primary mitigation for the
malicious-cheatsheet injection vector and must be designed in now, not retrofitted.
- [ ] tmux pane / prefill-into-shell output (prefill into the shell line, operator hits Enter —
never auto-run on nock's behalf)
- [ ] command history
- [x] tmux pane / prefill-into-shell output (prefill into the shell line, operator hits Enter —
never auto-run on nock's behalf) — `--fire=stdout|tmux` + per-command `ctrl+t` override;
tmux uses `send-keys -l` (no Enter), gated on `$TMUX` so it never fires where it can't work
- [x] command history — recall with `ctrl+r`; stores template + var bindings (never the
flattened resolved string), owner-only on every OS (0600 / Windows owner DACL),
`NOCK_HISTORY=off` disables
- [ ] Ship: GitHub Releases, Homebrew tap, `go install`, AUR
- [ ] **Launch-day: enable GitHub Discussions** (community + traction, feeds the funding
story — sponsors/grants want a visible community). Categories: Announcements,
Expand Down
17 changes: 17 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,20 @@ Out of scope:

nock reads AI provider API keys from environment variables and never writes them to
disk, logs, or cheatsheet files. Report any deviation as a vulnerability.

## Automated security testing

Every pull request into `develop` and `main` runs a free, industry-standard
security stack in CI — the same baseline enterprises use at the free tier:

| Check | Tool | What it catches |
|---|---|---|
| SAST (Go) | [gosec](https://github.com/securego/gosec) via golangci-lint | injection, weak crypto, unsafe file perms |
| SAST (deep) | [CodeQL](https://codeql.github.com) (`security-and-quality`) | data-flow vulnerabilities |
| Dependency CVEs | [govulncheck](https://pkg.go.dev/golang.org/x/vuln) + Dependabot | known vulns in pinned deps |
| Secrets | [gitleaks](https://github.com/gitleaks/gitleaks) | API keys/tokens committed to history |
| Supply chain | [OpenSSF Scorecard](https://github.com/ossf/scorecard) | project security posture |

gosec and govulncheck gate every PR; CodeQL and gitleaks cover both merge hops
(`feature -> develop` and `develop -> main`). The binary is built `CGO_ENABLED=0`
with `-trimpath`; releases ship checksums and an SBOM.
30 changes: 30 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,39 @@ nock version print build metadata [available]
| Variable | Default | Meaning |
|---|---|---|
| `NOCK_CHEATSHEETS` | `examples/cheatsheets` | Directory scanned for `.json`/`.yaml`/`.yml` cheatsheets. |
| `NOCK_HISTORY` | `<config-dir>/nock/history.jsonl` | History file path. Set to an explicit path to relocate it, or `off` to disable history entirely. |

Load errors are non-fatal: a bad file is reported on stderr and the rest still load.

`<config-dir>` is `os.UserConfigDir()` — `~/.config` (Linux), `~/Library/Application
Support` (macOS), `%AppData%` (Windows).

## `nock` (interactive TUI)

The default mode: fuzzy-search, fill `<var>` placeholders, review the resolved
command, then fire it.

```
--fire=stdout|tmux where a confirmed command is delivered (default: stdout)
```

- **stdout** (default, every platform): the command is printed once after the UI
tears down, for the shell to capture — nock never runs it.
- **tmux**: the command is *prefilled* into the current tmux pane via
`tmux send-keys -l` with **no** trailing Enter, so you still fire it yourself.
Only offered inside a tmux session (`$TMUX` set); unavailable on Windows.

Keys: `ctrl+t` on the confirm screen overrides the target to tmux for one command;
`ctrl+r` opens history.

### History

Fired commands are recalled with `ctrl+r`. nock stores each command's **template
and variable bindings separately** — never the flattened resolved string — so a
recall re-resolves through the engine and values stay redactable. The history file
is created **owner-only on every OS**: POSIX `0600` on Linux/macOS, an owner-only
ACL on Windows. Set `NOCK_HISTORY=off` to disable persistence.

## `nock search <query>`

Fuzzy-search loaded commands and print matches, best first. Offline, instant, no
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/goccy/go-yaml v1.19.2
golang.org/x/sys v0.38.0
)

require (
Expand All @@ -36,6 +37,5 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
90 changes: 90 additions & 0 deletions internal/fire/fire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Package fire delivers a confirmed, fully resolved command out of nock to the
// operator's chosen target. It is the concrete edge of show-before-fire (ADR 009/
// 015): nothing here resolves or expands anything — the string passed in is
// exactly what the confirm screen displayed.
//
// nock never executes the command itself:
// - stdout prints it for the operator's shell to capture (the default; ADR 015).
// - tmux prefills it into the current pane's command line via `send-keys -l`
// with NO trailing newline, so the operator still presses Enter to run it.
//
// Both are explicit, operator-driven acts. Displayed == emitted; no auto-run.
package fire

import (
"errors"
"fmt"
"io"
"os"
"os/exec"
)

// Target is where a confirmed command is delivered.
type Target string

const (
// Stdout prints the command to standard output (default).
Stdout Target = "stdout"
// Tmux prefills the command into the current tmux pane without running it.
Tmux Target = "tmux"
)

// ErrUnknownTarget is returned by Parse for an unrecognised target name.
var ErrUnknownTarget = errors.New("fire: unknown target")

// Parse converts a flag value to a Target. Empty string defaults to Stdout.
func Parse(s string) (Target, error) {
switch Target(s) {
case "", Stdout:
return Stdout, nil
case Tmux:
return Tmux, nil
default:
return "", fmt.Errorf("%w %q (want stdout|tmux)", ErrUnknownTarget, s)
}
}

// Available reports whether the target can be used in the current environment.
// Tmux requires running inside a tmux session ($TMUX set); Stdout is always
// available. The TUI uses this to avoid offering a tmux fire that would fail.
func (t Target) Available() bool {
switch t {
case Tmux:
return os.Getenv("TMUX") != ""
default:
return true
}
}

// Emit delivers cmd to the target. Stdout writes to the package's Stdout writer
// (a trailing newline, so a shell reads a complete line). Tmux shells out to
// `tmux send-keys -l` — literal, no Enter — leaving cmd on the pane's prompt for
// the operator to fire.
func Emit(t Target, cmd string) error {
switch t {
case Tmux:
if !t.Available() {
return errors.New("fire: not inside a tmux session ($TMUX unset)")
}
// -l sends the argument literally; the absence of a following Enter is
// what makes this a prefill, not an auto-run.
c := exec.Command("tmux", "send-keys", "-l", cmd)
if out, err := c.CombinedOutput(); err != nil {
return fmt.Errorf("fire: tmux send-keys: %w: %s", err, out)
}
return nil
case "", Stdout:
if _, err := fmt.Fprintln(Out, cmd); err != nil {
return fmt.Errorf("fire: stdout: %w", err)
}
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default:
// An unknown target must fail loudly, not silently fall back to stdout —
// that would deliver a command somewhere the operator did not choose.
return fmt.Errorf("%w %q (want stdout|tmux)", ErrUnknownTarget, t)
}
}

// Out is the writer the Stdout target prints to. It is a package variable so
// tests can capture emitted commands without a real terminal.
var Out io.Writer = os.Stdout
Loading
Loading