@comity-dev/semgrep-rules 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Filippo Bovo and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @comity-dev/semgrep-rules
2
+
3
+ ## Purpose
4
+
5
+ Reusable Semgrep rule set for Comity structural source-code patterns. Semgrep is well-suited to **structural patterns** that ESLint cannot express cleanly: class shape, interface structure, method signatures, prohibited implementation patterns.
6
+
7
+ ## Scope
8
+
9
+ The canonical rule files live under `./rules/` as YAML. The package also exports a loader that produces a single combined Semgrep config suitable for a single `--config` invocation.
10
+
11
+ ## Ownership
12
+
13
+ Owned by `comity-development`. Consumed by `@comity-dev/validate` which actually invokes the `semgrep` binary.
14
+
15
+ The rule set targets **runtime** Core Modules and Adapters only. It does NOT apply to Development tooling packages (`comity.layer: "dev-tooling"`), which legitimately use Node APIs (`process.env`, etc.) that the rules forbid for runtime code.
16
+
17
+ ## Public API
18
+
19
+ ```ts
20
+ import {
21
+ listRuleFiles,
22
+ loadRule,
23
+ loadAllRules,
24
+ buildSemgrepConfig,
25
+ } from "@comity-dev/semgrep-rules";
26
+ ```
27
+
28
+ The `./rules` subpath exposes the raw YAML rule files:
29
+
30
+ ```ts
31
+ import rulePath from "@comity-dev/semgrep-rules/rules/<file>.yaml";
32
+ ```
33
+
34
+ ## Relationship to Comity Standards
35
+
36
+ - `adapters.md` §11 — Adapter contract implementation shape.
37
+ - `errors.md` — error class shape.
38
+ - `modules.md` — module surface shape.
39
+
40
+ ## Development
41
+
42
+ ```bash
43
+ pnpm build
44
+ pnpm test
45
+ ```
46
+
47
+ ## Tool availability
48
+
49
+ The `semgrep` binary is **optional**. When missing, the validator reports `TOOL-UNAVAILABLE` rather than fabricating PASS. Install via `pip install semgrep` or set `SEMGREP_BIN` to an absolute path.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Builds a single combined Semgrep config file from all canonical rule
3
+ * files. Semgrep's CLI accepts a single `--config`; the validate engine
4
+ * writes the combined content to a temp file and invokes semgrep with
5
+ * that path.
6
+ */
7
+ export declare function buildSemgrepConfig(): Promise<string>;
@@ -0,0 +1,19 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { listRuleFiles } from "./filesystem.js";
3
+ /**
4
+ * Builds a single combined Semgrep config file from all canonical rule
5
+ * files. Semgrep's CLI accepts a single `--config`; the validate engine
6
+ * writes the combined content to a temp file and invokes semgrep with
7
+ * that path.
8
+ */
9
+ export async function buildSemgrepConfig() {
10
+ const files = await listRuleFiles();
11
+ const ruleBlocks = [];
12
+ for (const f of files) {
13
+ const text = await readFile(f, "utf8");
14
+ // Strip the top-level `rules:` line; we want just the array entries.
15
+ const body = text.replace(/^\s*rules:\s*\n/m, "");
16
+ ruleBlocks.push(body.trimEnd());
17
+ }
18
+ return `rules:\n${ruleBlocks.join("\n")}\n`;
19
+ }
@@ -0,0 +1,11 @@
1
+ import { type ParsedSemgrepRule } from "./parse-yaml.js";
2
+ export interface SemgrepRule extends ParsedSemgrepRule {
3
+ /** The absolute path of the rule file from which this rule was loaded. */
4
+ file: string;
5
+ }
6
+ /** Returns the canonical rule file paths under `./rules/`. */
7
+ export declare function listRuleFiles(): Promise<string[]>;
8
+ /** Loads and parses a single Semgrep rule file. */
9
+ export declare function loadRule(filePath: string): Promise<SemgrepRule[]>;
10
+ /** Loads every rule in the package and returns them as a flat array. */
11
+ export declare function loadAllRules(): Promise<SemgrepRule[]>;
@@ -0,0 +1,28 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseSemgrepYaml } from "./parse-yaml.js";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const RULES_DIR = join(__dirname, "..", "rules");
7
+ /** Returns the canonical rule file paths under `./rules/`. */
8
+ export async function listRuleFiles() {
9
+ const entries = await readdir(RULES_DIR);
10
+ return entries
11
+ .filter((f) => f.endsWith(".yaml"))
12
+ .map((f) => join(RULES_DIR, f));
13
+ }
14
+ /** Loads and parses a single Semgrep rule file. */
15
+ export async function loadRule(filePath) {
16
+ const text = await readFile(filePath, "utf8");
17
+ return parseSemgrepYaml(text).map((r) => ({ ...r, file: filePath }));
18
+ }
19
+ /** Loads every rule in the package and returns them as a flat array. */
20
+ export async function loadAllRules() {
21
+ const files = await listRuleFiles();
22
+ const all = [];
23
+ for (const f of files) {
24
+ const rules = await loadRule(f);
25
+ all.push(...rules);
26
+ }
27
+ return all;
28
+ }
@@ -0,0 +1,2 @@
1
+ export { buildSemgrepConfig } from "./build-config.js";
2
+ export { listRuleFiles, loadAllRules, loadRule } from "./filesystem.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { buildSemgrepConfig } from "./build-config.js";
2
+ export { listRuleFiles, loadAllRules, loadRule } from "./filesystem.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Internal helper for parsing Semgrep YAML rule files.
3
+ *
4
+ * The YAML format we target is a strict subset:
5
+ * rules:
6
+ * - id: <name>
7
+ * severity: <level>
8
+ * languages: [<lang>]
9
+ * message: |
10
+ * <text>
11
+ * pattern: |
12
+ * <pattern>
13
+ *
14
+ * We do not use a YAML library because the canonical rule files are
15
+ * authored by the Development repository and follow a known shape.
16
+ */
17
+ export interface ParsedSemgrepRule {
18
+ id: string;
19
+ severity: string | null;
20
+ languages: string[] | null;
21
+ message: string | null;
22
+ raw: string;
23
+ }
24
+ export declare function parseSemgrepYaml(text: string): ParsedSemgrepRule[];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Internal helper for parsing Semgrep YAML rule files.
3
+ *
4
+ * The YAML format we target is a strict subset:
5
+ * rules:
6
+ * - id: <name>
7
+ * severity: <level>
8
+ * languages: [<lang>]
9
+ * message: |
10
+ * <text>
11
+ * pattern: |
12
+ * <pattern>
13
+ *
14
+ * We do not use a YAML library because the canonical rule files are
15
+ * authored by the Development repository and follow a known shape.
16
+ */
17
+ export function parseSemgrepYaml(text) {
18
+ const ruleBlockRe = /^[ \t]{2}- id:\s*(.+?)\n((?:^(?![ \t]{2}- id:|rules:|---).*\n?)*)/gm;
19
+ const rules = [];
20
+ let match;
21
+ while ((match = ruleBlockRe.exec(text)) !== null) {
22
+ const id = (match[1] ?? "").trim();
23
+ const body = match[2] ?? "";
24
+ const severityMatch = body.match(/^\s*severity:\s*(.+)$/m);
25
+ const languagesMatch = body.match(/languages:\s*\[(.+?)\]/m);
26
+ const messageMatch = body.match(/message:\s*\|?\s*\n((?:[ \t]+.+\n?)*)/m);
27
+ rules.push({
28
+ id,
29
+ severity: severityMatch?.[1]?.trim() ?? null,
30
+ languages: languagesMatch?.[1]?.split(",").map((s) => s.trim()) ?? null,
31
+ message: messageMatch?.[1]?.trim() ?? null,
32
+ raw: ` - id: ${id}\n${body}`,
33
+ });
34
+ }
35
+ return rules;
36
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@comity-dev/semgrep-rules",
3
+ "version": "0.1.0",
4
+ "description": "Reusable Semgrep rules for Comity structural source-code patterns. Development tooling.",
5
+ "type": "module",
6
+ "private": false,
7
+ "license": "MIT",
8
+ "comity": {
9
+ "layer": "dev-tooling"
10
+ },
11
+ "engines": {
12
+ "node": ">=24.0.0"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./rules": "./rules/"
22
+ },
23
+ "files": [
24
+ "./dist",
25
+ "./rules"
26
+ ],
27
+ "devDependencies": {
28
+ "@types/node": "^24.13.3",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "test": "vitest run"
34
+ }
35
+ }
@@ -0,0 +1,38 @@
1
+ rules:
2
+ - id: comity-adapter-must-declare-implements
3
+ languages: [typescript]
4
+ severity: ERROR
5
+ message: |
6
+ A Technology Adapter MUST `implements` exactly one Core Module contract.
7
+ The implementing class is the structural binding between the technology
8
+ and the contract.
9
+ - adapters.md §11
10
+ - ADR-007
11
+ pattern: |
12
+ class $NAME implements $I {
13
+ ...
14
+ }
15
+ pattern-not: |
16
+ class $NAME implements $I {
17
+ constructor(...args) { ... }
18
+ ...
19
+ }
20
+ metadata:
21
+ category: comity-architecture
22
+ standard: adapters.md
23
+ section: 11
24
+
25
+ - id: comity-adapter-no-raw-error-throw
26
+ languages: [typescript]
27
+ severity: WARNING
28
+ message: |
29
+ Adapters MAY throw platform errors, but the boundary they expose to Core
30
+ Modules MUST use Result<T, E>. Throwing raw Error from a Core-facing
31
+ method breaks the Result contract.
32
+ - errors.md §6
33
+ pattern: |
34
+ throw new Error($MSG);
35
+ metadata:
36
+ category: comity-architecture
37
+ standard: errors.md
38
+ section: 6
@@ -0,0 +1,42 @@
1
+ rules:
2
+ - id: comity-no-static-state-in-core
3
+ languages: [typescript]
4
+ severity: WARNING
5
+ message: |
6
+ Core Modules MUST NOT use static class state. Composition owns wiring;
7
+ static state in Core breaks testability.
8
+ - architecture-principles.md
9
+ pattern: |
10
+ class $NAME {
11
+ ...
12
+ static $F: $T = $V;
13
+ ...
14
+ }
15
+ paths:
16
+ include:
17
+ - "**/packages/**/src/**/*.ts"
18
+ exclude:
19
+ - "**/packages/*/adapters/**"
20
+ - "**/node_modules/**"
21
+ metadata:
22
+ category: comity-architecture
23
+ standard: architecture-principles.md
24
+
25
+ - id: comity-no-singleton-instance
26
+ languages: [typescript]
27
+ severity: WARNING
28
+ message: |
29
+ Comity avoids singletons. Provide composition facades via the kernel's
30
+ `composition` module instead.
31
+ - architecture-principles.md
32
+ pattern: |
33
+ export const $NAME = new $CLASS(...$ARGS);
34
+ paths:
35
+ include:
36
+ - "**/packages/**/src/index.ts"
37
+ - "**/packages/**/src/contracts/index.ts"
38
+ exclude:
39
+ - "**/node_modules/**"
40
+ metadata:
41
+ category: comity-architecture
42
+ standard: architecture-principles.md
@@ -0,0 +1,77 @@
1
+ rules:
2
+ - id: comity-core-no-process-env
3
+ languages: [typescript]
4
+ severity: WARNING
5
+ message: |
6
+ Core Modules MUST NOT read `process.env` directly — environment access
7
+ belongs in adapters. Inject configuration via composition facades.
8
+ - architecture-validation.md §4
9
+ - configuration.md
10
+ pattern: process.env[$KEY]
11
+ paths:
12
+ include:
13
+ - "**/packages/**/src/**/*.ts"
14
+ exclude:
15
+ - "**/packages/*/adapters/**"
16
+ - "**/node_modules/**"
17
+ metadata:
18
+ category: comity-architecture
19
+ standard: configuration.md
20
+
21
+ - id: comity-core-no-fs
22
+ languages: [typescript]
23
+ severity: WARNING
24
+ message: |
25
+ Core Modules MUST NOT import Node's `fs` API. File system access belongs
26
+ in adapters.
27
+ - architecture-validation.md §4
28
+ - adapters.md §11
29
+ pattern: import { ... } from "node:fs"
30
+ paths:
31
+ include:
32
+ - "**/packages/**/src/**/*.ts"
33
+ exclude:
34
+ - "**/packages/*/adapters/**"
35
+ - "**/node_modules/**"
36
+ metadata:
37
+ category: comity-architecture
38
+ standard: adapters.md
39
+ section: 11
40
+
41
+ - id: comity-core-no-fs-promises
42
+ languages: [typescript]
43
+ severity: WARNING
44
+ message: |
45
+ Core Modules MUST NOT import `node:fs/promises`. File system access belongs
46
+ in adapters.
47
+ - architecture-validation.md §4
48
+ - adapters.md §11
49
+ pattern: import { ... } from "node:fs/promises"
50
+ paths:
51
+ include:
52
+ - "**/packages/**/src/**/*.ts"
53
+ exclude:
54
+ - "**/packages/*/adapters/**"
55
+ - "**/node_modules/**"
56
+ metadata:
57
+ category: comity-architecture
58
+ standard: adapters.md
59
+ section: 11
60
+
61
+ - id: comity-no-console-log
62
+ languages: [typescript]
63
+ severity: WARNING
64
+ message: |
65
+ Core Modules MUST NOT use `console.log` for telemetry. Use the framework's
66
+ logger abstraction.
67
+ - coding.md
68
+ pattern: console.log(...$ARGS)
69
+ paths:
70
+ include:
71
+ - "**/packages/**/src/**/*.ts"
72
+ exclude:
73
+ - "**/packages/*/adapters/**"
74
+ - "**/node_modules/**"
75
+ metadata:
76
+ category: comity-architecture
77
+ standard: coding.md
@@ -0,0 +1,47 @@
1
+ rules:
2
+ - id: comity-error-class-must-extend-base-error
3
+ languages: [typescript]
4
+ severity: ERROR
5
+ message: |
6
+ Every Comity error class MUST extend `@comity/primitives/errors/BaseError`
7
+ or a documented domain base. Standalone Error classes lose the structured
8
+ `code` field and break cross-package error handling.
9
+ - errors.md §6
10
+ - public-api.md §3.1
11
+ patterns:
12
+ - pattern: |
13
+ class $NAME extends Error { ... }
14
+ - pattern-not: |
15
+ class $NAME extends BaseError { ... }
16
+ - pattern-not: |
17
+ class $NAME extends $BASE { ... }
18
+ - pattern-not-inside: |
19
+ /** ... */
20
+ ...
21
+ class $NAME { ... }
22
+ metadata:
23
+ category: comity-architecture
24
+ standard: errors.md
25
+ section: 6
26
+
27
+ - id: comity-error-class-must-have-code-field
28
+ languages: [typescript]
29
+ severity: WARNING
30
+ message: |
31
+ A Comity error class SHOULD expose a stable `code` field for cross-package
32
+ error matching.
33
+ - errors.md §6
34
+ pattern: |
35
+ class $NAME extends BaseError {
36
+ ...
37
+ }
38
+ pattern-not: |
39
+ class $NAME extends BaseError {
40
+ ...
41
+ readonly code: $T;
42
+ ...
43
+ }
44
+ metadata:
45
+ category: comity-architecture
46
+ standard: errors.md
47
+ section: 6
@@ -0,0 +1,43 @@
1
+ rules:
2
+ - id: comity-module-no-default-export
3
+ languages: [typescript]
4
+ severity: WARNING
5
+ message: |
6
+ Comity Core Modules MUST use named exports, not default exports. Default
7
+ exports obscure the public surface and break tree-shaking guarantees.
8
+ - public-api.md §3
9
+ - architecture-validation.md §4
10
+ pattern: export default $X
11
+ paths:
12
+ include:
13
+ - "**/packages/**/src/index.ts"
14
+ - "**/packages/**/src/contracts/index.ts"
15
+ exclude:
16
+ - "**/node_modules/**"
17
+ metadata:
18
+ category: comity-architecture
19
+ standard: public-api.md
20
+ section: 3
21
+
22
+ - id: comity-module-must-have-internal-marker
23
+ languages: [typescript]
24
+ severity: INFO
25
+ message: |
26
+ Files intended to be internal-only SHOULD be located under an `internal/`
27
+ or `lazy/` directory; the ESLint rule `@comity/no-forbidden-deep-import`
28
+ enforces the reverse — consumers MUST NOT reach into these paths.
29
+ - public-api.md §3.2
30
+ patterns:
31
+ - pattern: |
32
+ /** @internal */
33
+ ...
34
+ - pattern-not-inside: |
35
+ /** @internal */
36
+ ...
37
+ ...
38
+ /** @internal */
39
+ ...
40
+ metadata:
41
+ category: comity-architecture
42
+ standard: public-api.md
43
+ section: "3.2"
@@ -0,0 +1,56 @@
1
+ rules:
2
+ - id: comity-value-object-must-be-readonly
3
+ languages: [typescript]
4
+ severity: WARNING
5
+ message: |
6
+ Value objects MUST be immutable. All fields SHOULD be `readonly`.
7
+ - domain-modeling.md
8
+ pattern: |
9
+ class $NAME {
10
+ ...
11
+ constructor(...) {
12
+ ...
13
+ this.$F = $V;
14
+ ...
15
+ }
16
+ }
17
+ pattern-not: |
18
+ class $NAME {
19
+ ...
20
+ constructor(...) {
21
+ ...
22
+ this.$F: readonly $T = $V;
23
+ ...
24
+ }
25
+ }
26
+ paths:
27
+ include:
28
+ - "**/packages/**/src/domain/**/*.ts"
29
+ exclude:
30
+ - "**/node_modules/**"
31
+ metadata:
32
+ category: comity-domain-modeling
33
+ standard: domain-modeling.md
34
+
35
+ - id: comity-entity-must-emit-events
36
+ languages: [typescript]
37
+ severity: INFO
38
+ message: |
39
+ Domain entities SHOULD expose a typed event surface via the `@comity/primitives/events`
40
+ primitive, not raw EventEmitter.
41
+ - events.md
42
+ pattern: |
43
+ class $NAME {
44
+ ...
45
+ new EventEmitter();
46
+ ...
47
+ }
48
+ paths:
49
+ include:
50
+ - "**/packages/**/src/domain/**/*.ts"
51
+ - "**/packages/**/src/core/**/*.ts"
52
+ exclude:
53
+ - "**/node_modules/**"
54
+ metadata:
55
+ category: comity-events
56
+ standard: events.md