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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
25 changes: 13 additions & 12 deletions src/websec_validator/templates/probes/race-conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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']
Expand Down