220 lines
8.8 KiB
Scheme
220 lines
8.8 KiB
Scheme
;; pmacs audit-lint rule set, v1.0 (T M7.9).
|
|
;;
|
|
;; Each pattern below corresponds to one entry in
|
|
;; `src/audit/rules.rs::DEFAULT_RULES` *by index*. Reordering or
|
|
;; inserting patterns here without updating the Rust table is a bug;
|
|
;; the unit test `rules_table_aligns_with_query_file` catches it.
|
|
;;
|
|
;; The outermost node of every pattern is captured as `@violation`.
|
|
;; The audit engine reports that node's byte range as the finding's
|
|
;; snippet and its row/column as the location.
|
|
;;
|
|
;; Style: queries deliberately match exact identifier text rather
|
|
;; than relying on naming heuristics. A package author who routes
|
|
;; through a local alias (`local f = io.open` then `f("x", "w")`)
|
|
;; bypasses these rules; that is acceptable for v1.0 -- the rule
|
|
;; set is a reviewable contract, not an undefeatable barrier.
|
|
;; Whole-program data-flow analysis is reserved for the full-moon
|
|
;; tier (see TRANSITION-M7.md, T M7.9 section).
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 0: no-private-surface (require)
|
|
;;
|
|
;; Forbids `require("pmacs._internal.X")` and `require("pmacs.core.X")`.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(identifier) @fn
|
|
(arguments (string (string_content) @arg)))
|
|
(#eq? @fn "require")
|
|
(#match? @arg "^pmacs\\.(_internal|core)(\\.|$)")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 1: no-private-surface (identifier)
|
|
;;
|
|
;; Forbids any identifier prefixed with `_pmacs_internal_` or `_core_`.
|
|
;; ---------------------------------------------------------------------------
|
|
((identifier) @id
|
|
(#match? @id "^(_pmacs_internal_|_core_)")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 2: no-ffi-call
|
|
;;
|
|
;; Forbids `ffi.cdef`, `ffi.load`, `ffi.metatype` (LuaJIT FFI surface).
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "ffi")
|
|
(#match? @m "^(cdef|load|metatype)$")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 3: no-package-loadlib
|
|
;;
|
|
;; Forbids `package.loadlib(...)` (Lua 5.4 native-library equivalent of
|
|
;; the LuaJIT FFI surface).
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "package")
|
|
(#eq? @m "loadlib")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 4: no-package-cpath-mutation
|
|
;;
|
|
;; Forbids assignment to `package.cpath` (would extend the C-loader
|
|
;; search path, defeating no-FFI).
|
|
;; ---------------------------------------------------------------------------
|
|
((assignment_statement
|
|
(variable_list (dot_index_expression (identifier) @t (identifier) @f)))
|
|
(#eq? @t "package")
|
|
(#eq? @f "cpath")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 5: no-debug-sethook
|
|
;;
|
|
;; Forbids `debug.sethook(...)`. The pmacs cancellation hook (T M7.8)
|
|
;; is installed once at VM init; a package overwriting it disables
|
|
;; cooperative cancellation editor-wide.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "debug")
|
|
(#eq? @m "sethook")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 6: no-debug-setmetatable
|
|
;;
|
|
;; Forbids `debug.setmetatable(...)` (bypasses normal metatable rules,
|
|
;; can re-skin third-party tables silently).
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "debug")
|
|
(#eq? @m "setmetatable")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 7: no-rawget-rawset-on-globals
|
|
;;
|
|
;; Forbids `rawget(_G, ...)` and `rawset(_G, ...)` (escape from any
|
|
;; per-package _ENV sandboxing into the shared global table).
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(identifier) @fn
|
|
(arguments . (identifier) @arg1))
|
|
(#match? @fn "^(rawget|rawset)$")
|
|
(#eq? @arg1 "_G")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 8: no-setfenv-getfenv
|
|
;;
|
|
;; Forbids `setfenv` and `getfenv` (Lua 5.1 / LuaJIT environment
|
|
;; manipulation; absent from Lua 5.4 by design but still callable on
|
|
;; LuaJIT). Static analysis cannot tell whether the target stack
|
|
;; frame belongs to another package, so any call is flagged.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(identifier) @fn)
|
|
(#match? @fn "^(setfenv|getfenv)$")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 9: no-fs-mutation-io-open-write
|
|
;;
|
|
;; Warns on `io.open(<path>, "w"|"a"|"w+"|"a+"|"r+"|"wb"|...)`. The
|
|
;; mode string contains 'w' or 'a' or '+' for any non-read-only mode.
|
|
;; Read-only `io.open(path, "r")` is allowed; omitting the mode
|
|
;; defaults to read and is also allowed.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m)
|
|
(arguments . (_) (string (string_content) @mode)))
|
|
(#eq? @t "io")
|
|
(#eq? @m "open")
|
|
(#match? @mode "[wa+]")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 10: no-fs-mutation-os
|
|
;;
|
|
;; Warns on `os.remove(...)` and `os.rename(...)`.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "os")
|
|
(#match? @m "^(remove|rename)$")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 11: no-process-spawn-io
|
|
;;
|
|
;; Warns on `io.popen(...)`.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "io")
|
|
(#eq? @m "popen")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 12: no-process-spawn-os
|
|
;;
|
|
;; Warns on `os.execute(...)`.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression (identifier) @t (identifier) @m))
|
|
(#eq? @t "os")
|
|
(#eq? @m "execute")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 13: no-process-spawn-pmacs
|
|
;;
|
|
;; Warns on `pmacs.process.spawn(...)`. Legitimate users (REPL,
|
|
;; magit, LSP launcher) declare process access in their manifest;
|
|
;; anything else is a finding.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(dot_index_expression
|
|
(dot_index_expression (identifier) @t1 (identifier) @t2)
|
|
(identifier) @m))
|
|
(#eq? @t1 "pmacs")
|
|
(#eq? @t2 "process")
|
|
(#eq? @m "spawn")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 14: reach-around-require
|
|
;;
|
|
;; Info-level: detects `require("name.submodule")` calls where the
|
|
;; require target is dotted (looks like cross-package access). The
|
|
;; auditor classifies each finding against the target package's
|
|
;; declared `exports`. Absent a known-package registry, this rule
|
|
;; fires once per dotted require and a human verifies. With the
|
|
;; registry (`pmacs-audit --known-packages packages.toml`) the
|
|
;; engine promotes confirmed cross-package private accesses to
|
|
;; Error-level findings.
|
|
;;
|
|
;; The `pmacs.X` namespaces (the host's own surface) are excluded
|
|
;; because rules 0/1 already handle private-surface access there.
|
|
;; ---------------------------------------------------------------------------
|
|
((function_call
|
|
(identifier) @fn
|
|
(arguments (string (string_content) @arg)))
|
|
(#eq? @fn "require")
|
|
(#match? @arg "\\.")
|
|
(#not-match? @arg "^pmacs(\\.|$)")) @violation
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Rule 15: reach-around-require-field
|
|
;;
|
|
;; Info-level: detects field access directly on a required module
|
|
;; when the field name is private-looking (`_name`) or a loud
|
|
;; in-tree test seam (`__pmacs_*_DO_NOT_USE`). This catches
|
|
;; `require("pkg")._private` / `require("pkg").__pmacs_X_DO_NOT_USE`,
|
|
;; which the dotted-require rule above cannot see.
|
|
;;
|
|
;; The host `pmacs.X` namespaces are excluded for the same reason as
|
|
;; Rule 14.
|
|
;; ---------------------------------------------------------------------------
|
|
((dot_index_expression
|
|
(function_call
|
|
(identifier) @fn
|
|
(arguments (string (string_content) @arg)))
|
|
(identifier) @field)
|
|
(#eq? @fn "require")
|
|
(#not-match? @arg "^pmacs(\\.|$)")
|
|
(#match? @field "^_|^__pmacs_.*_DO_NOT_USE$")) @violation
|