diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88f41ee..a1b39c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7367af1..0fa2ef4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.12" @@ -71,4 +71,4 @@ jobs: with: name: dist path: dist/ - - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1.14.0 diff --git a/Dockerfile b/Dockerfile index bc826c2..160ea97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,9 @@ FROM python:3.14-slim # TARGETARCH is auto-populated by BuildKit (arm64/amd64) — do NOT give it a # default, or it shadows the real build arch and pulls the wrong-arch packages. ARG TARGETARCH -ARG NOIR_VERSION=1.0.0 +ARG NOIR_VERSION=1.1.0 ARG GITLEAKS_VERSION=8.30.1 -ARG TRIVY_VERSION=0.58.1 +ARG TRIVY_VERSION=0.59.1 RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git \ @@ -37,7 +37,7 @@ RUN case "${TARGETARCH}" in amd64) GL=x64 ;; arm64) GL=arm64 ;; *) GL="${TARGETA && tar -xzf /tmp/gl.tgz -C /usr/local/bin gitleaks && rm /tmp/gl.tgz # Semgrep (SAST) + Checkov (IaC) -RUN pip install --no-cache-dir semgrep checkov +RUN pip install --no-cache-dir semgrep==1.168.0 checkov==3.3.6 # The tool WORKDIR /opt/websec diff --git a/action.yml b/action.yml index 377a129..a118849 100644 --- a/action.yml +++ b/action.yml @@ -35,7 +35,7 @@ runs: using: "composite" steps: - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.11" @@ -54,7 +54,7 @@ runs: - name: Upload SARIF to Code Scanning if: ${{ always() && inputs.upload-sarif == 'true' }} - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@8533807ff379ac610d2b2c389c47e7c629d31d13 # v4.36.3 with: sarif_file: ${{ inputs.out }}/latest/results.sarif category: websec-validator diff --git a/src/websec_validator/templates/probes/race-conditions.py b/src/websec_validator/templates/probes/race-conditions.py index 4acf09e..ae481b1 100755 --- a/src/websec_validator/templates/probes/race-conditions.py +++ b/src/websec_validator/templates/probes/race-conditions.py @@ -16,11 +16,10 @@ - Compare to expected_unique (usually 1 — only one assignment should win) - If success_count > expected_unique -> race condition likely -Uses async httpx for true parallelism (synchronous loops can't trigger races). +Uses asyncio.to_thread with urllib for parallelism (synchronous loops can't trigger races). -Install: pip install httpx """ -import asyncio, httpx, json, os, re, sys +import asyncio, json, os, re, sys, urllib.request, urllib.error from pathlib import Path from collections import Counter @@ -46,23 +45,25 @@ if not TARGETS: sys.exit("No write endpoints in probe-context.json — nothing to probe.") -async def fire(client, t): +def fire_sync(t): """Single request, return (status_code, response_body_preview)""" try: - r = await client.request( - t['method'], t['url'], - json=t['payload'], - headers=HEADERS, - timeout=30.0, + req = urllib.request.Request( + t['url'], + data=json.dumps(t['payload']).encode('utf-8') if t['payload'] else None, + headers={**HEADERS, "Content-Type": "application/json"} if t['payload'] else HEADERS, + method=t['method'] ) - return (r.status_code, r.text[:120]) + with urllib.request.urlopen(req, timeout=30.0) as r: + return (r.status, r.read().decode('utf-8', errors='ignore')[:120]) + except urllib.error.HTTPError as e: + return (e.code, e.read().decode('utf-8', errors='ignore')[:120]) except Exception as e: return (None, str(e)[:120]) async def run_target(t): print(f" Firing {PARALLEL} parallel {t['method']} to {t['url'][len(TARGET):]}") - async with httpx.AsyncClient() as client: - results = await asyncio.gather(*[fire(client, t) for _ in range(PARALLEL)]) + results = await asyncio.gather(*[asyncio.to_thread(fire_sync, t) for _ in range(PARALLEL)]) codes = Counter(r[0] for r in results) success = sum(1 for r in results if r[0] and 200 <= r[0] < 300) race_likely = success > t['expected_unique']