Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Revisions that only reformat or mechanically re-lint code.
# Configure once per clone:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# (GitHub applies this file automatically in its blame view.)
48 changes: 48 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Lint

env:
PYTHON_VERSION: "3.12"

# Deliberately not path-filtered. Ruff finishes in well under a minute, and its
# trigger surface is every Python file in the repository -- including the ones
# outside the Unit Tests filters (.hooks/, benchmarks/, tests/e2e/). Running
# unconditionally also keeps this usable as a required status check: a
# path-filtered workflow reports as "not run" rather than "passed", which blocks
# any pull request that does not happen to touch the filtered paths.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: lint-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
- name: 🐍 setup python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: 🛠️ install deps
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --extra dev
# Same ruff version the pre-commit hook uses (pinned in pyproject.toml,
# locked in uv.lock), so a clean commit locally stays clean here.
- name: 🧹 ruff check
run: uv run ruff check
- name: 🎨 ruff format
run: uv run ruff format --check
12 changes: 12 additions & 0 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ jobs:
core.setOutput('head_sha', pullRequest.head.sha);

build:
# This job checks out and executes untrusted pull request code. Under
# workflow_dispatch it runs in the default branch's context, which means its
# Actions cache scope is main's. Do not add caching here -- no `actions/cache`
# step, and no `cache:` input on setup-python -- or a preview build of a
# malicious branch could plant an entry that every workflow on main then
# restores. CodeQL's actions/cache-poisoning/poisonable-step alerts point at
# these steps for exactly that reason; they are inert only while nothing in
# this job writes a cache.
#
# The privilege split is what keeps this safe: this job holds `contents: read`
# and no secrets, and publish-package (which holds `id-token: write`) never
# checks out code -- it only downloads the built artifact.
needs: context
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
20 changes: 0 additions & 20 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,6 @@ jobs:
uv export --no-hashes --no-emit-project --format requirements-txt > /tmp/req-audit.txt
uvx pip-audit --strict --progress-spinner off --disable-pip --no-deps -r /tmp/req-audit.txt

ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
- name: 🐍 setup python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: 🛠️ install deps
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --extra dev
- name: 🧹 run ruff
run: uv run ruff check

unsupported-python-install:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/version-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ jobs:
pip install packaging

# Get version from current PR
PR_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'")
PR_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "\"'")
echo "PR_VERSION=$PR_VERSION" >> $GITHUB_ENV

# Get version from main branch
MAIN_VERSION=$(git show origin/main:socketsecurity/__init__.py | grep -o "__version__.*" | awk '{print $3}' | tr -d "'")
MAIN_VERSION=$(git show origin/main:socketsecurity/__init__.py | grep -o "__version__.*" | awk '{print $3}' | tr -d "\"'")
echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV

export PR_VERSION
Expand Down
17 changes: 14 additions & 3 deletions .hooks/sync_version.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PYPI_PROD_API = "https://pypi.org/pypi/socketsecurity/json"
PYPI_TEST_API = "https://test.pypi.org/pypi/socketsecurity/json"


def read_version_from_init(path: pathlib.Path) -> str:
content = path.read_text()
match = VERSION_PATTERN.search(content)
Expand All @@ -24,6 +25,7 @@ def read_version_from_init(path: pathlib.Path) -> str:
sys.exit(1)
return match.group(1)


def read_version_from_git(path: str) -> str:
try:
output = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True)
Expand All @@ -34,13 +36,15 @@ def read_version_from_git(path: str) -> str:
except subprocess.CalledProcessError:
return None


def bump_patch_version(version: str) -> str:
if ".dev" in version:
version = version.split(".dev")[0]
parts = version.split(".")
parts[-1] = str(int(parts[-1]) + 1)
return ".".join(parts)


def parse_stable_version(version: str):
if not STABLE_VERSION_PATTERN.fullmatch(version):
return None
Expand Down Expand Up @@ -72,6 +76,7 @@ def fetch_latest_stable_pypi_version():
return None
return max(stable_versions)


def find_next_available_dev_version(base_version: str) -> str:
existing_versions = fetch_existing_versions(PYPI_TEST_API)
for i in range(1, 100):
Expand All @@ -94,12 +99,13 @@ def find_next_stable_patch_version(current_version: str) -> str:
next_parts = (base_parts[0], base_parts[1], base_parts[2] + 1)
return format_stable_version(next_parts)


def inject_version(version: str):
print(f"🔁 Updating version to: {version}")

# Update __init__.py
init_content = INIT_FILE.read_text()
new_init_content = VERSION_PATTERN.sub(f"__version__ = '{version}'", init_content)
new_init_content = VERSION_PATTERN.sub(f'__version__ = "{version}"', init_content)
INIT_FILE.write_text(new_init_content)

# Update pyproject.toml
Expand Down Expand Up @@ -190,16 +196,21 @@ def main():
inject_version(new_version)
uv_lock_changed = run_uv_lock()
lock_hint = " and uv.lock" if uv_lock_changed else ""
print(f"⚠️ Version {current_version} is already published on PyPI — auto-bumped to {new_version}. Please git add{lock_hint} + commit again.")
print(
f"⚠️ Version {current_version} is already published on PyPI — auto-bumped to {new_version}. Please git add{lock_hint} + commit again."
)
sys.exit(1)

uv_lock_changed = run_uv_lock()
if uv_lock_changed:
print("⚠️ Version already bumped, but uv.lock was out of date and has been updated. Please git add uv.lock + commit again.")
print(
"⚠️ Version already bumped, but uv.lock was out of date and has been updated. Please git add uv.lock + commit again."
)
sys.exit(1)

print("✅ Version already bumped and uv.lock is up to date — proceeding.")
sys.exit(0)


if __name__ == "__main__":
main()
27 changes: 26 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,29 @@ repos:
entry: python .hooks/sync_version.py
language: python
always_run: true
pass_filenames: false
pass_filenames: false

# Ruff runs out of the project environment rather than the upstream
# astral-sh/ruff-pre-commit mirror so its version is pinned in exactly one
# place: `ruff==0.16.4` under [project.optional-dependencies].dev, locked
# in uv.lock and used verbatim by the Lint workflow. Dependabot has no
# pre-commit ecosystem and will not touch a mirror's `rev:`, so a mirror
# would drift out of step with CI and produce the worst failure mode for a
# hook -- clean locally, red on the pull request.
#
# `--fix` applies only ruff's fixes marked safe. When it changes a file
# pre-commit aborts the commit and leaves the edit in the working tree, so
# nothing lands without being looked at.
- id: ruff-check
name: ruff check
entry: uv run --extra dev ruff check --force-exclude --fix
language: system
types_or: [python, pyi]
require_serial: true

- id: ruff-format
name: ruff format
entry: uv run --extra dev ruff format --force-exclude
language: system
types_or: [python, pyi]
require_serial: true
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
# Changelog

## 2.7.3

### Fixed: credentials could appear in debug log output

- Running with `--debug` logged the whole configuration, including the Socket API
token, in clear text. In CI that lands in the job log, which is retained, shared
in support tickets and world-readable for public repositories. Configuration is
now logged through a redacted view that masks credential-bearing fields.
- The Slack integration logged the full webhook URL, once unconditionally at debug
level. A webhook URL is a bearer credential -- anyone holding it can post into
the channel. These log lines now show only the scheme and host. Because the
Slack plugin runs while server log streaming is active, and that handler applies
no level filter, those URLs were also being uploaded to Socket.
- If a Socket API token or Slack webhook URL may have been exposed in CI logs,
rotate it.

### Fixed: manifest links used the wrong host for some organizations

- The source-control type was partly inferred by searching the Socket report URL
for "github", "gitlab" or "bitbucket". That URL is always a Socket dashboard
link, so the only part that could match was the organization slug: an org whose
slug contained one of those words got manifest links pointing at a repository
host it may not use. The type now comes from `--scm` alone.

### Fixed: package timestamps were truncated

- `Package.created_at` stripped its `" (Coordinated Universal Time)"` suffix with
`str.strip()`, which treats its argument as a set of characters rather than a
suffix. Timestamps beginning with `Tue` lost their leading `T`, and timestamps
that carried no such suffix lost a trailing `T`. The suffix is now removed with
`str.removesuffix()`.

### Fixed: notification delivery could hang a pipeline indefinitely

- Slack, Teams, Jira, generic webhook and GitLab commit-status requests were sent
without a timeout. `requests` blocks forever by default, so an unresponsive
endpoint could hold a run open until the CI job itself timed out. All of these
calls now use an explicit 30 second timeout.

### Fixed: two internal guards did nothing under `python -O`

- A manifest upload checked its organization slug with `assert`, which the
interpreter removes entirely in optimised mode. It is now an explicit check that
raises with a readable message. A second, redundant `assert` was removed.

### Fixed: a debug message was written to stdout

- Duplicate packages in a scan's SBOM artifacts printed to stdout, which also
carries machine-readable output such as SARIF. The message is now logged at
debug level.

### Changed: configuration messages follow the CLI logger

- `config.py` logged through the root logger, so its warnings and errors ignored
the configured log level and format. They now use the `socketcli` logger like
the rest of the CLI.

## 2.7.2

### Changed: bump pinned @coana-tech/cli to 15.10.39
Expand Down
75 changes: 75 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ dependencies:
uv sync --all-extras
```

Install the git hooks once per clone:

```bash
make hooks
```

Before opening a pull request, run:

```bash
make lint
make test
uv run hatch build
uv run python -m twine check dist/*
Expand All @@ -22,6 +29,74 @@ uv run python -m twine check dist/*
To develop against a local SDK checkout, set `SOCKET_SDK_PATH` if it is not at
`../socketdev`, then run `make first-time-local-setup`.

## Linting

Ruff is the only linter. It runs in three places, all reading the same
configuration from `pyproject.toml` and the same version pinned in
`[project.optional-dependencies].dev`:

- `make lint` locally,
- the `ruff-check` pre-commit hook, on the files a commit touches,
- the `Lint` workflow, on every pull request and every push to `main`.

The pre-commit hook applies ruff's safe fixes and then fails the commit, leaving
the edits unstaged so they get read before they land. CI is the backstop for
commits made with `--no-verify` or without hooks installed.

`ruff format` is enforced the same way. It owns line length (120) and
whitespace, so the linter does not duplicate those checks: `E501` and `W291`/
`W293` are deliberately not selected. Everything the formatter cannot reflow is
a string literal -- argparse help text, log messages, the Markdown used to build
pull request comments -- where rewrapping risks silently changing text that
customers read.

The pull request comment markup is the clearest case. It uses trailing
double-spaces as Markdown hard line breaks, so stripping them takes the rendered
comment from two lines to one and runs the "Caution" banner into the body text:

```
> **Caution**··
> **Review the following alerts detected in dependencies.**··
```

Rendered with those two trailing spaces the banner sits on its own line. Without
them both lines collapse into a single paragraph. Whitespace inside a string is
content, and the formatter is right to leave it alone.

### One trap worth knowing

Never run `ruff check --select <narrow-list> --fix` with `RUF100` in the select.
With a narrow select, RUF100 considers every `# noqa` for a *non-selected* rule
to be unused and deletes it -- silently stripping the complexity suppressions
across the repository. Run `make lint-fix`, which uses the full configured rule
set, instead of hand-rolling a `--select`.

### Complexity limits

Two rules bound how large a single function may get:

| Rule | Limit | What it measures |
| --- | --- | --- |
| `C901` | 12 | Cyclomatic complexity: independent paths through a function, which is also the number of tests needed to cover it. |
| `PLR0913` | 8 | Arguments in a function definition. |

Functions that already exceed these limits carry an explicit
`# noqa: C901` / `# noqa: PLR0913` on their `def` line. That list is a backlog,
not a precedent:

- **Do not add a new suppression.** If a function you are writing trips the
limit, split it. This matters most for generated or model-assisted code, where
branches accumulate quickly and nothing pushes back.
- **Suppressions clean themselves up.** `RUF100` fails the build on a `# noqa`
that no longer applies, so refactoring a function back under the limit forces
the marker to be removed. The backlog can only shrink.

To see what is left:

```bash
grep -rn 'noqa: C901\|noqa: PLR0913' socketsecurity/ tests/
```

## Pull request validation

The `Package Check` workflow runs automatically for pull requests. It builds
Expand Down
Loading