--- name: subprojects description: Enumerate scannable sub-folders inside a repository. Identifies monorepo packages, workspaces, and discrete modules so the analyst can scope deep-dive scans to a specific sub-path instead of treating a huge tree as one unit. Runs at repo level; writes back a list that surfaces on the repo overview. license: MIT compatibility: Needs network access to the scrutineer API only for logging; the enumeration itself is filesystem-only against ./src. metadata: scrutineer.version: 1 scrutineer.output_file: report.json scrutineer.output_kind: subprojects --- # subprojects List the discrete scannable units inside a repository so the analyst can scope security scans to a sub-path instead of the whole tree. A repository with a single package at the root gets an empty list — that is the expected shape for the common case. A monorepo like apache/airflow or kubernetes/kubernetes gets a row per sub-package. ## Workspace - `./src` — the repository at HEAD. Read-only. - `./context.json` — has `scrutineer.api_base`, `scrutineer.token`, `scrutineer.repository_id`. Not finding-scoped, not sub-path-scoped (this skill *produces* sub-paths; it does not consume one). - `./report.json` — write the enumeration here. - `./schema.json` — output shape. ## How to identify subprojects A subproject is a sub-folder that looks like an independently buildable, scannable unit. Heuristics, in descending order of signal strength: 1. **Explicit monorepo declarations.** If the repo has any of these at the root, the file names the workspaces directly. Expand any globs against `./src` and emit one row per matched directory: - `pnpm-workspace.yaml` — `packages:` glob list - `lerna.json` — `packages:` array - `nx.json` / `workspace.json` — NX workspace layout - `turbo.json` alongside a root `package.json` with a `workspaces` field - `go.work` — `use (...)` list - root `Cargo.toml` with `[workspace].members` - `pyproject.toml` with `[tool.uv.workspace].members` or Rye/hatch equivalents 2. **Sub-folder package manifests.** Scan sub-folders (depth ≤ 3, skip `node_modules`, `vendor`, `.git`, `dist`, `build`, `target`) for any of the manifests below. Run this even when a workspace declaration exists, then union the two sets — `go.work` and friends are often incomplete. - `go.mod` → go-module - `package.json` with a `name` field → npm-package - `pyproject.toml` / `setup.py` / `setup.cfg` → python-package - `Cargo.toml` with `[package]` → rust-crate - `composer.json` → composer-package - `pom.xml` or `build.gradle[.kts]` → maven/gradle-module - `Gemfile` and/or `*.gemspec` → ruby-gem - `Package.swift` → swift-package - `Dockerfile` alongside a README in a `services/` or `apps/` tree → service 3. **Cluster by top-level directory.** If heuristic 2 produces many hits under a common parent (e.g. `providers/amazon`, `providers/google` each has its own `pyproject.toml`), keep each sub-folder as its own row rather than rolling them up — an analyst may want to scan just one cloud provider's code. ## What NOT to emit - **The root as a subproject.** A repo with one package at the root has zero subprojects; scrutineer already treats root as the default scan scope. - **Test fixtures, examples, vendored deps.** Folders called `testdata/`, `fixtures/`, `examples/`, `vendor/`, `third_party/`, `external/` almost always ship code that is not the project itself — skip them. - **Build outputs.** `dist/`, `build/`, `out/`, `target/`, `node_modules/`, `.venv/`, `__pycache__/` — same. - **Nested workspaces with no independent manifest.** If a sub-folder has no manifest and is not named by a workspace declaration, it is not a subproject; it is just a sub-folder. ## Output Write `./report.json`: ```json { "subprojects": [ { "path": "airflow-core", "name": "airflow-core", "kind": "python-package", "description": "Core Airflow scheduler, webserver, and DAG runtime." }, { "path": "airflow-ctl", "name": "airflow-ctl", "kind": "python-package", "description": "Airflow CLI distributed as a separate package." }, { "path": "providers/amazon", "name": "apache-airflow-providers-amazon", "kind": "python-package", "description": "AWS provider package. Ships operators, hooks, and sensors for S3, EMR, Glue, and other AWS services." } ] } ``` Fields: - `path` — required, relative to repo root, no leading slash. This is what scrutineer stores on `Scan.sub_path` when the analyst scans it. - `name` — short human label. Use the package's own name when the manifest has one (`name` in package.json, `module` path in go.mod, `[package].name` in Cargo.toml); otherwise the last segment of `path`. - `kind` — the detection hit: `go-module`, `npm-package`, `python-package`, `rust-crate`, `composer-package`, `maven-module`, `gradle-module`, `ruby-gem`, `swift-package`, `service`, etc. Free-form; the UI renders it as a badge. - `description` — one or two sentences. Read the README in the sub-folder if present, or infer from the package name and directory structure. Keep it specific ("AWS provider package", not "code for AWS"). ## Constraints - Empty list is the correct output for a single-package repo. Do not invent subprojects to avoid an empty array. - Do not include the root directory as a row. Root is implicit. - Do not add more than ~50 rows. If a monorepo is bigger than that, keep workspace-declared entries first, then fill the remainder by file count (largest first), and set the top-level `notes` field to `"truncated to 50 of N"`. - Scrutineer replaces the full set on each re-run, so missing rows from a previous run will disappear — do not try to merge with prior output.