Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
31 changes: 31 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,34 @@ updates:
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 1

# Every `uses:` in .github/workflows is pinned to a full commit SHA — which is the right posture,
# and is exactly why this entry is required rather than optional. A SHA pin is immutable, so it
# does NOT pick up upstream security fixes the way a moving tag would: without an update lane the
# pins silently rot, and the ones that rot are security-critical (harden-runner, trivy-action,
# checkout, cosign-installer). Dependabot rewrites the SHA and the trailing `# vX.Y.Z` comment
# together, so the pin stays readable.
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 1

# The production image's base is digest-pinned in Dockerfile.native, and release.yml GATES every
# release on a Trivy scan of it at HIGH+CRITICAL with `ignore-unfixed: false`. Without an update
# lane the first unfixed CVE in that base blocks releases with no automation to relieve it, and
# re-pinning becomes a manual scramble under time pressure. This entry keeps the base moving
# ahead of the gate instead of behind it.
- package-ecosystem: docker
directory: "/api-sheriff/src/main/docker"
schedule:
interval: "weekly"
cooldown:
default-days: 3
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 1
20 changes: 16 additions & 4 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,13 @@ jobs:
# measured run does not approach the cap.
timeout-minutes: 75
permissions:
# Needed to upload artifacts
contents: write
# READ, not write. The former `contents: write` was justified in-comment as "needed to upload
# artifacts", which is not what that scope does: actions/upload-artifact uses the Actions
# runtime token, not GITHUB_TOKEN's contents scope. Every genuine write in this job goes to
# the OTHER repository (cuioss.github.io) and is authenticated by the GitHub App token minted
# below — the checkout of that repo passes it explicitly and the `git push` inherits it. So
# nothing here needs write access to THIS repository's contents.
contents: read

steps:
- name: Harden the runner (Audit all outbound calls)
Expand Down Expand Up @@ -160,13 +165,18 @@ jobs:
--benchmark-outcome "${BENCHMARK_OUTCOME}" \
--summary-file "$GITHUB_STEP_SUMMARY"

# The commit SHA reaches the shell through the environment, never through `${{ }}` expansion
# inside the script body — expression interpolation into a run block is a script-injection
# sink, and maven.yml and release.yml already hold this line for every value they pass.
- name: Assemble benchmark artifacts for deployment
env:
COMMIT_SHA: ${{ github.sha }}
run: |
python3 benchmarks/scripts/benchmark-pages.py assemble \
--integration-results benchmarks/target/benchmark-results/gh-pages-ready \
--previous-pages-dir previous-pages/api-sheriff/benchmarks \
--output-dir gh-pages \
--commit-sha "${{ github.sha }}"
--commit-sha "${COMMIT_SHA}"

- name: Upload benchmark results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
Expand All @@ -184,6 +194,8 @@ jobs:
token: ${{ steps.app-token.outputs.token }}

- name: Deploy to cuioss.github.io
env:
COMMIT_SHA: ${{ github.sha }}
run: |
TARGET_DIR="_pages-deploy/api-sheriff/benchmarks"
rm -rf "$TARGET_DIR"
Expand All @@ -193,5 +205,5 @@ jobs:
git config user.name "cuioss-release-bot[bot]"
git config user.email "cuioss-release-bot[bot]@users.noreply.github.com"
git add .
git diff --staged --quiet || git commit -m "Deploy benchmark results from ${{ github.sha }}"
git diff --staged --quiet || git commit -m "Deploy benchmark results from ${COMMIT_SHA}"
git push
6 changes: 6 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
# Matches every other checkout in this repository. It matters more here than elsewhere:
# this job runs an LLM agent with Bash access (git*, gh*) over comment bodies that any
# commenter can author, so leaving the token in .git/config would put a credential
# directly in reach of the very surface this workflow hands to a model. The job needs no
# authenticated git operation — the action talks to the API with its own token.
persist-credentials: false
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@
* <p>
* Structured {@code INFO} (1-99) and {@code WARN} (100-199) messages carry the
* {@code ApiSheriff} prefix and a stable numeric identifier, so they are greppable and
* assertable. This catalogue's identifier ranges are disjoint from
* {@link de.cuioss.sheriff.gateway.config.ConfigLogMessages}'s (the boot-time configuration
* subsystem catalogue), which shares the same {@code ApiSheriff} prefix: {@code 1} / {@code 4} /
* {@code 6-7} / {@code 100} / {@code 103-109} here vs {@code 2-3} / {@code 101-102} / {@code 200-201}
* there — never renumber one catalogue without checking the other for a collision.
* assertable. Identifiers are allocated across every catalogue sharing the {@code ApiSheriff}
* prefix, not per class, and that allocation is enforced by
* {@code LogMessagesCatalogueTest} rather than by an inventory kept here by hand.
* Security-relevant {@code WARN}s record only the failure <em>type</em> and route id —
* never the raw offending payload. {@code DEBUG} / {@code TRACE} diagnostics use the logger
* directly and are not catalogued here.
Expand Down Expand Up @@ -158,5 +156,23 @@ public static final class WARN {
.identifier(109)
.template("Reserved-path request body exceeded the %s byte ceiling (%s) — rejected 413")
.build();

/**
* The directory asset source could not obtain a {@code SecureDirectoryStream} on its root,
* so it serves assets through the resolved path rather than by descending from the root one
* confined component at a time. Emitted once per source: it reports a fixed property of the
* platform, not a per-request event.
* <p>
* The consequence is concrete and is stated rather than glossed: the descriptor-relative
* walk closes the check-then-act window on every path component, the fallback closes it on
* the final component only, so an ancestor directory of the asset can still be replaced by a
* symlink after the in-root check and be traversed by the read. The template names the
* configured asset root — operator-supplied configuration, never request content.
*/
public static final LogRecord ASSET_CONFINED_WALK_UNAVAILABLE = LogRecordModel.builder()
.prefix(PREFIX)
.identifier(117)
.template("Asset root '%s' offers no SecureDirectoryStream on this platform — falling back to the resolved-path read, which leaves an ancestor-directory symlink swap after the in-root check unrefused")
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@
*/
public sealed interface AssetSource permits DirectoryAssetSource, UpstreamAssetSource {

/**
* The served-asset byte cap (10 MiB) — <strong>the single derivation seam both implementations
* read</strong>, never re-declared per class (ADR-0026, "one derivation seam, not one per
* stage").
* <p>
* It lives on the sealed seam rather than on either implementation because the permitted set is
* exactly the two sources that enforce it: declaring it twice let the two literals drift
* independently, with nothing structurally tying them together, which is the seam-erosion shape
* ADR-0026 names. Reading one constant makes "both sources cap at the same value" true by
* construction instead of by coincidence.
* <p>
* <strong>This is NOT the per-route request-body cap.</strong> The two bounds are routinely
* conflated because both are byte ceilings on the request path, so the distinction is stated
* here once:
* <ul>
* <li><strong>This cap</strong> bounds the <em>served asset</em> — the response bytes a
* {@code directory} or {@code upstream} asset route returns. It is a fixed product
* constant, deliberately <em>not</em> operator-configurable ({@code asset_defaults}
* carries content types only), and it is enforced inside each source.</li>
* <li><strong>{@code security_filter.max_body_bytes}</strong> bounds the <em>inbound request
* body</em>. It is per-route and operator-configurable, it is enforced by
* {@code ThoroughChecksStage} and streamed by {@code DispatchStage.ByteCappedBodyStream},
* and it answers a breach with {@code 413 CONTENT_TOO_LARGE} (ADR-0023).</li>
* </ul>
* An asset route's terminal action applies no request-body cap of its own, and this cap never
* bounds a proxied request body — they govern opposite directions over different inputs.
*/
long DEFAULT_MAX_BYTES = 10L * 1024 * 1024;

/**
* Serves the confined asset addressed by {@code subPath}.
*
Expand Down
Loading
Loading