Skip to content

Commit f65b6af

Browse files
leliaclaude
andauthored
Improve monorepo scan diagnostics and guidance (#325)
* Improve monorepo scan diagnostics * Bump version to 2.6.9 * Bump version to 2.7.0 * docs: document the monorepo scan layout trade-off The mechanics of --sub-path and --workspace-name were documented, but not the choice they force. One combined scan gives a single dashboard entry and no per-component attribution; one scan per component gives attribution, baselines and per-component policy, but adds a repository entry per component, which grows the dashboard's repository list. There is no layout that provides both today. Customers hit this at a dozen-plus components and reasonably assume they have configured something wrong. Naming the trade-off, and adding rules of thumb for picking a side, is cheaper than each of them discovering it. Cross-referenced from the CI/CD guide's independent-workspace pattern, which is the layout that grows the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e50b3aa commit f65b6af

12 files changed

Lines changed: 592 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 2.8.0
4+
5+
### Changed: improve monorepo scan diagnostics and guidance
6+
7+
- Added aggregate scan configuration, manifest-count, baseline-selection, and
8+
fallback diagnostics without listing submitted manifest paths.
9+
- Clarified monorepo scan scoping, workspace flags, CI path filters, and timeout
10+
behavior, with a changed-workspace GitHub Actions example.
11+
12+
### Fixed: apply configured exit codes to API failures
13+
14+
- Full-scan and streamed-diff API failures now use the configured infrastructure
15+
error exit code instead of the security-finding exit code.
16+
317
## 2.7.2
418

519
### Changed: bump pinned @coana-tech/cli to 15.10.39

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ value — e.g. a Buildkite
229229
code, or `0` to swallow infra errors. Exit `3` is a Socket convention, not an
230230
industry standard.
231231

232+
This mapping applies to errors the CLI receives and handles. An external process
233+
supervisor (for example GNU `timeout`) can terminate the CLI before it handles an
234+
error, so the supervisor's exit status (commonly 124 or 137) takes precedence.
235+
232236
### How these options interact
233237

234238
The two flags that affect exit codes can cancel each other out, so the order of

docs/ci-cd.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,239 @@ Equivalent JSON:
7171
SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }}
7272
```
7373
74+
#### GitHub Actions: scan changed monorepo workspaces independently
75+
76+
GitHub Actions `paths` filters only decide whether a workflow starts. They do not
77+
change `socketcli` discovery or upload scope. For a merge gate, it is usually safer
78+
to start a small selector job on every PR update, then create one scan job per
79+
affected logical workspace. This also avoids a required check remaining pending
80+
when GitHub skips the entire workflow because of a top-level path filter.
81+
82+
This pattern produces one dashboard entry per logical workspace, which is what
83+
gives each component its own alerts, baseline, and policy. It is also the layout
84+
that grows the dashboard's repository list. See
85+
[Choosing a scan layout](cli-reference.md#choosing-a-scan-layout) for when that
86+
trade-off is worth making.
87+
88+
Define a repository variable named `SOCKET_MONOREPO_WORKSPACES_JSON`. Its value is
89+
an array with one stable workspace name, one or more scan roots, and the path globs
90+
that should select that workspace. Fill these placeholders with the repository's
91+
real layout. A workspace definition selects directory roots; shared root manifests,
92+
lockfiles, and cross-directory path dependencies outside those roots are not included
93+
automatically.
94+
95+
```json
96+
[
97+
{
98+
"name": "<stable-workspace-name>",
99+
"sub_paths": ["<repo-relative-scan-root>"],
100+
"watch_globs": ["<repo-relative-changed-file-glob>"]
101+
}
102+
]
103+
```
104+
105+
Each `sub_paths` value must be a directory, not an individual manifest or lockfile.
106+
Using `.` includes the entire target path. Do not use this changed-workspace pattern
107+
until the directory boundaries preserve every shared input needed to resolve each
108+
logical graph. If root workspace metadata governs most or all of the repository, a
109+
smaller coverage-preserving split may not be representable with `--sub-path` alone.
110+
111+
Also define `SOCKETCLI_VERSION` as the exact package version validated for the
112+
workflow. The workflow below logs that version, uses full Git history for reliable
113+
base/head selection, creates one matrix job (and therefore one graph and baseline)
114+
per selected workspace, and fails closed on CLI/API/timeout failures. It uses API
115+
SCM mode plus `--enable-diff` because parallel `--scm github` jobs can race while
116+
updating the same PR comments; the matrix checks and report links are the gate.
117+
118+
```yaml
119+
name: Socket Security
120+
121+
on:
122+
pull_request:
123+
types: [opened, synchronize, reopened]
124+
push:
125+
branches: [main]
126+
127+
permissions:
128+
contents: read
129+
130+
jobs:
131+
select-workspaces:
132+
runs-on: ubuntu-latest
133+
outputs:
134+
count: ${{ steps.select.outputs.count }}
135+
matrix: ${{ steps.select.outputs.matrix }}
136+
steps:
137+
- uses: actions/checkout@v5
138+
with:
139+
fetch-depth: 0
140+
persist-credentials: false
141+
142+
- id: select
143+
name: Select changed workspaces
144+
env:
145+
WORKSPACES_JSON: ${{ vars.SOCKET_MONOREPO_WORKSPACES_JSON }}
146+
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
147+
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
148+
shell: bash
149+
run: |
150+
python - <<'PY'
151+
import fnmatch
152+
import json
153+
import os
154+
import re
155+
import subprocess
156+
157+
workspaces = json.loads(os.environ["WORKSPACES_JSON"])
158+
if not isinstance(workspaces, list):
159+
raise SystemExit("SOCKET_MONOREPO_WORKSPACES_JSON must be a JSON array")
160+
161+
base = os.environ["BASE_SHA"]
162+
head = os.environ["HEAD_SHA"]
163+
if not base or set(base) == {"0"}:
164+
base = subprocess.check_output(
165+
["git", "rev-parse", f"{head}^"], text=True
166+
).strip()
167+
changed_output = subprocess.check_output(
168+
["git", "diff", "--name-only", "-z", base, head]
169+
)
170+
changed = [
171+
item.decode("utf-8", "surrogateescape")
172+
for item in changed_output.split(b"\0")
173+
if item
174+
]
175+
176+
selected = []
177+
for workspace in workspaces:
178+
name = workspace.get("name", "")
179+
sub_paths = workspace.get("sub_paths") or []
180+
watch_globs = workspace.get("watch_globs") or []
181+
if not re.fullmatch(r"[A-Za-z0-9._-]+", name):
182+
raise SystemExit(f"Invalid workspace name: {name!r}")
183+
if not sub_paths or any(
184+
not isinstance(path, str)
185+
or path.startswith("/")
186+
or ".." in path.split("/")
187+
for path in sub_paths
188+
):
189+
raise SystemExit(f"Invalid sub_paths for workspace {name!r}")
190+
if not watch_globs:
191+
watch_globs = [
192+
pattern
193+
for path in sub_paths
194+
for pattern in (
195+
["*"]
196+
if path.strip("/") in ("", ".")
197+
else [path.rstrip("/"), f"{path.rstrip('/')}/*"]
198+
)
199+
]
200+
if any(
201+
fnmatch.fnmatchcase(path, pattern)
202+
for path in changed
203+
for pattern in watch_globs
204+
):
205+
selected.append({"name": name, "sub_paths": sub_paths})
206+
207+
matrix = json.dumps({"include": selected}, separators=(",", ":"))
208+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
209+
output.write(f"count={len(selected)}\n")
210+
output.write(f"matrix={matrix}\n")
211+
PY
212+
213+
scan-workspace:
214+
needs: select-workspaces
215+
if: needs.select-workspaces.outputs.count != '0'
216+
timeout-minutes: 20
217+
strategy:
218+
fail-fast: false
219+
matrix: ${{ fromJSON(needs.select-workspaces.outputs.matrix) }}
220+
name: Socket scan (${{ matrix.name }})
221+
runs-on: ubuntu-latest
222+
steps:
223+
- uses: actions/checkout@v5
224+
with:
225+
fetch-depth: 0
226+
persist-credentials: false
227+
228+
- uses: actions/setup-python@v6
229+
with:
230+
python-version: '3.12'
231+
232+
- name: Install pinned Socket CLI
233+
env:
234+
SOCKETCLI_VERSION: ${{ vars.SOCKETCLI_VERSION }}
235+
run: |
236+
python -m pip install "socketsecurity==$SOCKETCLI_VERSION"
237+
socketcli --version
238+
239+
- name: Scan workspace
240+
env:
241+
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }}
242+
PR_NUMBER: ${{ github.event.pull_request.number || 0 }}
243+
WORKSPACE_NAME: ${{ matrix.name }}
244+
SUB_PATHS_JSON: ${{ toJSON(matrix.sub_paths) }}
245+
shell: bash
246+
run: |
247+
set +e
248+
args=(
249+
--target-path "$GITHUB_WORKSPACE"
250+
--workspace-name "$WORKSPACE_NAME"
251+
--enable-diff
252+
--pr-number "$PR_NUMBER"
253+
--exit-code-on-api-error 3
254+
--report-link-file socket-report-link.txt
255+
--summary-file socket-summary.txt
256+
)
257+
while IFS= read -r sub_path; do
258+
args+=(--sub-path "$sub_path")
259+
done < <(jq -r '.[]' <<<"$SUB_PATHS_JSON")
260+
261+
socketcli "${args[@]}" 2>&1 | tee socket-output.log
262+
code=${PIPESTATUS[0]}
263+
264+
{
265+
echo "## Socket scan: $WORKSPACE_NAME"
266+
if [ -s socket-report-link.txt ]; then
267+
echo "[View the report]($(cat socket-report-link.txt))"
268+
fi
269+
if [ -s socket-summary.txt ]; then
270+
echo '```'
271+
cat socket-summary.txt
272+
echo '```'
273+
fi
274+
} >> "$GITHUB_STEP_SUMMARY"
275+
276+
exit "$code"
277+
278+
socket-security:
279+
if: always()
280+
needs: [select-workspaces, scan-workspace]
281+
runs-on: ubuntu-latest
282+
steps:
283+
- name: Enforce matrix result
284+
env:
285+
SELECT_RESULT: ${{ needs.select-workspaces.result }}
286+
SCAN_RESULT: ${{ needs.scan-workspace.result }}
287+
run: |
288+
test "$SELECT_RESULT" = success
289+
[[ "$SCAN_RESULT" = success || "$SCAN_RESULT" = skipped ]]
290+
```
291+
292+
Each configuration object may intentionally contain several `sub_paths` when
293+
those directories are one logical dependency graph. To split backend resolution,
294+
use separate objects with different `name` values. Add `--workspace <name>` only
295+
when the Socket organization requires API workspace association; it is not a scan
296+
scope control. Use `--save-submitted-files-list` in a non-required canary to verify
297+
the exact manifests selected before adopting workspace-level scans as a merge gate.
298+
299+
The job has an explicit 20-minute total budget. Tune that value from observed
300+
workspace-level latency after the split; a five-minute cap can still be too close
301+
to a slow request plus local startup. The CLI's `--timeout` is different: it
302+
defaults to 1,200 seconds **per API request**. If an operator adds GNU `timeout`,
303+
that process supervisor can terminate the CLI before it maps an error through
304+
`--exit-code-on-api-error`; without `--preserve-status`, GNU reports 124 after its
305+
initial timeout signal or 137 if `SIGKILL` is involved.
306+
74307
### Buildkite
75308

76309
```yaml

docs/cli-reference.md

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,67 @@ Pre-configured workflow files are in [`../workflows/`](../workflows/).
5353

5454
> **Note:** If you're looking to associate a scan with a named Socket workspace (e.g. because your repo is identified as `org/repo`), see the [`--workspace` flag](#repository) instead. The `--workspace-name` flag described in this section is an unrelated monorepo feature.
5555
56-
The Socket CLI supports scanning specific workspaces within monorepo structures while preserving git context from the repository root. This is useful for organizations that maintain multiple applications or services in a single repository.
56+
The Socket CLI supports scanning selected directories within a monorepo while preserving git context from the repository root. Scan scope is controlled by `--target-path` and `--sub-path`; CI workflow path filters and the CLI's changed-file detection do not narrow the manifests uploaded after a scan starts.
5757

5858
### Key Features
5959

60-
- **Multiple Sub-paths**: Specify multiple `--sub-path` options to scan different directories within your monorepo
61-
- **Combined Workspace**: All sub-paths are scanned together as a single workspace in Socket
60+
- **Target path**: Supplies repository/Git context and is the discovery root when no `--sub-path` is present
61+
- **Multiple Sub-paths**: Restrict discovery to those directories, but combine every repeated `--sub-path` into one upload and one server-side dependency graph
6262
- **Git Context Preserved**: Repository metadata (commits, branches, etc.) comes from the main target-path
63-
- **Workspace Naming**: Use `--workspace-name` to differentiate scans from different parts of your monorepo
63+
- **Workspace Naming**: Use a stable, unique `--workspace-name` for each independently scanned logical workspace; it suffixes the repository slug and therefore gives that workspace its own repository head/baseline
64+
65+
`--workspace` is different: it sends Socket organization workspace context with the full-scan API request. It does not narrow client-side filesystem discovery, split the upload into independent scans, or change the repository suffix. Backend policy/routing for that workspace remains server-owned.
66+
67+
> **Performance consequence:** If the goal is smaller independently resolvable graphs, run one CLI invocation per logical workspace, with a distinct `--workspace-name`. Adding several unrelated directories to one command with repeated `--sub-path` flags still asks the backend to resolve one combined graph.
68+
69+
Normal scan logs include the effective repository and Socket workspace context,
70+
repository-relative discovery roots, aggregate manifest count, and selected baseline.
71+
Individual manifest paths remain opt-in through `--save-submitted-files-list`.
72+
73+
### Choosing a scan layout
74+
75+
`--sub-path` and `--workspace-name` support two layouts, and picking between them
76+
is a trade-off rather than a preference. There is no third option today.
77+
78+
**One combined scan** — a single invocation, no `--workspace-name`, with
79+
`--target-path` at the repository root or several repeated `--sub-path` values
80+
sharing one workspace name:
81+
82+
- One dashboard entry for the repository, named after the repository
83+
- One server-side dependency graph covering everything that was uploaded
84+
- Alerts are **not** broken out by component, so a finding does not tell you which
85+
part of the monorepo introduced it
86+
- Transitive findings can surface without a clear owning component, because the
87+
combined graph has no component boundaries to attribute them to
88+
89+
**One scan per component** — a separate invocation per component, each with its
90+
own `--sub-path` and a distinct `--workspace-name`:
91+
92+
- Per-component alerts, baselines, and policy
93+
- Each component gets its own dependency graph, which is also the faster option
94+
(see the performance note above)
95+
- But `--workspace-name` suffixes the repository slug, so *N* components produce
96+
*N* separate entries in the dashboard's repository list
97+
98+
The second point is what makes this a real choice: a monorepo with a dozen or more
99+
independently scanned components produces a dozen or more repository entries, which
100+
gets hard to navigate as the list grows. A single consolidated entry that still
101+
preserves per-component attribution is a known request and is not available today.
102+
103+
Rules of thumb:
104+
105+
- **Few components, or components that share a release cycle** — use one combined
106+
scan and accept coarser attribution.
107+
- **Many components, or components with different owners or policies** — use
108+
per-component scans and accept the extra dashboard entries. Per-component policy
109+
is only possible in this layout.
110+
- **Components that are genuinely one application** — group them under a single
111+
`--workspace-name`, as in the first example below. Grouping is per logical
112+
application, not per directory.
64113

65114
### Usage Examples
66115

67-
**Scan multiple frontend and backend workspaces:**
116+
**Scan several directories that belong to one logical application:**
68117
```bash
69118
socketcli --target-path /path/to/monorepo \
70119
--sub-path frontend \
@@ -89,6 +138,19 @@ This will:
89138
- Create a repository in Socket named like `my-repo-mobile-web`
90139
- Preserve git context (commits, branch info) from the repository root
91140

141+
**Create independent frontend and backend scans:**
142+
```bash
143+
socketcli --target-path /path/to/monorepo \
144+
--sub-path frontend \
145+
--workspace-name frontend
146+
147+
socketcli --target-path /path/to/monorepo \
148+
--sub-path backend \
149+
--workspace-name backend
150+
```
151+
152+
These are two full-scan uploads, two server-side graphs, and two repository head/baseline sequences. In CI they can run as separate matrix jobs. See [GitHub Actions: scan changed monorepo workspaces independently](ci-cd.md#github-actions-scan-changed-monorepo-workspaces-independently).
153+
92154
**Generate GitLab Security Dashboard report:**
93155
```bash
94156
socketcli --enable-gitlab-security \
@@ -138,6 +200,7 @@ This will simultaneously generate:
138200

139201
- Both `--sub-path` and `--workspace-name` must be specified together
140202
- `--sub-path` can be used multiple times to include multiple directories
203+
- Repeated `--sub-path` values are combined into one scan; they do not create independent workspace scans
141204
- All specified sub-paths must exist within the target-path
142205

143206
## Usage
@@ -372,7 +435,7 @@ The launcher can be tuned via the `SOCKET_CLI_COANA_LAUNCHER` environment variab
372435
| `--strict-blocking` | False | False | Fail on ANY security policy violations (blocking severity), not just new ones. Only works in diff mode. See [Strict Blocking Mode](#strict-blocking-mode) for details. |
373436
| `--enable-diff` | False | False | Enable diff mode even when using `--integration api` (forces diff mode without SCM integration) |
374437
| `--scm` | False | api | Source control management type |
375-
| `--timeout` | False | | Timeout in seconds for API requests |
438+
| `--timeout` | False | 1200 | Timeout in seconds for each API request. This is not a total CLI runtime limit and does not limit local discovery, Git, or reachability analysis. |
376439
377440
#### Plugins
378441

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.7.2"
9+
version = "2.8.0"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.7.2'
2+
__version__ = '2.8.0'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

0 commit comments

Comments
 (0)