Skip to content

Commit 20afb9f

Browse files
committed
feat(ci): preserve pull request context in scan metadata
1 parent 0c4524e commit 20afb9f

11 files changed

Lines changed: 682 additions & 37 deletions

File tree

docs/ci-cd.md

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Use this guide for pipeline-focused CLI usage across platforms.
44

5+
The shell commands in the recommended patterns are CI-provider neutral. Buildkite
6+
pipeline equivalents and provider-specific considerations are called out alongside
7+
the relevant guidance below.
8+
59
## Recommended patterns
610

711
### Dashboard-style reachable SARIF
@@ -27,6 +31,27 @@ socketcli \
2731
--strict-blocking
2832
```
2933

34+
### Buildkite: retain SARIF as a build artifact
35+
36+
Either recommended pattern can run directly in a Buildkite command step. When the
37+
scan writes SARIF, add
38+
[`artifact_paths`](https://buildkite.com/docs/pipelines/configure/artifacts#upload-artifacts-with-a-command-step)
39+
so developers can download the report from the build after the command finishes:
40+
41+
```yaml
42+
steps:
43+
- label: ":socket: Socket reachable diff"
44+
command: |
45+
socketcli \
46+
--reach \
47+
--sarif-file results.sarif \
48+
--sarif-scope diff \
49+
--sarif-reachability reachable \
50+
--strict-blocking
51+
artifact_paths:
52+
- "results.sarif"
53+
```
54+
3055
## Config file usage in CI
3156
3257
Use `--config .socketcli.toml` or `--config .socketcli.json` to keep pipeline commands small.
@@ -60,6 +85,9 @@ Equivalent JSON:
6085
}
6186
```
6287

88+
The Buildkite examples below use the same checked-in `.socketcli.toml` file; no
89+
Buildkite-specific config-file format is required.
90+
6391
## Platform examples
6492

6593
### GitHub Actions
@@ -73,14 +101,33 @@ Equivalent JSON:
73101

74102
### Buildkite
75103

104+
This example assumes a GitHub-hosted repository. Change
105+
`SOCKET_SCM_INTEGRATION` to `gitlab` for a GitLab-hosted repository, or `api`
106+
when provider association is not wanted. The doubled dollar signs defer
107+
Buildkite variable expansion until the command runs on an agent.
108+
76109
```yaml
110+
env:
111+
SOCKET_SCM_INTEGRATION: "github"
112+
77113
steps:
78114
- label: "Socket scan"
79-
command: "socketcli --config .socketcli.toml --target-path ."
80-
env:
81-
SOCKET_SECURITY_API_TOKEN: "${SOCKET_SECURITY_API_TOKEN}"
115+
command: |
116+
socketcli \
117+
--config .socketcli.toml \
118+
--target-path . \
119+
--integration "$${SOCKET_SCM_INTEGRATION:-api}" \
120+
--pr-number "$${BUILDKITE_PULL_REQUEST:-0}"
121+
secrets:
122+
- SOCKET_SECURITY_API_TOKEN
82123
```
83124

125+
The `secrets` block expects a
126+
[Buildkite secret](https://buildkite.com/docs/pipelines/security/secrets/buildkite-secrets)
127+
named `SOCKET_SECURITY_API_TOKEN`. If your organization uses an external secrets
128+
plugin or an agent hook instead, remove that block and inject the same environment
129+
variable through your existing mechanism. Do not store the token in pipeline YAML.
130+
84131
#### Merge-base baselines in Buildkite (dynamic pipelines)
85132

86133
Notes for using `--base-commit-sha` (see the
@@ -139,6 +186,18 @@ socket_scan:
139186
SOCKET_SECURITY_API_TOKEN: $SOCKET_SECURITY_API_TOKEN
140187
```
141188

189+
### Azure Pipelines
190+
191+
```yaml
192+
- script: |
193+
socketcli \
194+
--integration azure \
195+
--enable-diff \
196+
--target-path "$(Build.SourcesDirectory)"
197+
env:
198+
SOCKET_SECURITY_API_TOKEN: $(SOCKET_SECURITY_API_TOKEN)
199+
```
200+
142201
### Bitbucket Pipelines
143202

144203
```yaml
@@ -149,6 +208,44 @@ pipelines:
149208
- socketcli --config .socketcli.toml --target-path .
150209
```
151210

211+
## Pull request and Dashboard association
212+
213+
The CLI sends the resolved pull request number with each full scan and attaches
214+
the pull request URL to diff scans so the Socket Dashboard can associate the
215+
report with its originating change. If `--pr-number` is supplied, it wins;
216+
passing `--pr-number 0` explicitly disables automatic association.
217+
218+
Without an explicit value, the CLI recognizes:
219+
220+
- GitHub Actions: `PR_NUMBER`, then the PR number in `GITHUB_REF`.
221+
- GitLab CI: `CI_MERGE_REQUEST_IID`.
222+
- Azure Pipelines: `SYSTEM_PULLREQUEST_PULLREQUESTNUMBER` for GitHub-hosted
223+
repositories, otherwise `SYSTEM_PULLREQUEST_PULLREQUESTID` for Azure Repos.
224+
225+
### Buildkite PR context
226+
227+
Buildkite is SCM-provider neutral, so the CLI does not infer a provider or consume
228+
its PR variable automatically. Pass Buildkite's
229+
[`BUILDKITE_PULL_REQUEST`](https://buildkite.com/docs/pipelines/configure/environment-variables#BUILDKITE_PULL_REQUEST)
230+
value to
231+
`--pr-number` and identify the repository host with `--integration`, as shown in
232+
the Buildkite platform example above. Buildkite sets `BUILDKITE_PULL_REQUEST` to
233+
`false` outside PR builds; the CLI treats that value as no PR.
234+
235+
Use `--integration github` for GitHub-hosted repositories and `--integration gitlab`
236+
for GitLab-hosted ones. In both cases the CLI reads the repository slug and host from
237+
[`BUILDKITE_REPO`](https://buildkite.com/docs/pipelines/configure/environment-variables#BUILDKITE_REPO)
238+
to build the pull request or merge request link, so github.com, GitLab.com, and
239+
self-hosted installations all work without extra configuration. Setting
240+
`CI_PROJECT_URL` still overrides the derived GitLab project URL. Keep `--scm api`
241+
unless you also intend to configure an existing GitHub or GitLab comment adapter and
242+
its provider token.
243+
244+
`--scm github` and `--scm gitlab` also imply the matching scan integration for
245+
Dashboard metadata unless `--integration` was explicitly supplied. PR comments
246+
remain limited to the existing GitHub and GitLab SCM adapters; Azure receives
247+
console output and Dashboard association but does not post a PR comment.
248+
152249
## Workflow templates
153250

154251
Prebuilt examples in this repo:
@@ -165,3 +262,11 @@ Prebuilt examples in this repo:
165262
- `--sarif-grouping alert` currently applies to `--sarif-scope full`.
166263
- Diff-based SARIF can validly be empty when there are no matching net-new alerts.
167264
- Keep API tokens in secret stores (`SOCKET_SECURITY_API_TOKEN`), not in config files.
265+
- In Buildkite pipeline YAML, follow its
266+
[runtime interpolation](https://buildkite.com/docs/pipelines/configure/environment-variables#runtime-variable-interpolation)
267+
guidance and use `$$` for variables that must expand when the command runs rather
268+
than when the pipeline is uploaded.
269+
- Security findings with `props.firstPatchedVersionIdentifier` show that value in
270+
the console table, including native Buildkite job logs, and in GitHub/GitLab
271+
security comments when that SCM adapter is configured. Findings without a known
272+
patched release leave the console cell blank and omit the comment field.

docs/cli-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ If you don't want to provide the Socket API Token every time then you can use th
175175
| `--repo` | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) |
176176
| `--workspace` | False | | The Socket workspace to associate the scan with (e.g. `my-org` in `my-org/my-repo`). See note below. |
177177
| `--repo-is-public` | False | False | If set, flags a new repository creation as public. Defaults to false. |
178-
| `--integration` | False | api | Integration type (api, github, gitlab, azure, bitbucket) |
178+
| `--integration` | False | api | Integration type (api, github, gitlab, azure, bitbucket). When omitted, `--scm github` or `--scm gitlab` implies the matching integration. |
179179
| `--owner` | False | | Name of the integration owner, defaults to the socket organization slug |
180180
| `--branch` | False | *auto* | Branch name (auto-detected from git) |
181181
| `--committers` | False | *auto* | Committer(s) to filter by (auto-detected from git commit) |
@@ -189,7 +189,7 @@ If you don't want to provide the Socket API Token every time then you can use th
189189
#### Pull Request and Commit
190190
| Parameter | Required | Default | Description |
191191
|:-----------------|:---------|:--------|:-----------------------------------------------|
192-
| `--pr-number` | False | "0" | Pull request number |
192+
| `--pr-number` | False | *auto* | Pull request number. Auto-detected in GitHub Actions, GitLab CI, and Azure Pipelines; explicitly passing `0` disables detection. |
193193
| `--commit-message` | False | *auto* | Commit message (auto-detected from git) |
194194
| `--commit-sha` | False | *auto* | Commit SHA (auto-detected from git) |
195195
| `--base-scan-id` | False | | Full scan ID to diff against, overriding the repository's head scan as the baseline. Mutually exclusive with `--base-commit-sha` |

socketsecurity/config.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import argparse
2+
import json
23
import logging
34
import os
5+
import tomllib
46
from dataclasses import asdict, dataclass, field
57
from typing import List, Optional
6-
from socketsecurity import __version__
8+
79
from socketdev import INTEGRATION_TYPES, IntegrationType
8-
import json
9-
import tomllib
10+
11+
from socketsecurity import __version__
1012

1113

1214
def get_plugin_config_from_env(prefix: str) -> dict:
@@ -113,6 +115,7 @@ class CliConfig:
113115
branch: str = ""
114116
committers: Optional[List[str]] = None
115117
pr_number: str = "0"
118+
pr_number_explicit: bool = False
116119
commit_message: Optional[str] = None
117120
default_branch: bool = False
118121
target_path: str = "./"
@@ -206,17 +209,28 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
206209
pre_parser.add_argument("--config", dest="config_file", default=None)
207210
pre_args, _ = pre_parser.parse_known_args(args_list)
208211

212+
normalized_defaults = {}
209213
if pre_args.config_file:
210214
config_defaults = load_cli_config_file(pre_args.config_file)
211215
valid_dests = {action.dest for action in parser._actions if action.dest != "help"}
212-
normalized_defaults = {}
213216
for key, value in config_defaults.items():
214217
dest = str(key).replace("-", "_")
215218
if dest in valid_dests:
216219
normalized_defaults[dest] = value
217220
parser.set_defaults(**normalized_defaults)
218221

219222
args = parser.parse_args(args_list)
223+
integration_explicit = hasattr(args, "integration")
224+
pr_number_explicit = hasattr(args, "pr_number")
225+
226+
integration_type = getattr(args, "integration", "api")
227+
pr_number = getattr(args, "pr_number", "0")
228+
if (
229+
not integration_explicit and
230+
integration_type == "api" and
231+
args.scm in ("github", "gitlab")
232+
):
233+
integration_type = args.scm
220234

221235
if args.reach_exclude_paths:
222236
logging.warning(
@@ -260,7 +274,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
260274
'repo': args.repo,
261275
'branch': args.branch,
262276
'committers': args.committers,
263-
'pr_number': args.pr_number,
277+
'pr_number': pr_number,
278+
'pr_number_explicit': pr_number_explicit,
264279
'commit_message': commit_message,
265280
'default_branch': args.default_branch,
266281
'target_path': os.path.expanduser(args.target_path),
@@ -292,7 +307,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
292307
'disable_ignore': args.disable_ignore,
293308
'upload_logs': args.upload_logs,
294309
'strict_blocking': args.strict_blocking,
295-
'integration_type': args.integration,
310+
'integration_type': integration_type,
296311
'pending_head': args.pending_head,
297312
'timeout': args.timeout,
298313
'exit_code_on_api_error': args.exit_code_on_api_error,
@@ -517,8 +532,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
517532
"--integration",
518533
choices=INTEGRATION_TYPES,
519534
metavar="<type>",
520-
help="Integration type of api, github, gitlab, azure, or bitbucket. Defaults to api",
521-
default="api"
535+
help=(
536+
"Integration type of api, github, gitlab, azure, or bitbucket. "
537+
"Defaults to api; --scm github/gitlab implies the matching integration "
538+
"when this option is omitted"
539+
),
540+
default=argparse.SUPPRESS
522541
)
523542
integration_group.add_argument(
524543
"--owner",
@@ -533,13 +552,17 @@ def create_argument_parser() -> argparse.ArgumentParser:
533552
"--pr-number",
534553
dest="pr_number",
535554
metavar="<number>",
536-
help="Pull request number",
537-
default="0"
555+
help=(
556+
"Pull request number. Auto-detected in supported CI environments when omitted; "
557+
"pass 0 explicitly to disable detection"
558+
),
559+
default=argparse.SUPPRESS
538560
)
539561
pr_group.add_argument(
540562
"--pr_number",
541563
dest="pr_number",
542-
help=argparse.SUPPRESS
564+
help=argparse.SUPPRESS,
565+
default=argparse.SUPPRESS
543566
)
544567
pr_group.add_argument(
545568
"--commit-message",

socketsecurity/core/__init__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,7 +1329,8 @@ def get_license_text_via_purl(self, packages: dict[str, Package], batch_size: in
13291329
def get_diff_scan_artifacts(
13301330
self,
13311331
head_full_scan_id: str,
1332-
new_full_scan_id: str
1332+
new_full_scan_id: str,
1333+
external_href: Optional[str] = None
13331334
) -> DiffArtifacts:
13341335
"""Compare two full scans via the diff-scans endpoints, polling for the result.
13351336
@@ -1352,6 +1353,8 @@ def get_diff_scan_artifacts(
13521353
Args:
13531354
head_full_scan_id: The before/base full scan ID
13541355
new_full_scan_id: The after/head full scan ID
1356+
external_href: Optional pull request or merge request URL to associate
1357+
with the diff scan in the Socket Dashboard
13551358
13561359
Returns:
13571360
DiffArtifacts with the added/removed/unchanged/replaced/updated lists
@@ -1361,6 +1364,8 @@ def get_diff_scan_artifacts(
13611364
"after": new_full_scan_id,
13621365
"description": f"Socket Security CLI v{__version__} scan comparison",
13631366
}
1367+
if external_href:
1368+
create_params["external_href"] = external_href
13641369
try:
13651370
result = self.sdk.diffscans.create_from_ids(self.config.org_slug, create_params)
13661371
diff_scan = result.get("diff_scan") or {}
@@ -1444,7 +1449,8 @@ def get_added_and_removed_packages(
14441449
self,
14451450
head_full_scan_id: str,
14461451
new_full_scan_id: str,
1447-
include_license_details: bool = False
1452+
include_license_details: bool = False,
1453+
external_href: Optional[str] = None
14481454
) -> Tuple[Dict[str, Package], Dict[str, Package], Dict[str, Package]]:
14491455
"""
14501456
Get packages that were added and removed between scans.
@@ -1477,6 +1483,8 @@ def get_added_and_removed_packages(
14771483
is retained as an explicit override seam, not wired to the
14781484
``--exclude-license-details`` user flag (which still governs the
14791485
human-facing dashboard report URL).
1486+
external_href: Optional pull request or merge request URL to associate
1487+
with the primary diff-scan resource
14801488
14811489
Returns:
14821490
Tuple of (added_packages, removed_packages) dictionaries
@@ -1488,7 +1496,8 @@ def get_added_and_removed_packages(
14881496
try:
14891497
diff_artifacts = self.get_diff_scan_artifacts(
14901498
head_full_scan_id,
1491-
new_full_scan_id
1499+
new_full_scan_id,
1500+
external_href=external_href,
14921501
)
14931502
except Exception as error:
14941503
# SDK error messages can span many lines (path + response headers); the
@@ -1592,7 +1601,8 @@ def create_new_diff(
15921601
save_files_list_path: Optional[str] = None,
15931602
save_manifest_tar_path: Optional[str] = None,
15941603
base_paths: Optional[List[str]] = None,
1595-
explicit_files: Optional[List[str]] = None
1604+
explicit_files: Optional[List[str]] = None,
1605+
external_href: Optional[str] = None
15961606
) -> Diff:
15971607
"""Create a new diff using the Socket SDK.
15981608
@@ -1604,6 +1614,8 @@ def create_new_diff(
16041614
save_manifest_tar_path: Optional path to save manifest files tar.gz archive
16051615
base_paths: List of base paths for the scan (optional)
16061616
explicit_files: Optional list of explicit files to use instead of discovering files
1617+
external_href: Optional pull request or merge request URL to associate
1618+
with the diff scan
16071619
"""
16081620
log.debug(f"starting create_new_diff with no_change: {no_change}")
16091621
if no_change:
@@ -1728,7 +1740,8 @@ def create_new_diff(
17281740
) = self.get_added_and_removed_packages(
17291741
head_full_scan_id,
17301742
new_full_scan.id,
1731-
include_license_details=False
1743+
include_license_details=False,
1744+
external_href=external_href,
17321745
)
17331746

17341747
# Separate unchanged packages from added/removed for --strict-blocking support

0 commit comments

Comments
 (0)