diff --git a/CHANGELOG.md b/CHANGELOG.md index 927a027..6c2060b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please choose versions by [Semantic Versioning](http://semver.org/). ## Unreleased +- feat(rules): add four `### RULE` blocks to `docs/go-security-linting.md` — `go-security/file-perms-too-permissive`, `go-security/dir-perms-too-permissive`, `go-security/nosec-requires-reason`, `go-security/chmod-return-checked`; matching ast-grep YAMLs + `rules/index.json` entries - chore(release): live-verify `github-releaser-agent` plugin-manifest bumping — release commit should rewrite `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json` version fields alongside the CHANGELOG (feature shipped via `bborbe/maintainer` PR #33). ## v0.11.0 diff --git a/docs/go-security-linting.md b/docs/go-security-linting.md index 34e358c..4bf8fae 100644 --- a/docs/go-security-linting.md +++ b/docs/go-security-linting.md @@ -54,3 +54,87 @@ cmd := exec.CommandContext(ctx, "git", "push") - [ ] All `#nosec` have explanatory comments - [ ] All `os.Chmod` return values checked (or explicitly `_ =` with comment) - [ ] No `os.ReadFile` with untrusted paths without `#nosec` + reason + +### RULE go-security/file-perms-too-permissive (MUST) + +**Owner**: go-security-specialist +**Applies when**: `os.WriteFile($PATH, $DATA, $PERM)` or `os.OpenFile($PATH, $FLAGS, $PERM)` calls in a `*.go` file outside `*_test.go` and `vendor/`, where `$PERM` is a literal octal that is NOT `0600` / `0o600`. +**Enforcement**: `rules/go/file-perms-too-permissive.yml` +**Why**: world-readable file permissions (e.g. `0644`) expose configuration data, lock files, and other artifacts to any process on the host. gosec G306 flags this; the convention is owner-only `0600` for ALL files unless there is a specific reason documented via `#nosec` with explanation. + +#### Bad + +```go +os.WriteFile(path, data, 0644) +os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) +``` + +#### Good + +```go +os.WriteFile(path, data, 0600) +os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) +``` + +### RULE go-security/dir-perms-too-permissive (MUST) + +**Owner**: go-security-specialist +**Applies when**: `os.MkdirAll($PATH, $PERM)` or `os.Mkdir($PATH, $PERM)` calls in a `*.go` file outside `*_test.go` and `vendor/`, where `$PERM` is a literal octal that is NOT `0750` / `0o750`. +**Enforcement**: `rules/go/dir-perms-too-permissive.yml` +**Why**: world-readable directories (e.g. `0755`) expose contents to any process on the host. The convention is `0750` (owner-rwx + group-rx, no world) for ALL agent-created directories. + +#### Bad + +```go +os.MkdirAll(dir, 0755) +os.Mkdir(dir, 0755) +``` + +#### Good + +```go +os.MkdirAll(dir, 0750) +os.Mkdir(dir, 0750) +``` + +### RULE go-security/nosec-requires-reason (MUST) + +**Owner**: go-security-specialist +**Applies when**: a `// #nosec ` comment in a `*.go` file outside `*_test.go` and `vendor/` appears WITHOUT a `-- ` text component on the same line. +**Enforcement**: `rules/go/nosec-requires-reason.yml` +**Why**: bare `#nosec` suppresses a finding without explaining why the input is trusted. The next reviewer has no audit trail. Mandate `// #nosec G304 -- path from internal ListQueued(), not user input` style. + +#### Bad + +```go +// #nosec G304 +data, err := os.ReadFile(userPath) +``` + +#### Good + +```go +// #nosec G304 -- path from internal ListQueued(), not user input +data, err := os.ReadFile(trustedPath) +``` + +### RULE go-security/chmod-return-checked (MUST) + +**Owner**: go-security-specialist +**Applies when**: an `os.Chmod($PATH, $PERM)` call in a `*.go` file outside `*_test.go` and `vendor/` whose return value is discarded (no `if err := os.Chmod(...); err != nil` wrapper, no `_ = os.Chmod(...)` with an explanatory comment). Detecting "return value used in error check" requires reading the surrounding statement — pure ast-grep cannot reliably distinguish a checked `os.Chmod(...)` from an unchecked one without false positives. +**Enforcement**: judgment +**Why**: silent `os.Chmod` failures leave file permissions in an unexpected state. The convention is either `if err := os.Chmod(...); err != nil { return ... }` or explicit `_ = os.Chmod(...)` with a comment explaining why the error is ignored. + +#### Bad + +```go +os.Chmod(path, 0600) +``` + +#### Good + +```go +if err := os.Chmod(path, 0600); err != nil { + return errors.Wrapf(ctx, err, "chmod %s", path) +} +``` diff --git a/prompts/completed/008-bootstrap-rules-go-security-linting.md b/prompts/completed/008-bootstrap-rules-go-security-linting.md new file mode 100644 index 0000000..4e261d8 --- /dev/null +++ b/prompts/completed/008-bootstrap-rules-go-security-linting.md @@ -0,0 +1,262 @@ +--- +status: completed +summary: 'Migrated docs/go-security-linting.md from prose-only to rule-blocks-inline: appended four ### RULE blocks (3 mechanical + 1 judgment), created three ast-grep YAML detectors, ran make build-index to grow rules/index.json from 9 to 13 entries, and added ## Unreleased to CHANGELOG.md.' +container: coding-bootstrap-go-security-exec-008-bootstrap-rules-go-security-linting +dark-factory-version: v0.173.0 +created: "2026-06-01T19:53:46Z" +queued: "2026-06-01T19:57:23Z" +started: "2026-06-01T19:57:29Z" +completed: "2026-06-01T19:59:55Z" +--- + + +- Third bootstrap prompt — migrates `docs/go-security-linting.md` from prose-only to rule-blocks-inline (Model A) +- Appends 4 `### RULE` blocks (3 MUST mechanical + 1 MUST judgment) corresponding to the doc's `## Rules` section +- Adds 3 `rules/go/*.yml` ast-grep YAMLs for the mechanical rules; the judgment rule carries `Enforcement: judgment` +- Runs `make build-index` so `rules/index.json` grows from 9 entries to 13 +- Mirrors the proven `bootstrap-rules-go-time-injection.md` + `bootstrap-rules-go-error-wrapping.md` shape +- Scope note: doc lists 6 rules; this prompt extracts 4. Skipped: #3 ("fix on first attempt" — process rule, not enforceable) and #6 ("lock/PID files 0600" — covered by rule 1 which already mandates 0600 for ALL files) + + + +Following the schema in `docs/rule-block-schema.md` and the ast-grep conventions in `docs/ast-grep-rule-writing-guide.md`, extract four rules from `docs/go-security-linting.md` as inline `### RULE` blocks, write ast-grep YAML detectors for the three mechanical rules, and refresh `rules/index.json` via the walker. Every block must conform to the schema (Owner, Applies when, Enforcement fields, ID format `//`, anchor = id verbatim). + + + +Read `CLAUDE.md` for project conventions, including the doc-agent alignment table (`go-security-linting.md` → `go-security-specialist`). +Read `docs/rule-block-schema.md` for the rule-block contract — required fields, level tokens, ID format, anchor rule (`anchor == id` verbatim). +Read `docs/ast-grep-rule-writing-guide.md` for the YAML conventions — frontmatter shape, pattern strategies, pitfalls (especially the `main.go` + `**/main.go` dual-ignore + `**/mocks/**` defaults), smoke testing. +Read `docs/go-security-linting.md` — the doc to migrate. The `## Rules` section (around line 41) lists the rules; the `## File Permissions`, `## File Path from Variable`, and `## Subprocess from Variable` sections provide Bad/Good code snippets. +Read `docs/go-context-cancellation-in-loops.md` lines 160-192 — the canonical pilot `### RULE` block. The new blocks MUST match its formatting exactly: heading line `### RULE (LEVEL)`, then bolded fields `**Owner**:`, `**Applies when**:`, `**Enforcement**:`, then `**Why**:`, then `#### Bad` / `#### Good` code blocks. +Read `rules/go/no-fmt-errorf.yml`, `rules/go/no-time-now-direct.yml` — canonical small ast-grep YAMLs. Match their shape. +Read `scripts/build-index.py` and run `python3 scripts/build-index.py` once to see the current `rules/index.json` shape (9 entries) before changes. + + + +1. Append four `### RULE` blocks at the END of `docs/go-security-linting.md` (after the existing `## Checklist` section). Do NOT modify any prose above. The four blocks, in order: + +**Rule 1 — MUST mechanical** +- Heading: `### RULE go-security/file-perms-too-permissive (MUST)` +- Owner: `go-security-specialist` +- Applies when: `os.WriteFile($PATH, $DATA, $PERM)` or `os.OpenFile($PATH, $FLAGS, $PERM)` calls in a `*.go` file outside `*_test.go` and `vendor/`, where `$PERM` is a literal octal that is NOT `0600` / `0o600`. +- Enforcement: `rules/go/file-perms-too-permissive.yml` +- Why: world-readable file permissions (e.g. `0644`) expose configuration data, lock files, and other artifacts to any process on the host. gosec G306 flags this; the convention is owner-only `0600` for ALL files unless there is a specific reason documented via `#nosec` with explanation. +- Bad/Good snippets: reuse the doc's `## File Permissions` section (`os.WriteFile(path, data, 0644)` → `os.WriteFile(path, data, 0600)`). + +**Rule 2 — MUST mechanical** +- Heading: `### RULE go-security/dir-perms-too-permissive (MUST)` +- Owner: `go-security-specialist` +- Applies when: `os.MkdirAll($PATH, $PERM)` or `os.Mkdir($PATH, $PERM)` calls in a `*.go` file outside `*_test.go` and `vendor/`, where `$PERM` is a literal octal that is NOT `0750` / `0o750`. +- Enforcement: `rules/go/dir-perms-too-permissive.yml` +- Why: world-readable directories (e.g. `0755`) expose contents to any process on the host. The convention is `0750` (owner-rwx + group-rx, no world) for ALL agent-created directories. +- Bad/Good snippets: reuse the doc's `## File Permissions` `MkdirAll` example. + +**Rule 3 — MUST mechanical** +- Heading: `### RULE go-security/nosec-requires-reason (MUST)` +- Owner: `go-security-specialist` +- Applies when: a `// #nosec ` comment in a `*.go` file outside `*_test.go` and `vendor/` appears WITHOUT a `-- ` text component on the same line. +- Enforcement: `rules/go/nosec-requires-reason.yml` +- Why: bare `#nosec` suppresses a finding without explaining why the input is trusted. The next reviewer has no audit trail. Mandate `// #nosec G304 -- path from internal ListQueued(), not user input` style. +- Bad/Good snippets: reuse the doc's `## File Path from Variable` + `## Subprocess from Variable` good examples. + +**Rule 4 — MUST judgment** +- Heading: `### RULE go-security/chmod-return-checked (MUST)` +- Owner: `go-security-specialist` +- Applies when: an `os.Chmod($PATH, $PERM)` call in a `*.go` file outside `*_test.go` and `vendor/` whose return value is discarded (no `if err := os.Chmod(...); err != nil` wrapper, no `_ = os.Chmod(...)` with an explanatory comment). Detecting "return value used in error check" requires reading the surrounding statement — pure ast-grep cannot reliably distinguish a checked `os.Chmod(...)` from an unchecked one without false positives. +- Enforcement: `judgment` (no ast-grep — return-value-check semantics require whole-statement reasoning). +- Why: silent `os.Chmod` failures leave file permissions in an unexpected state. The convention is either `if err := os.Chmod(...); err != nil { return ... }` or explicit `_ = os.Chmod(...)` with a comment explaining why the error is ignored. +- Bad/Good snippets: inline the following minimal pair (the doc doesn't have explicit Bad/Good code for this rule): + + Bad: + ```go + os.Chmod(path, 0600) + ``` + + Good: + ```go + if err := os.Chmod(path, 0600); err != nil { + return errors.Wrapf(ctx, err, "chmod %s", path) + } + ``` + +Each block must follow the canonical pilot's exact formatting (`docs/go-context-cancellation-in-loops.md:160-192`). + +2. Create `rules/go/file-perms-too-permissive.yml`: + - `id: go-security/file-perms-too-permissive` + - `language: go` + - `severity: error` + - `message`: 3-line block — short summary, blank line, `See docs/go-security-linting.md (RULE go-security/file-perms-too-permissive).` + - `rule`: use `any:` alternation across known-bad permission patterns. Common bad values: `0644`, `0o644`, `0666`, `0o666`, `0777`, `0o777`. Pattern guidance: + ```yaml + rule: + any: + - pattern: os.WriteFile($PATH, $DATA, 0644) + - pattern: os.WriteFile($PATH, $DATA, 0o644) + - pattern: os.WriteFile($PATH, $DATA, 0666) + - pattern: os.WriteFile($PATH, $DATA, 0o666) + - pattern: os.WriteFile($PATH, $DATA, 0777) + - pattern: os.WriteFile($PATH, $DATA, 0o777) + - pattern: os.OpenFile($PATH, $FLAGS, 0644) + - pattern: os.OpenFile($PATH, $FLAGS, 0o644) + - pattern: os.OpenFile($PATH, $FLAGS, 0666) + - pattern: os.OpenFile($PATH, $FLAGS, 0o666) + ``` + Alternative: detect any `os.WriteFile`/`os.OpenFile` call with constraints on the perm metavariable — only acceptable if the constraint correctly excludes `0600` / `0o600` and the pattern is verified against a synthetic file. + - `ignores`: `main.go`, `**/main.go`, `**/*_test.go`, `vendor/**`, `**/vendor/**`, `**/mocks/**` + +3. Create `rules/go/dir-perms-too-permissive.yml`: + - `id: go-security/dir-perms-too-permissive` + - `language: go` + - `severity: error` + - `message`: 3-line block referencing the doc + - `rule`: similar `any:` alternation across `0755`, `0o755`, `0777`, `0o777`, `0700` (too restrictive for some workflows but uncommon as a bad value — include if simple) for `os.MkdirAll` and `os.Mkdir`. Bad values to cover at minimum: + ```yaml + rule: + any: + - pattern: os.MkdirAll($PATH, 0755) + - pattern: os.MkdirAll($PATH, 0o755) + - pattern: os.MkdirAll($PATH, 0777) + - pattern: os.MkdirAll($PATH, 0o777) + - pattern: os.Mkdir($PATH, 0755) + - pattern: os.Mkdir($PATH, 0o755) + - pattern: os.Mkdir($PATH, 0777) + - pattern: os.Mkdir($PATH, 0o777) + ``` + - `ignores`: same as rule 1 + +4. Create `rules/go/nosec-requires-reason.yml`: + - `id: go-security/nosec-requires-reason` + - `language: go` + - `severity: error` + - `message`: 3-line block referencing the doc + - `rule`: detecting a comment's content is harder than detecting a Go AST node. ast-grep's `kind: comment` may suffice with a regex constraint. Pattern guidance: + ```yaml + rule: + kind: comment + regex: '#nosec' + not: + regex: '#nosec.*--' + ``` + If `kind: comment` + `regex` + `not.regex` doesn't work in ast-grep's Go grammar, fall back to a simpler pattern that matches the `#nosec` literal text without the `--`: + ```yaml + rule: + pattern-regex: '//\s*#nosec\b(?!.*--)' + ``` + Either acceptable as long as the smoke shows bare `#nosec G304` matches and `#nosec G304 -- reason` does not. + - `ignores`: same as rule 1 + +5. Rule 4 (`chmod-return-checked`) has NO ast-grep YAML. Its `### RULE` block carries `Enforcement: judgment` (literal string, no path). + +6. No ast-grep smoke inside this prompt — operator runs `scripts/scan.sh ` post-merge to confirm the detectors fire correctly on real code. + +7. Run `make build-index` to refresh `rules/index.json`. It must grow from 9 entries to **13 entries** (existing pilot + 3 go-time + 5 go-errors + 4 new go-security), with entries sorted by `id`. + +8. Verify each new index entry has: + - `owner: "go-security-specialist"` + - `doc_path: "docs/go-security-linting.md"` + - `anchor == id` + - `level in ("MUST","SHOULD","MAY")` + - non-empty `applies_when` and `enforcement` + - For rule 4: `enforcement: "judgment"` (literal string) + +9. Run `make precommit` — must pass. + + + +- Do NOT commit — dark-factory handles git +- Do NOT modify the existing rule blocks in `docs/go-context-cancellation-in-loops.md`, `docs/go-time-injection.md`, or `docs/go-error-wrapping-guide.md`, or their YAMLs +- Do NOT modify the schema doc or the ast-grep guide +- Do NOT rewrite `docs/go-security-linting.md`'s existing prose — only APPEND the four `### RULE` blocks at the END of the file (after `## Checklist`) +- Do NOT wire ast-grep into `make precommit` +- Do NOT attempt `ast-grep scan` from inside the container +- Use the same Bad/Good code snippets shown in `docs/go-security-linting.md` itself where they exist; for rule 4, synthesize minimal generic examples (User/Order/Product) per CLAUDE.md +- Generic examples only — no Candle/Epic/Broker/SignalStore +- No personal paths anywhere +- No `Co-Authored-By:` or attribution trailers + + + +Run from repo root: +```bash +# Four new ### RULE blocks added to the doc +grep -c '^### RULE go-security/' docs/go-security-linting.md +# Must return: 4 + +# Three ast-grep YAMLs created +test -f rules/go/file-perms-too-permissive.yml && \ +test -f rules/go/dir-perms-too-permissive.yml && \ +test -f rules/go/nosec-requires-reason.yml && \ +echo "yamls present: ok" + +# YAMLs reference the correct ids on their first line +head -1 rules/go/file-perms-too-permissive.yml | grep -q '^id: go-security/file-perms-too-permissive' && echo "yaml1 id: ok" +head -1 rules/go/dir-perms-too-permissive.yml | grep -q '^id: go-security/dir-perms-too-permissive' && echo "yaml2 id: ok" +head -1 rules/go/nosec-requires-reason.yml | grep -q '^id: go-security/nosec-requires-reason' && echo "yaml3 id: ok" + +# All three YAMLs use severity: error +for f in rules/go/file-perms-too-permissive.yml rules/go/dir-perms-too-permissive.yml rules/go/nosec-requires-reason.yml; do + head -5 "$f" | grep -q '^severity: error' || { echo "FAIL: $f missing severity: error"; exit 1; } +done +echo "severity: ok" + +# No YAML for the judgment rule (rule 4) +test ! -f rules/go/chmod-return-checked.yml && echo "rule 4 has no YAML: ok" + +# rules/index.json now has 13 entries +python3 -c " +import json +d = json.load(open('rules/index.json')) +assert isinstance(d, list) and len(d) == 13, f'expected 13 entries, got {len(d)}' +print(f'entries: {len(d)} ok') +" + +# Expected ids present and sorted +python3 -c " +import json +ids = [e['id'] for e in json.load(open('rules/index.json'))] +expected = [ + 'go-context/cancel-check-in-loop', + 'go-errors/inner-closure-no-double-wrap', + 'go-errors/no-bare-return-err', + 'go-errors/no-context-background-in-business-logic', + 'go-errors/no-fmt-errorf', + 'go-errors/sentinel-err-prefix-naming', + 'go-security/chmod-return-checked', + 'go-security/dir-perms-too-permissive', + 'go-security/file-perms-too-permissive', + 'go-security/nosec-requires-reason', + 'go-time/inject-getter-not-create', + 'go-time/no-time-now-direct', + 'go-time/no-time-time-in-fields', +] +assert ids == expected, f'ids mismatch:\n got: {ids}\n expected: {expected}' +print('ids sorted: ok') +" + +# Four new entries owned by go-security-specialist + point at the right doc +python3 -c " +import json +go_sec = [e for e in json.load(open('rules/index.json')) if e['id'].startswith('go-security/')] +assert len(go_sec) == 4, f'expected 4 go-security/* entries, got {len(go_sec)}' +for e in go_sec: + assert e['owner'] == 'go-security-specialist', f\"owner mismatch for {e['id']}: {e['owner']}\" + assert e['doc_path'] == 'docs/go-security-linting.md', f\"doc_path mismatch for {e['id']}\" + assert e['anchor'] == e['id'], f\"anchor != id for {e['id']}: {e['anchor']}\" + assert e['level'] in ('MUST','SHOULD','MAY'), f\"invalid level for {e['id']}: {e['level']}\" + assert e['applies_when'], f\"empty applies_when for {e['id']}\" + assert e['enforcement'], f\"empty enforcement for {e['id']}\" +judgment_ids = {'go-security/chmod-return-checked'} +for e in go_sec: + if e['id'] in judgment_ids: + assert e['enforcement'] == 'judgment', f\"{e['id']} enforcement must be literal 'judgment', got: {e['enforcement']}\" +print('go-security entries: ok') +" + +# Determinism — running build-index again produces identical bytes +make build-index +git diff --exit-code rules/index.json && echo "deterministic: ok" + +# make precommit clean +make precommit +``` + diff --git a/rules/go/dir-perms-too-permissive.yml b/rules/go/dir-perms-too-permissive.yml new file mode 100644 index 0000000..b90d09e --- /dev/null +++ b/rules/go/dir-perms-too-permissive.yml @@ -0,0 +1,40 @@ +id: go-security/dir-perms-too-permissive +language: go +severity: error +message: | + Directory permissions must not be world-readable. + Use 0750 (owner-rwx, group-rx, no world) for all directories created via os.MkdirAll or os.Mkdir. + See docs/go-security-linting.md (RULE go-security/dir-perms-too-permissive). +rule: + any: + # Broadened enumeration — see file-perms-too-permissive.yml for rationale. + # 0700/0o700 intentionally NOT listed — owner-only directory (not + # world-readable), more restrictive than 0750, doesn't violate the rule's + # "no world-readable" intent. + - pattern: os.MkdirAll($PATH, 0740) + - pattern: os.MkdirAll($PATH, 0o740) + - pattern: os.MkdirAll($PATH, 0755) + - pattern: os.MkdirAll($PATH, 0o755) + - pattern: os.MkdirAll($PATH, 0770) + - pattern: os.MkdirAll($PATH, 0o770) + - pattern: os.MkdirAll($PATH, 0775) + - pattern: os.MkdirAll($PATH, 0o775) + - pattern: os.MkdirAll($PATH, 0777) + - pattern: os.MkdirAll($PATH, 0o777) + - pattern: os.Mkdir($PATH, 0740) + - pattern: os.Mkdir($PATH, 0o740) + - pattern: os.Mkdir($PATH, 0755) + - pattern: os.Mkdir($PATH, 0o755) + - pattern: os.Mkdir($PATH, 0770) + - pattern: os.Mkdir($PATH, 0o770) + - pattern: os.Mkdir($PATH, 0775) + - pattern: os.Mkdir($PATH, 0o775) + - pattern: os.Mkdir($PATH, 0777) + - pattern: os.Mkdir($PATH, 0o777) +ignores: + - "main.go" + - "**/main.go" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/go/file-perms-too-permissive.yml b/rules/go/file-perms-too-permissive.yml new file mode 100644 index 0000000..782ea7e --- /dev/null +++ b/rules/go/file-perms-too-permissive.yml @@ -0,0 +1,54 @@ +id: go-security/file-perms-too-permissive +language: go +severity: error +message: | + File permissions must not be world-readable. + Use 0600 (owner-only) for all files created via os.WriteFile or os.OpenFile. + See docs/go-security-linting.md (RULE go-security/file-perms-too-permissive). +rule: + any: + # Broadened enumeration — ast-grep 0.43.0 metavariable constraints don't filter + # int literals, so we list common gosec-flagged permissive octals explicitly. + # NOT exhaustive; uncommon values (e.g. 0641) slip through and are caught by + # gosec G306 directly + the judgment-tier review. + - pattern: os.WriteFile($PATH, $DATA, 0644) + - pattern: os.WriteFile($PATH, $DATA, 0o644) + - pattern: os.WriteFile($PATH, $DATA, 0640) + - pattern: os.WriteFile($PATH, $DATA, 0o640) + - pattern: os.WriteFile($PATH, $DATA, 0660) + - pattern: os.WriteFile($PATH, $DATA, 0o660) + - pattern: os.WriteFile($PATH, $DATA, 0664) + - pattern: os.WriteFile($PATH, $DATA, 0o664) + - pattern: os.WriteFile($PATH, $DATA, 0666) + - pattern: os.WriteFile($PATH, $DATA, 0o666) + # 0700/0o700 intentionally NOT listed — owner-only (not world-readable), + # more restrictive than 0600, doesn't violate the rule's "no world-readable" intent. + - pattern: os.WriteFile($PATH, $DATA, 0744) + - pattern: os.WriteFile($PATH, $DATA, 0o744) + - pattern: os.WriteFile($PATH, $DATA, 0755) + - pattern: os.WriteFile($PATH, $DATA, 0o755) + - pattern: os.WriteFile($PATH, $DATA, 0777) + - pattern: os.WriteFile($PATH, $DATA, 0o777) + - pattern: os.OpenFile($PATH, $FLAGS, 0644) + - pattern: os.OpenFile($PATH, $FLAGS, 0o644) + - pattern: os.OpenFile($PATH, $FLAGS, 0640) + - pattern: os.OpenFile($PATH, $FLAGS, 0o640) + - pattern: os.OpenFile($PATH, $FLAGS, 0660) + - pattern: os.OpenFile($PATH, $FLAGS, 0o660) + - pattern: os.OpenFile($PATH, $FLAGS, 0664) + - pattern: os.OpenFile($PATH, $FLAGS, 0o664) + - pattern: os.OpenFile($PATH, $FLAGS, 0666) + - pattern: os.OpenFile($PATH, $FLAGS, 0o666) + - pattern: os.OpenFile($PATH, $FLAGS, 0744) + - pattern: os.OpenFile($PATH, $FLAGS, 0o744) + - pattern: os.OpenFile($PATH, $FLAGS, 0755) + - pattern: os.OpenFile($PATH, $FLAGS, 0o755) + - pattern: os.OpenFile($PATH, $FLAGS, 0777) + - pattern: os.OpenFile($PATH, $FLAGS, 0o777) +ignores: + - "main.go" + - "**/main.go" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/go/nosec-requires-reason.yml b/rules/go/nosec-requires-reason.yml new file mode 100644 index 0000000..0e9436f --- /dev/null +++ b/rules/go/nosec-requires-reason.yml @@ -0,0 +1,16 @@ +id: go-security/nosec-requires-reason +language: go +severity: error +message: | + #nosec comments must include a `-- ` explanation. + Bare suppressions give no audit trail for the next reviewer. + See docs/go-security-linting.md (RULE go-security/nosec-requires-reason). +rule: + pattern-regex: '//\s*#nosec\b(?!.*--)' +ignores: + - "main.go" + - "**/main.go" + - "**/*_test.go" + - "vendor/**" + - "**/vendor/**" + - "**/mocks/**" diff --git a/rules/index.json b/rules/index.json index c85143a..b152378 100644 --- a/rules/index.json +++ b/rules/index.json @@ -53,6 +53,42 @@ "level": "SHOULD", "owner": "go-error-assistant" }, + { + "anchor": "go-security/chmod-return-checked", + "applies_when": "an `os.Chmod($PATH, $PERM)` call in a `*.go` file outside `*_test.go` and `vendor/` whose return value is discarded (no `if err := os.Chmod(...); err != nil` wrapper, no `_ = os.Chmod(...)` with an explanatory comment). Detecting \"return value used in error check\" requires reading the surrounding statement — pure ast-grep cannot reliably distinguish a checked `os.Chmod(...)` from an unchecked one without false positives.", + "doc_path": "docs/go-security-linting.md", + "enforcement": "judgment", + "id": "go-security/chmod-return-checked", + "level": "MUST", + "owner": "go-security-specialist" + }, + { + "anchor": "go-security/dir-perms-too-permissive", + "applies_when": "`os.MkdirAll($PATH, $PERM)` or `os.Mkdir($PATH, $PERM)` calls in a `*.go` file outside `*_test.go` and `vendor/`, where `$PERM` is a literal octal that is NOT `0750` / `0o750`.", + "doc_path": "docs/go-security-linting.md", + "enforcement": "`rules/go/dir-perms-too-permissive.yml`", + "id": "go-security/dir-perms-too-permissive", + "level": "MUST", + "owner": "go-security-specialist" + }, + { + "anchor": "go-security/file-perms-too-permissive", + "applies_when": "`os.WriteFile($PATH, $DATA, $PERM)` or `os.OpenFile($PATH, $FLAGS, $PERM)` calls in a `*.go` file outside `*_test.go` and `vendor/`, where `$PERM` is a literal octal that is NOT `0600` / `0o600`.", + "doc_path": "docs/go-security-linting.md", + "enforcement": "`rules/go/file-perms-too-permissive.yml`", + "id": "go-security/file-perms-too-permissive", + "level": "MUST", + "owner": "go-security-specialist" + }, + { + "anchor": "go-security/nosec-requires-reason", + "applies_when": "a `// #nosec ` comment in a `*.go` file outside `*_test.go` and `vendor/` appears WITHOUT a `-- ` text component on the same line.", + "doc_path": "docs/go-security-linting.md", + "enforcement": "`rules/go/nosec-requires-reason.yml`", + "id": "go-security/nosec-requires-reason", + "level": "MUST", + "owner": "go-security-specialist" + }, { "anchor": "go-time/inject-getter-not-create", "applies_when": "a factory or constructor file outside `main.go` calls `libtime.NewCurrentDateTime()`.",