Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 2.11.12

### Changed: `--repo` takes a repository slug

- `--repo` takes the repository slug without its owner (`my-repo`, not
`my-org/my-repo`). A value containing `/` now exits `1` and names the slug to
use. Pass the owner with `--workspace`.

### Fixed: manifest links in pull request comments

- Manifest links in GitHub, GitLab and Bitbucket comments now include the
repository owner. When the CLI can't determine the owner, they link to the
Socket report instead.

### Deprecated: `--pending-head`

- `--pending-head` has no effect and logs a deprecation warning. Use
`--default-branch`.

## 2.11.11

### Changed: use lowercase Python CLI User-Agent
Expand Down
19 changes: 9 additions & 10 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ These are two full-scan uploads, two server-side graphs, and two repository head
**Generate GitLab Security Dashboard report:**
```bash
socketcli --enable-gitlab-security \
--repo owner/repo \
--repo my-repo \
--target-path .
```

Expand All @@ -167,7 +167,7 @@ This will:
**Save SARIF report to file (e.g. for GitHub Code Scanning, SonarQube, or VS Code):**
```bash
socketcli --sarif-file results.sarif \
--repo owner/repo \
--repo my-repo \
--target-path .
```

Expand All @@ -176,7 +176,7 @@ socketcli --sarif-file results.sarif \
socketcli --enable-json \
--sarif-file results.sarif \
--enable-gitlab-security \
--repo owner/repo
--repo my-repo
```

This will simultaneously generate:
Expand Down Expand Up @@ -212,7 +212,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--
[--base-scan-id BASE_SCAN_ID | --base-commit-sha BASE_COMMIT_SHA]
[--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--license-file-name LICENSE_FILE_NAME] [--save-submitted-files-list SAVE_SUBMITTED_FILES_LIST]
[--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME]
[--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--exclude-paths EXCLUDE_PATHS] [--include-dirs INCLUDE_DIRS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug]
[--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--exclude-paths EXCLUDE_PATHS] [--include-dirs INCLUDE_DIRS] [--default-branch] [--generate-license] [--enable-debug]
[--enable-json] [--enable-sarif] [--sarif-file <path>] [--sarif-scope {diff,full}] [--sarif-grouping {instance,alert}] [--sarif-reachability {all,reachable,potentially,reachable-or-potentially}] [--enable-gitlab-security] [--gitlab-security-file <path>]
[--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue]
[--ignore-commit-files] [--disable-blocking] [--disable-ignore] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders]
Expand All @@ -235,7 +235,7 @@ If you don't want to provide the Socket API Token every time then you can use th
#### Repository
| Parameter | Required | Default | Description |
|:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------|
| `--repo` | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) |
| `--repo` | False | *auto* | Repository slug in Socket, without the owner: `my-repo`, not `my-org/my-repo`. Auto-detected from the git remote. A value containing `/` is rejected; use `--workspace` for the owner. |
| `--workspace` | False | | The Socket workspace to associate the scan with (e.g. `my-org` in `my-org/my-repo`). See note below. |
| `--repo-is-public` | False | False | If set, flags a new repository creation as public. Defaults to false. |
| `--integration` | False | api | Integration type (api, github, gitlab, azure, bitbucket). When omitted, `--scm github` or `--scm gitlab` implies the matching integration. |
Expand Down Expand Up @@ -308,7 +308,6 @@ If you don't want to provide the Socket API Token every time then you can use th
| Parameter | Required | Default | Description |
|:-------------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------|
| `--default-branch` | False | *auto* | Make this branch the default branch (auto-detected from git and CI environment when not specified) |
| `--pending-head` | False | *auto* | If true, the new scan will be set as the branch's head scan (automatically synced with default-branch) |
| `--include-module-folders` | False | False | If enabled, re-includes the JS/TS module folders (`node_modules`, `bower_components`, `jspm_packages`) in manifest discovery. For other excluded directories, use `--include-dirs`. |

#### Output Configuration
Expand Down Expand Up @@ -551,7 +550,7 @@ export SOCKET_SLACK_BOT_TOKEN="xoxb-your-bot-token-here"

Webhook mode (`url_configs`):
- `reachability_alerts_only` (boolean, default: false): When `--reach` is enabled, only send reachable vulnerabilities from the selected diff alert set (uses reachability facts when available; otherwise falls back to blocking-status behavior)
- `repos` (array): Only send alerts for specific repositories (e.g., `["owner/repo1", "owner/repo2"]`)
- `repos` (array): Only send alerts for specific repositories, matched against the `--repo` slug without the owner (e.g., `["my-repo1", "my-repo2"]`)
- `alert_types` (array): Only send specific alert types (e.g., `["malware", "typosquat"]`)
- `severities` (array): Only send alerts with specific severities (e.g., `["high", "critical"]`)

Expand Down Expand Up @@ -683,7 +682,7 @@ The CLI uses intelligent default branch detection with the following priority:
3. **Git repository analysis**: Compares current branch with repository's default branch
4. **Fallback**: Defaults to `false` if none of the above methods succeed

Both `--default-branch` and `--pending-head` parameters are automatically synchronized to ensure consistent behavior.
A scan on the default branch also becomes that branch's head scan. `--pending-head` is deprecated and has no effect: it is still accepted so existing pipelines keep working, but it logs a warning. Use `--default-branch` instead.

## Who can ignore an alert

Expand Down Expand Up @@ -763,7 +762,7 @@ Socket CLI can generate reports compatible with GitLab's Security Dashboard, all
To generate a GitLab-compatible security report:

```bash
socketcli --enable-gitlab-security --repo owner/repo
socketcli --enable-gitlab-security --repo my-repo
```

This creates a `gl-dependency-scanning-report.json` file following GitLab's Dependency Scanning report schema.
Expand All @@ -782,7 +781,7 @@ socket_security_scan:
script:
- socketcli
--api-token $SOCKET_API_TOKEN
--repo $CI_PROJECT_PATH
--repo $CI_PROJECT_NAME
--branch $CI_COMMIT_REF_NAME
--commit-sha $CI_COMMIT_SHA
--enable-gitlab-security
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "socketsecurity"
version = "2.11.11"
version = "2.11.12"
requires-python = ">= 3.11"
license = {"file" = "LICENSE"}
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__author__ = 'socket.dev'
__version__ = '2.11.11'
__version__ = '2.11.12'
USER_AGENT_NAME = 'socket-python-cli'
USER_AGENT = f'{USER_AGENT_NAME}/{__version__}'
28 changes: 25 additions & 3 deletions socketsecurity/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ class CliConfig:
sub_paths: List[str] = field(default_factory=list)
workspace_name: Optional[str] = None
workspace: Optional[str] = None
# Web URL of the scanned repository, resolved at runtime; see resolve_repository_url.
repository_url: Optional[str] = None
# Reachability Flags
reach: bool = False
reach_version: Optional[str] = None
Expand Down Expand Up @@ -448,6 +450,22 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
logging.error("--workspace-name requires --sub-path to be specified")
exit(1)

# The API validates the repo as a slug, which can't contain "/", so an
# owner/repo value would otherwise fail later at repository creation.
if args.repo and "/" in args.repo:
suggested = args.repo.rstrip("/").rsplit("/", 1)[-1]
logging.error(
f"--repo takes the repository slug without its owner, but got '{args.repo}'. "
f"Use --repo {suggested}, and --workspace for the owner if your repositories use one."
)
exit(1)

if args.pending_head:
logging.warning(
"--pending-head is deprecated and has no effect. A scan becomes the head scan "
"when it runs on the default branch; use --default-branch to mark it as such."
)

# argparse only enforces the mutually exclusive group for real CLI args;
# this also catches both values arriving via a --config file.
if args.base_scan_id and args.base_commit_sha:
Expand Down Expand Up @@ -527,8 +545,11 @@ def create_argument_parser() -> argparse.ArgumentParser:
repo_group = parser.add_argument_group('Repository')
repo_group.add_argument(
"--repo",
metavar="<owner/repo>",
help="Repository name in owner/repo format",
metavar="<repo>",
help=(
"Repository slug in Socket, without the owner (e.g. my-repo, not my-org/my-repo). "
"Defaults to the git remote's repository name. Use --workspace for the owner."
),
required=False
)
repo_group.add_argument(
Expand Down Expand Up @@ -743,11 +764,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
action="store_true",
help=argparse.SUPPRESS
)
# Deprecated no-op, still accepted so existing pipelines don't fail to parse.
config_group.add_argument(
"--pending-head",
dest="pending_head",
action="store_true",
help="If true, the new scan will be set as the branch's head scan"
help=argparse.SUPPRESS
)
config_group.add_argument(
"--ignore-authorization",
Expand Down
2 changes: 2 additions & 0 deletions socketsecurity/core/git_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ def __init__(self, path: str, base_commit_sha: str | None = None):
log.debug(f"Commit committer: {self.commit.committer.name} <{self.commit.committer.email}>")

# Extract repository name from git remote, with fallback to default
self.remote_url: str | None = None
try:
remote_url = self.repo.remotes.origin.url
self.remote_url = remote_url
self.repo_name = remote_url.split('.git')[0].split('/')[-1]
log.debug(f"Repository name detected from git remote: {self.repo_name}")
except Exception as error:
Expand Down
31 changes: 6 additions & 25 deletions socketsecurity/core/messages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
import logging
import os
import re
import uuid
from datetime import datetime, timezone
Expand Down Expand Up @@ -94,30 +93,12 @@ def get_manifest_file_url(diff: Diff, manifest_path: str, config=None) -> str:

# Generate URL based on SCM type using config information
# NEVER use diff.diff_url for SCM URLs - those are Socket URLs for "View report" links
if scm_type == "github":
if config and hasattr(config, 'repo') and config.repo:
# Get branch from config, default to main
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
# Construct GitHub URL from repo info (could be github.com or GitHub Enterprise)
github_server = os.getenv('GITHUB_SERVER_URL', 'https://github.com')
return f"{github_server}/{config.repo}/blob/{branch}/{clean_path}"

elif scm_type == "gitlab":
if config and hasattr(config, 'repo') and config.repo:
# Get branch from config, default to main
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
# Construct GitLab URL from repo info (could be gitlab.com or self-hosted GitLab)
gitlab_server = os.getenv('CI_SERVER_URL', 'https://gitlab.com')
return f"{gitlab_server}/{config.repo}/-/blob/{branch}/{clean_path}"

elif scm_type == "bitbucket":
if config and hasattr(config, 'repo') and config.repo:
# Get branch from config, default to main
branch = getattr(config, 'branch', 'main') if hasattr(config, 'branch') and config.branch else 'main'
# Construct Bitbucket URL from repo info (could be bitbucket.org or Bitbucket Server)
bitbucket_server = os.getenv('BITBUCKET_SERVER_URL', 'https://bitbucket.org')
return f"{bitbucket_server}/{config.repo}/src/{branch}/{clean_path}"

repository_url = getattr(config, 'repository_url', None) if config else None
if repository_url and scm_type in ("github", "gitlab", "bitbucket"):
branch = getattr(config, 'branch', None) or 'main'
blob_path = {"github": "blob", "gitlab": "-/blob", "bitbucket": "src"}[scm_type]
return f"{repository_url}/{blob_path}/{branch}/{clean_path}"

# Fallback to Socket file view for API or unknown repository types
if hasattr(diff, 'report_url') and diff.report_url:
# Strip leading slash and URL encode for Socket dashboard
Expand Down
74 changes: 69 additions & 5 deletions socketsecurity/core/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ def _github_number(env: Mapping[str, str]) -> int:
return parse_pull_request_number(match.group(1)) if match else 0


def _github_url(number: int, repo: Optional[str], env: Mapping[str, str]) -> Optional[str]:
remote_host, remote_path = parse_git_remote(env.get("BUILDKITE_REPO"))
def _github_repository_url(
repo: Optional[str], env: Mapping[str, str], remote_url: Optional[str] = None
) -> Optional[str]:
remote_host, remote_path = parse_git_remote(env.get("BUILDKITE_REPO") or remote_url)
# config.repo is only ever a bare repository name, so it cannot produce a
# slug on its own; it is kept last for callers that pass a full owner/repo.
repository = env.get("GITHUB_REPOSITORY") or remote_path or repo
Expand All @@ -71,20 +73,82 @@ def _github_url(number: int, repo: Optional[str], env: Mapping[str, str]) -> Opt
or (_http_url(f"https://{remote_host}") if remote_host else None)
or "https://github.com"
)
return f"{server}/{repository.strip('/')}/pull/{number}"
return f"{server}/{repository.strip('/')}"


def _gitlab_url(number: int, repo: Optional[str], env: Mapping[str, str]) -> Optional[str]:
def _gitlab_project_url(
repo: Optional[str], env: Mapping[str, str], remote_url: Optional[str] = None
) -> Optional[str]:
project_url = _repository_url(env.get("CI_PROJECT_URL"))
if not project_url:
remote_host, remote_path = parse_git_remote(env.get("BUILDKITE_REPO"))
remote_host, remote_path = parse_git_remote(env.get("BUILDKITE_REPO") or remote_url)
project_path = env.get("CI_PROJECT_PATH") or remote_path or repo
server = (
_http_url(env.get("CI_SERVER_URL"))
or (_http_url(f"https://{remote_host}") if remote_host else None)
)
if server and project_path and "/" in project_path:
project_url = f"{server}/{project_path.strip('/')}"
return project_url


def _bitbucket_repository_url(
repo: Optional[str], env: Mapping[str, str], remote_url: Optional[str] = None
) -> Optional[str]:
remote_host, remote_path = parse_git_remote(remote_url)
repository = env.get("BITBUCKET_REPO_FULL_NAME") or remote_path or repo
if not repository or "/" not in repository:
return None
server = (
_http_url(env.get("BITBUCKET_SERVER_URL"))
or (_http_url(f"https://{remote_host}") if remote_host else None)
or "https://bitbucket.org"
)
return f"{server}/{repository.strip('/')}"


def resolve_repository_url(
scm: Optional[str],
repo: Optional[str] = None,
env: Optional[Mapping[str, str]] = None,
remote_url: Optional[str] = None,
) -> Optional[str]:
"""Return the web URL of the scanned repository, such as ``https://github.com/owner/repo``.

The CI environment wins, then the checkout's ``origin`` remote. ``None`` means the
owner could not be determined: ``config.repo`` is the Socket repository slug,
which has no owner, so a link built from it alone would point nowhere.
"""
environment = env or {}
provider = str(scm or "").lower()
if provider == "github":
return _github_repository_url(repo, environment, remote_url)
if provider == "gitlab":
return _gitlab_project_url(repo, environment, remote_url)
if provider == "bitbucket":
return _bitbucket_repository_url(repo, environment, remote_url)
return None


def repository_owner(repository_url: Optional[str]) -> str:
"""Return everything before the repository name in ``repository_url``'s path.

That is the owner on GitHub and Bitbucket, and the full group path on GitLab,
where projects can be nested under subgroups.
"""
if not repository_url:
return ""
path = urlparse(repository_url).path.strip("/")
return path.rsplit("/", 1)[0] if "/" in path else ""


def _github_url(number: int, repo: Optional[str], env: Mapping[str, str]) -> Optional[str]:
repository_url = _github_repository_url(repo, env)
return f"{repository_url}/pull/{number}" if repository_url else None


def _gitlab_url(number: int, repo: Optional[str], env: Mapping[str, str]) -> Optional[str]:
project_url = _gitlab_project_url(repo, env)
return f"{project_url}/-/merge_requests/{number}" if project_url else None


Expand Down
Loading
Loading