Skip to content

Commit 7c1ec81

Browse files
committed
feat(ci): preserve pull request context in scan metadata
1 parent 950694b commit 7c1ec81

11 files changed

Lines changed: 673 additions & 32 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
@@ -306,14 +334,33 @@ initial timeout signal or 137 if `SIGKILL` is involved.
306334

307335
### Buildkite
308336

337+
This example assumes a GitHub-hosted repository. Change
338+
`SOCKET_SCM_INTEGRATION` to `gitlab` for a GitLab-hosted repository, or `api`
339+
when provider association is not wanted. The doubled dollar signs defer
340+
Buildkite variable expansion until the command runs on an agent.
341+
309342
```yaml
343+
env:
344+
SOCKET_SCM_INTEGRATION: "github"
345+
310346
steps:
311347
- label: "Socket scan"
312-
command: "socketcli --config .socketcli.toml --target-path ."
313-
env:
314-
SOCKET_SECURITY_API_TOKEN: "${SOCKET_SECURITY_API_TOKEN}"
348+
command: |
349+
socketcli \
350+
--config .socketcli.toml \
351+
--target-path . \
352+
--integration "$${SOCKET_SCM_INTEGRATION:-api}" \
353+
--pr-number "$${BUILDKITE_PULL_REQUEST:-0}"
354+
secrets:
355+
- SOCKET_SECURITY_API_TOKEN
315356
```
316357

358+
The `secrets` block expects a
359+
[Buildkite secret](https://buildkite.com/docs/pipelines/security/secrets/buildkite-secrets)
360+
named `SOCKET_SECURITY_API_TOKEN`. If your organization uses an external secrets
361+
plugin or an agent hook instead, remove that block and inject the same environment
362+
variable through your existing mechanism. Do not store the token in pipeline YAML.
363+
317364
The CLI reads Buildkite's native `BUILDKITE_COMMIT`, `BUILDKITE_BRANCH`,
318365
`BUILDKITE_PULL_REQUEST`, and `BUILDKITE_PULL_REQUEST_BASE_BRANCH` variables.
319366
For pull-request builds, ensure the checkout contains the base branch and the
@@ -385,6 +432,18 @@ socket_scan:
385432
SOCKET_SECURITY_API_TOKEN: $SOCKET_SECURITY_API_TOKEN
386433
```
387434

435+
### Azure Pipelines
436+
437+
```yaml
438+
- script: |
439+
socketcli \
440+
--integration azure \
441+
--enable-diff \
442+
--target-path "$(Build.SourcesDirectory)"
443+
env:
444+
SOCKET_SECURITY_API_TOKEN: $(SOCKET_SECURITY_API_TOKEN)
445+
```
446+
388447
### Bitbucket Pipelines
389448

390449
```yaml
@@ -395,6 +454,44 @@ pipelines:
395454
- socketcli --config .socketcli.toml --target-path .
396455
```
397456

457+
## Pull request and Dashboard association
458+
459+
The CLI sends the resolved pull request number with each full scan and attaches
460+
the pull request URL to diff scans so the Socket Dashboard can associate the
461+
report with its originating change. If `--pr-number` is supplied, it wins;
462+
passing `--pr-number 0` explicitly disables automatic association.
463+
464+
Without an explicit value, the CLI recognizes:
465+
466+
- GitHub Actions: `PR_NUMBER`, then the PR number in `GITHUB_REF`.
467+
- GitLab CI: `CI_MERGE_REQUEST_IID`.
468+
- Azure Pipelines: `SYSTEM_PULLREQUEST_PULLREQUESTNUMBER` for GitHub-hosted
469+
repositories, otherwise `SYSTEM_PULLREQUEST_PULLREQUESTID` for Azure Repos.
470+
471+
### Buildkite PR context
472+
473+
Buildkite is SCM-provider neutral, so the CLI does not infer a provider or consume
474+
its PR variable automatically. Pass Buildkite's
475+
[`BUILDKITE_PULL_REQUEST`](https://buildkite.com/docs/pipelines/configure/environment-variables#BUILDKITE_PULL_REQUEST)
476+
value to
477+
`--pr-number` and identify the repository host with `--integration`, as shown in
478+
the Buildkite platform example above. Buildkite sets `BUILDKITE_PULL_REQUEST` to
479+
`false` outside PR builds; the CLI treats that value as no PR.
480+
481+
Use `--integration github` for GitHub-hosted repositories and `--integration gitlab`
482+
for GitLab-hosted ones. In both cases the CLI reads the repository slug and host from
483+
[`BUILDKITE_REPO`](https://buildkite.com/docs/pipelines/configure/environment-variables#BUILDKITE_REPO)
484+
to build the pull request or merge request link, so github.com, GitLab.com, and
485+
self-hosted installations all work without extra configuration. Setting
486+
`CI_PROJECT_URL` still overrides the derived GitLab project URL. Keep `--scm api`
487+
unless you also intend to configure an existing GitHub or GitLab comment adapter and
488+
its provider token.
489+
490+
`--scm github` and `--scm gitlab` also imply the matching scan integration for
491+
Dashboard metadata unless `--integration` was explicitly supplied. PR comments
492+
remain limited to the existing GitHub and GitLab SCM adapters; Azure receives
493+
console output and Dashboard association but does not post a PR comment.
494+
398495
## Workflow templates
399496

400497
Prebuilt examples in this repo:
@@ -411,3 +508,11 @@ Prebuilt examples in this repo:
411508
- `--sarif-grouping alert` currently applies to `--sarif-scope full`.
412509
- Diff-based SARIF can validly be empty when there are no matching net-new alerts.
413510
- Keep API tokens in secret stores (`SOCKET_SECURITY_API_TOKEN`), not in config files.
511+
- In Buildkite pipeline YAML, follow its
512+
[runtime interpolation](https://buildkite.com/docs/pipelines/configure/environment-variables#runtime-variable-interpolation)
513+
guidance and use `$$` for variables that must expand when the command runs rather
514+
than when the pipeline is uploaded.
515+
- Security findings with `props.firstPatchedVersionIdentifier` show that value in
516+
the console table, including native Buildkite job logs, and in GitHub/GitLab
517+
security comments when that SCM adapter is configured. Findings without a known
518+
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
@@ -238,7 +238,7 @@ If you don't want to provide the Socket API Token every time then you can use th
238238
| `--repo` | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) |
239239
| `--workspace` | False | | The Socket workspace to associate the scan with (e.g. `my-org` in `my-org/my-repo`). See note below. |
240240
| `--repo-is-public` | False | False | If set, flags a new repository creation as public. Defaults to false. |
241-
| `--integration` | False | api | Integration type (api, github, gitlab, azure, bitbucket) |
241+
| `--integration` | False | api | Integration type (api, github, gitlab, azure, bitbucket). When omitted, `--scm github` or `--scm gitlab` implies the matching integration. |
242242
| `--owner` | False | | Name of the integration owner, defaults to the socket organization slug |
243243
| `--branch` | False | *auto* | Branch name (auto-detected from git) |
244244
| `--committers` | False | *auto* | Committer(s) to filter by (auto-detected from git commit) |
@@ -252,7 +252,7 @@ If you don't want to provide the Socket API Token every time then you can use th
252252
#### Pull Request and Commit
253253
| Parameter | Required | Default | Description |
254254
|:-----------------|:---------|:--------|:-----------------------------------------------|
255-
| `--pr-number` | False | "0" | Pull request number |
255+
| `--pr-number` | False | *auto* | Pull request number. Auto-detected in GitHub Actions, GitLab CI, and Azure Pipelines; explicitly passing `0` disables detection. |
256256
| `--commit-message` | False | *auto* | Commit message (auto-detected from git) |
257257
| `--commit-sha` | False | *auto* | Commit SHA (auto-detected from git) |
258258
| `--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: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class CliConfig:
115115
branch: str = ""
116116
committers: Optional[List[str]] = None
117117
pr_number: str = "0"
118+
pr_number_explicit: bool = False
118119
commit_message: Optional[str] = None
119120
default_branch: bool = False
120121
target_path: str = "./"
@@ -208,17 +209,28 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
208209
pre_parser.add_argument("--config", dest="config_file", default=None)
209210
pre_args, _ = pre_parser.parse_known_args(args_list)
210211

212+
normalized_defaults = {}
211213
if pre_args.config_file:
212214
config_defaults = load_cli_config_file(pre_args.config_file)
213215
valid_dests = {action.dest for action in parser._actions if action.dest != "help"}
214-
normalized_defaults = {}
215216
for key, value in config_defaults.items():
216217
dest = str(key).replace("-", "_")
217218
if dest in valid_dests:
218219
normalized_defaults[dest] = value
219220
parser.set_defaults(**normalized_defaults)
220221

221222
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
222234

223235
if args.reach_exclude_paths:
224236
logging.warning(
@@ -262,7 +274,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
262274
'repo': args.repo,
263275
'branch': args.branch,
264276
'committers': args.committers,
265-
'pr_number': args.pr_number,
277+
'pr_number': pr_number,
278+
'pr_number_explicit': pr_number_explicit,
266279
'commit_message': commit_message,
267280
'default_branch': args.default_branch,
268281
'target_path': os.path.expanduser(args.target_path),
@@ -294,7 +307,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
294307
'disable_ignore': args.disable_ignore,
295308
'upload_logs': args.upload_logs,
296309
'strict_blocking': args.strict_blocking,
297-
'integration_type': args.integration,
310+
'integration_type': integration_type,
298311
'pending_head': args.pending_head,
299312
'timeout': args.timeout,
300313
'exit_code_on_api_error': args.exit_code_on_api_error,
@@ -519,8 +532,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
519532
"--integration",
520533
choices=INTEGRATION_TYPES,
521534
metavar="<type>",
522-
help="Integration type of api, github, gitlab, azure, or bitbucket. Defaults to api",
523-
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
524541
)
525542
integration_group.add_argument(
526543
"--owner",
@@ -535,13 +552,17 @@ def create_argument_parser() -> argparse.ArgumentParser:
535552
"--pr-number",
536553
dest="pr_number",
537554
metavar="<number>",
538-
help="Pull request number",
539-
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
540560
)
541561
pr_group.add_argument(
542562
"--pr_number",
543563
dest="pr_number",
544-
help=argparse.SUPPRESS
564+
help=argparse.SUPPRESS,
565+
default=argparse.SUPPRESS
545566
)
546567
pr_group.add_argument(
547568
"--commit-message",

‎socketsecurity/core/__init__.py‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,7 +1640,8 @@ def get_license_text_via_purl(self, packages: dict[str, Package], batch_size: in
16401640
def get_diff_scan_artifacts(
16411641
self,
16421642
head_full_scan_id: str,
1643-
new_full_scan_id: str
1643+
new_full_scan_id: str,
1644+
external_href: Optional[str] = None
16441645
) -> DiffArtifacts:
16451646
"""Compare two full scans via the diff-scans endpoints, polling for the result.
16461647
@@ -1663,6 +1664,8 @@ def get_diff_scan_artifacts(
16631664
Args:
16641665
head_full_scan_id: The before/base full scan ID
16651666
new_full_scan_id: The after/head full scan ID
1667+
external_href: Optional pull request or merge request URL to associate
1668+
with the diff scan in the Socket Dashboard
16661669
16671670
Returns:
16681671
DiffArtifacts with the added/removed/unchanged/replaced/updated lists
@@ -1672,6 +1675,8 @@ def get_diff_scan_artifacts(
16721675
"after": new_full_scan_id,
16731676
"description": f"Socket Security CLI v{__version__} scan comparison",
16741677
}
1678+
if external_href:
1679+
create_params["external_href"] = external_href
16751680
try:
16761681
result = self.sdk.diffscans.create_from_ids(self.config.org_slug, create_params)
16771682
diff_scan = result.get("diff_scan") or {}
@@ -1820,7 +1825,8 @@ def get_added_and_removed_packages(
18201825
self,
18211826
head_full_scan_id: str,
18221827
new_full_scan_id: str,
1823-
include_license_details: bool = False
1828+
include_license_details: bool = False,
1829+
external_href: Optional[str] = None
18241830
) -> Tuple[Dict[str, Package], Dict[str, Package], Dict[str, Package]]:
18251831
"""
18261832
Get packages that were added and removed between scans.
@@ -1853,6 +1859,8 @@ def get_added_and_removed_packages(
18531859
is retained as an explicit override seam, not wired to the
18541860
``--exclude-license-details`` user flag (which still governs the
18551861
human-facing dashboard report URL).
1862+
external_href: Optional pull request or merge request URL to associate
1863+
with the primary diff-scan resource
18561864
18571865
Returns:
18581866
Tuple of (added_packages, removed_packages) dictionaries
@@ -1864,7 +1872,8 @@ def get_added_and_removed_packages(
18641872
try:
18651873
diff_artifacts = self.get_diff_scan_artifacts(
18661874
head_full_scan_id,
1867-
new_full_scan_id
1875+
new_full_scan_id,
1876+
external_href=external_href,
18681877
)
18691878
except Exception as error:
18701879
# SDK error messages can span many lines (path + response headers); the
@@ -1980,7 +1989,8 @@ def create_new_diff(
19801989
save_files_list_path: Optional[str] = None,
19811990
save_manifest_tar_path: Optional[str] = None,
19821991
base_paths: Optional[List[str]] = None,
1983-
explicit_files: Optional[List[str]] = None
1992+
explicit_files: Optional[List[str]] = None,
1993+
external_href: Optional[str] = None
19841994
) -> Diff:
19851995
"""Create a new diff using the Socket SDK.
19861996
@@ -1992,6 +2002,8 @@ def create_new_diff(
19922002
save_manifest_tar_path: Optional path to save manifest files tar.gz archive
19932003
base_paths: List of base paths for the scan (optional)
19942004
explicit_files: Optional list of explicit files to use instead of discovering files
2005+
external_href: Optional pull request or merge request URL to associate
2006+
with the diff scan
19952007
"""
19962008
log.debug(f"starting create_new_diff with no_change: {no_change}")
19972009
if no_change:
@@ -2126,7 +2138,8 @@ def create_new_diff(
21262138
) = self.get_added_and_removed_packages(
21272139
head_full_scan_id,
21282140
new_full_scan.id,
2129-
include_license_details=False
2141+
include_license_details=False,
2142+
external_href=external_href,
21302143
)
21312144

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

0 commit comments

Comments
 (0)