@reneturcios/hookcheck 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ Every surface above is discovered by one bounded recursive walk, not only at the
31
31
  Classification per hook:
32
32
 
33
33
  - **SAFE**: matches known safe dev patterns such as test, lint, build, typecheck, husky, or lint-staged, and matches no risky rule.
34
- - **RISKY**: matches at least one risky rule: network fetch, curl piped to an interpreter, base64 decode piped to an interpreter, credential store paths or credential files (matched on content and paths, never on a script filename that merely contains the word credentials), credential shaped environment names (token, secret, password, api key) or bulk environment access, base64 decoding or eval style obfuscation, inline code execution, or a remote URL. Ordinary single named environment reads (NODE_ENV, HOME, RUNNER_DEBUG) are not flagged. Neither are operations that are not reads: `env -u NAME` unsets a variable, `os.environ.setdefault(...)` writes one key with a default, and a `[ -n "$NAME" ]` test checks existence. A hook whose target is a shell script is classified by that script's contents, not by the fact that it is a shell script. Comments are stripped before classification, so a docs URL inside a comment is not a fetch, and a fetch whose every URL is loopback (a localhost health check) is not treated as network behavior. In script files (JavaScript, Python), the contents of string and regex literals are data, not behavior: a defensive guard whose pattern list quotes `eval(` or `curl | sh` performs neither, and hookcheck does not classify it as an attacker. Literals handed to exec style calls (`execSync("...")`, `os.system("...")`) are code by construction and are still matched. Two rules are about value flow, not co-occurrence: ENV-EXFIL fires only when a credential value demonstrably flows into an outbound send (a request payload, a query string, a pipe into a network tool, or printed output next to a send), and CRED-EXFIL when credential store contents are piped or posted straight into a network send. Environment inheritance is not a send: when the only environment movement is inheritance or bulk access passed as a subprocess environment (env=os.environ, a {**os.environ, ...} spread, dict(os.environ), os.environ.copy(), and their JavaScript forms), ENV-EXFIL additionally requires a non-loopback destination in the flow, so a test harness that starts its own server on 127.0.0.1 and hands it the inherited environment does not verdict as exfiltration; the same forms serialized into a payload, or paired with a remote destination, still fire. The ENV-BULK evidence names the form it saw: inheritance and copy forms are described as bulk access in inheritance or copy form, not as a serialization or dump. A token used as authentication (an Authorization header, the userinfo of a push URL to the repo's own host, a `--token` flag) is authentication, not exfiltration, and never fires either rule. Authentication positions are recognized by shape, not only by the Authorization spelling: any curl `-H` header argument whatever its name, any dict or object entry whose key is an authentication header name (Authorization, `x-api-key`, `*-api-key`, `*-auth-token`, and friends) in a one hop script file, and any header shaped entry inside a headers construction (a dict assigned to a headers variable, a `headers=` argument, a headers property of a request options object) all count as authentication positions, while the same credential in a body, a query string, or printed output still counts as exfiltration. Fetch detection requires command position: curl or wget counts as a network fetch only when it is the invoked command (the head of a statement or pipeline segment, after sudo or env wrappers, or the head of a command string handed to an interpreter), never when the word merely names a package in an install line (`pacman -S curl`, `apt-get install curl`). In script files the remote URL rule follows the same data view as the other behavior rules: a URL that appears only inside a string literal or a comment (an error message, a documentation link, an API base constant) is data, while a URL handed to a network call (fetch, requests, http.get, urlopen, axios, sendBeacon) is a fetch target and still fires. Inline code execution (DOWNLOAD-EXEC) fires only when the evaluated content itself carries a download or remote reference; constant inline code such as a `node -e` version comparison downloads nothing and is not flagged.
34
+ - **RISKY**: matches at least one risky rule: network fetch, curl piped to an interpreter, base64 decode piped to an interpreter, credential store paths or credential files (matched on content and paths, never on a script filename that merely contains the word credentials), credential shaped environment names (token, secret, password, api key) or bulk environment access, base64 decoding or eval style obfuscation, inline code execution, or a remote URL. Ordinary single named environment reads (NODE_ENV, HOME, RUNNER_DEBUG) are not flagged. Neither are operations that are not reads: `env -u NAME` unsets a variable, `os.environ.setdefault(...)` writes one key with a default, and a `[ -n "$NAME" ]` test checks existence. A hook whose target is a shell script is classified by that script's contents, not by the fact that it is a shell script. Comments are stripped before classification, so a docs URL inside a comment is not a fetch, and a fetch whose every URL is loopback (a localhost health check) is not treated as network behavior. In script files (JavaScript, Python), the contents of string and regex literals are data, not behavior: a defensive guard whose pattern list quotes `eval(` or `curl | sh` performs neither, and hookcheck does not classify it as an attacker. Literals handed to exec style calls (`execSync("...")`, `os.system("...")`) are code by construction and are still matched. Two rules are about value flow, not co-occurrence: ENV-EXFIL fires only when a credential value demonstrably flows into an outbound send (a request payload, a query string, a pipe into a network tool, or printed output next to a send), and CRED-EXFIL when credential store contents are piped or posted straight into a network send. Environment inheritance is not a send: when the only environment movement is inheritance or bulk access passed as a subprocess environment (env=os.environ, a {**os.environ, ...} spread, dict(os.environ), os.environ.copy(), and their JavaScript forms), ENV-EXFIL additionally requires a non-loopback destination in the flow, so a test harness that starts its own server on 127.0.0.1 and hands it the inherited environment does not verdict as exfiltration; the same forms serialized into a payload, or paired with a remote destination, still fire. The ENV-BULK evidence names the form it saw: inheritance and copy forms are described as bulk access in inheritance or copy form, not as a serialization or dump. A token used as authentication (an Authorization header, the userinfo of a push URL to the repo's own host, a `--token` flag) is authentication, not exfiltration, and never fires either rule. Authentication positions are recognized by shape, not only by the Authorization spelling: any curl `-H` header argument whatever its name, any dict or object entry whose key is an authentication header name (Authorization, `x-api-key`, `*-api-key`, `*-auth-token`, and friends) in a one hop script file, and any header shaped entry inside a headers construction (a dict assigned to a headers variable, a `headers=` argument, a headers property of a request options object) all count as authentication positions. Further authentication positions are recognized by the same shape principle: a curl `-u` or `--user` basic auth pair, the value configured for a git `http.extraheader` key (an Authorization header value however it was built), an `::add-mask::` registration (printing a value there is the masking idiom itself), and destination positions in the URL of a send itself: a credential that is an entire URL path segment, the whole URL, or the URL origin (capability URLs are credentials by design: a build hook URL, a preview token in the path, a token query parameter), the value of an authentication named query parameter (`token`, `api_key`, `access_token`, `auth`, `secret`, `signature`, `key`), a bare reference in the curl URL operand position, and the destination first argument of a fetch style call. The body test decides: a credential presented in one of those positions with no credential or bulk data in the body is authentication, while the same credential in a body, in a data named query parameter, in a partial URL path segment, or in printed output still counts as exfiltration, and a capability URL shape plus credential data in the body still fires. Taint does not pass through a locally defined helper function's return value unless the helper itself returns tainted data (its body is analyzed as part of the same text), a printed length or count of a value (`len(x)`, `x.length`, `${#NAME}`) is metadata rather than a disclosure of the value, and a name occurrence that is an object key, a property name, an assignment target, or a parameter declaration is a binding, not a reference to the variable of that name. A shell variable assigned in the same text from a value containing no credential atom (a token named response variable bound to a literal file path, used as a curl `--output` sink), or bound via a command substitution whose only credential atoms sit in destination positions (an HTTP status code variable), is not itself a credential value. Fetch detection requires command position: curl or wget counts as a network fetch only when it is the invoked command (the head of a statement or pipeline segment, after sudo or env wrappers, or the head of a command string handed to an interpreter), never when the word merely names a package in an install line (`pacman -S curl`, `apt-get install curl`). In script files the remote URL rule follows the same data view as the other behavior rules: a URL that appears only inside a string literal or a comment (an error message, a documentation link, an API base constant) is data, while a URL handed to a network call (fetch, requests, http.get, urlopen, axios, sendBeacon) is a fetch target and still fires. Inline code execution (DOWNLOAD-EXEC) fires only when the evaluated content itself carries a download or remote reference; constant inline code such as a `node -e` version comparison downloads nothing and is not flagged.
35
35
  - **UNKNOWN**: matches neither safe nor risky patterns. A local script name alone is UNKNOWN until a human reads it. When a hook runs a local file such as `node setup.mjs`, hookcheck reads that file and escalates the finding if the file itself matches risky rules. The same one hop following applies to direct script execution (`./setup.sh`), `node --require ./setup.mjs`, and package.json script chains (`postinstall: npm run other-script`). If the referenced file itself spawns a further local script, the chain runs past the one hop limit: the finding is floored at UNKNOWN, never SAFE, and the evidence names the second hop file, so the verdict lands at REVIEW for a hand read.
36
36
 
37
37
  Verdict rules:
@@ -48,7 +48,7 @@ Honest boundaries, pinned by tests in test/hardening.test.ts:
48
48
 
49
49
  - One hop only. hookcheck follows a hook into the local file or package.json script it directly invokes, and classifies that target. It does not follow a second hop, and it does not trace data flow inside a script. A payload assembled at runtime from pieces that never appear as a recognizable pattern in any single file will not be named; the hook that runs it will usually still land in UNKNOWN or REVIEW, which is the signal to read it by hand. When the first hop file spawns a further local script, that landing is enforced rather than usual: the finding is floored at UNKNOWN and the evidence names the second hop.
50
50
  - Pattern based, not execution based. hookcheck never runs the hooks it finds. A RISKY verdict means the command text matches a known bad shape, not that the payload was observed doing harm. A CLEAN verdict means no known shape matched, not that the repo is proven safe.
51
- - Exfil detection is a static value flow check inside one finding: a credential value must visibly reach a send construct in the same command or referenced file (including through one variable assignment chain). A value laundered through several files, assembled at runtime, or sent by a tool whose arguments never name it will not be named as exfil; the finding still lands in RISKY or UNKNOWN on its other evidence. Conversely, a credential value placed in a request body or query string counts as exfil shape even to a first party host, because credentials belong in authentication positions, not payloads.
51
+ - Exfil detection is a static value flow check inside one finding: a credential value must visibly reach a send construct in the same command or referenced file (including through one variable assignment chain). A value laundered through several files, assembled at runtime, or sent by a tool whose arguments never name it will not be named as exfil; the finding still lands in RISKY or UNKNOWN on its other evidence. Conversely, a credential value placed in a request body, in a data named query parameter, or in a partial URL path segment counts as exfil shape even to a first party host, because credentials belong in authentication positions, not payloads; a credential presented as an entire URL path segment, as the whole URL, or as the value of an authentication named query parameter is a capability URL presentation and counts as authentication when the body carries no credential or bulk data.
52
52
  - Quote awareness has edges. In script files, string and regex literal contents are treated as data for the behavior rules; a payload hidden inside a string that is assembled and executed indirectly (not through a recognized exec call) can be missed. In shell text, quoted strings are command text and stay visible, except echo and printf arguments, which are output data.
53
53
  - Remote pre-commit repos and remote MCP servers are inventoried, not verified. hookcheck cannot see inside a remote pre-commit repo, so those entries are UNKNOWN with a note to review the pinned rev. A remote MCP server URL is RISKY by construction, because the server autostarts and is remote.
54
54
  - Duplicate JSON keys resolve last-wins, exactly as JSON.parse, Node, and npm resolve them, so the scanner sees the same value that would actually run.
package/dist/core.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { relative } from "node:path";
2
2
  import type { Classification, HookFinding, ScanReport, Verdict } from "./types.js";
3
- export declare const VERSION = "0.1.9";
3
+ export declare const VERSION = "0.1.11";
4
4
  interface RuleDef {
5
5
  id: string;
6
6
  pattern: RegExp;