Compare commits

...

2 Commits

Author SHA1 Message Date
rUv d9e87e13b4 fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action (#930)
Two real problems in the Static Application Security Testing job:

1. **It scanned a path that no longer exists.** `bandit -r src/` and
   `semgrep … src/` pointed at the repo-root `src/`, but the Python code
   moved to `archive/v1/src/` (64 .py files) when the runtime was rewritten
   in Rust. So the SAST scan matched nothing — a silent no-op (this is also
   why `bandit-results.sarif` was "Path does not exist" on recent runs).
   Fixed both to `archive/v1/src/`.

2. **Deprecated + redundant + flaky semgrep step.** The
   `returntocorp/semgrep-action@v1` step pulled `returntocorp/semgrep-agent:v1`
   from Docker Hub every run (intermittently timing out → red check, e.g. on
   #929) and is EOL. It was redundant: the pip `semgrep --sarif` step is what
   feeds GitHub Security; the action only pushed to the Semgrep cloud app via
   SEMGREP_APP_TOKEN. Removed it and folded its `p/docker` + `p/kubernetes`
   rulesets into the pip semgrep command, so coverage is preserved with no
   Docker pull.

The job stays `continue-on-error: true` (non-gating). YAML validated.
2026-06-03 11:18:49 +02:00
rUv be48143f77 fix(auth): match the Bearer scheme case-insensitively (RFC 6750) (#929)
`require_bearer` parsed the Authorization header with
`strip_prefix("Bearer ")`, which is case-sensitive. Per RFC 6750 §2.1 /
RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent
as `Authorization: bearer <token>` (or `BEARER`, or with extra whitespace)
was rejected with a confusing "invalid bearer token" 401 — needless friction
when setting up `RUVIEW_API_TOKEN` (the active #864/#924 theme).

Now the scheme is matched with `eq_ignore_ascii_case` and leading token
whitespace trimmed. The token comparison itself is unchanged — still exact
and constant-time (`ct_eq`) — so this does not weaken auth: a wrong token or
a non-Bearer scheme (`Basic …`) still returns 401.

New test `accepts_case_insensitive_bearer_scheme` covers `bearer`/`BEARER`/
extra-space (accept) and wrong-token/`Basic` (still reject). bearer_auth
suite: 9 passed.
2026-06-03 11:07:34 +02:00
2 changed files with 53 additions and 17 deletions
+17 -16
View File
@@ -46,7 +46,10 @@ jobs:
- name: Run Bandit security scan
run: |
bandit -r src/ -f sarif -o bandit-results.sarif
# The Python codebase lives under archive/v1/src (it moved there when
# the runtime was rewritten in Rust). Scanning `src/` matched nothing,
# so this SAST step was a silent no-op.
bandit -r archive/v1/src/ -f sarif -o bandit-results.sarif
continue-on-error: true
- name: Upload Bandit results to GitHub Security
@@ -57,22 +60,20 @@ jobs:
sarif_file: bandit-results.sarif
category: bandit
- name: Run Semgrep security scan
continue-on-error: true
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/python
p/docker
p/kubernetes
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- name: Generate Semgrep SARIF
# Removed the deprecated `returntocorp/semgrep-action@v1` step: it was
# redundant (the pip `semgrep --sarif` below is what feeds GitHub Security;
# the action only pushed to the Semgrep cloud app via SEMGREP_APP_TOKEN) and
# it pulled `returntocorp/semgrep-agent:v1` from Docker Hub on every run,
# which intermittently timed out and turned this check red. The pip semgrep
# (installed above) needs no Docker pull. The action's `p/docker` +
# `p/kubernetes` rulesets are folded into the command below so coverage is
# preserved.
- name: Run Semgrep + generate SARIF
run: |
semgrep --config=p/security-audit --config=p/secrets --config=p/python --sarif --output=semgrep.sarif src/
semgrep \
--config=p/security-audit --config=p/secrets --config=p/python \
--config=p/docker --config=p/kubernetes \
--sarif --output=semgrep.sarif archive/v1/src/
continue-on-error: true
- name: Upload Semgrep results to GitHub Security
@@ -100,7 +100,17 @@ pub async fn require_bearer(
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
// RFC 6750 §2.1 / RFC 7235 §2.1: the auth-scheme ("Bearer") is
// case-insensitive. Match it as such (and tolerate extra leading
// whitespace before the token) so a correct token isn't rejected
// just because a client sent `bearer`/`BEARER`. The token compare
// below stays exact + constant-time.
.and_then(|s| {
let (scheme, token) = s.split_once(' ')?;
scheme
.eq_ignore_ascii_case("Bearer")
.then(|| token.trim_start())
});
let ok = supplied
.map(|s| ct_eq(s.as_bytes(), expected.as_bytes()))
.unwrap_or(false);
@@ -185,6 +195,31 @@ mod tests {
);
}
#[tokio::test]
async fn accepts_case_insensitive_bearer_scheme() {
// RFC 6750 §2.1 / RFC 7235 §2.1: the auth-scheme is case-insensitive.
// A correct token must authenticate regardless of scheme casing or
// extra whitespace; a wrong token must still be rejected.
async fn req_status(auth_value: &str) -> StatusCode {
let r = wrap(AuthState::from_token("s3cr3t"));
let mut req = Request::builder()
.method("GET")
.uri("/api/v1/info")
.body(Body::empty())
.unwrap();
req.headers_mut()
.insert(AUTHORIZATION, auth_value.parse().unwrap());
r.oneshot(req).await.unwrap().status()
}
assert_eq!(req_status("Bearer s3cr3t").await, StatusCode::OK);
assert_eq!(req_status("bearer s3cr3t").await, StatusCode::OK);
assert_eq!(req_status("BEARER s3cr3t").await, StatusCode::OK);
assert_eq!(req_status("Bearer s3cr3t").await, StatusCode::OK); // extra space
// Scheme leniency must NOT weaken the token check.
assert_eq!(req_status("bearer nope").await, StatusCode::UNAUTHORIZED);
assert_eq!(req_status("Basic s3cr3t").await, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn enabled_blocks_api_with_wrong_bearer() {
let r = wrap(AuthState::from_token("s3cr3t"));