Skip to content

Commit 4b1ac09

Browse files
committed
Improve monorepo scan diagnostics
1 parent 5368c08 commit 4b1ac09

8 files changed

Lines changed: 520 additions & 18 deletions

File tree

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

76294
```yaml

docs/cli-reference.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,26 @@ 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`.
6472

6573
### Usage Examples
6674

67-
**Scan multiple frontend and backend workspaces:**
75+
**Scan several directories that belong to one logical application:**
6876
```bash
6977
socketcli --target-path /path/to/monorepo \
7078
--sub-path frontend \
@@ -89,6 +97,19 @@ This will:
8997
- Create a repository in Socket named like `my-repo-mobile-web`
9098
- Preserve git context (commits, branch info) from the repository root
9199

100+
**Create independent frontend and backend scans:**
101+
```bash
102+
socketcli --target-path /path/to/monorepo \
103+
--sub-path frontend \
104+
--workspace-name frontend
105+
106+
socketcli --target-path /path/to/monorepo \
107+
--sub-path backend \
108+
--workspace-name backend
109+
```
110+
111+
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).
112+
92113
**Generate GitLab Security Dashboard report:**
93114
```bash
94115
socketcli --enable-gitlab-security \
@@ -138,6 +159,7 @@ This will simultaneously generate:
138159

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

143165
## Usage
@@ -373,7 +395,7 @@ The launcher can be tuned via the `SOCKET_CLI_COANA_LAUNCHER` environment variab
373395
| `--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. |
374396
| `--enable-diff` | False | False | Enable diff mode even when using `--integration api` (forces diff mode without SCM integration) |
375397
| `--scm` | False | api | Source control management type |
376-
| `--timeout` | False | | Timeout in seconds for API requests |
398+
| `--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. |
377399
378400
#### Plugins
379401

0 commit comments

Comments
 (0)