Skip to content
Merged
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
42 changes: 3 additions & 39 deletions .github/security/container-vulnerability-baseline.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema_version": 1,
"reviewed_on": "2026-08-23",
"expires_on": "2026-11-01",
"base_image": "python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6",
"reviewed_on": "2026-08-26",
"expires_on": "2026-11-26",
"base_image": "python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83",
"images": {
"synthetic": "python-3.14-slim",
"getcomics": "python-3.14-slim",
Expand Down Expand Up @@ -89,24 +89,6 @@
"severity": "High",
"rationale": "Inherited from the pinned Python 3.14 Debian base; no supported-line fix is currently available."
},
{
"id": "CVE-2026-11940",
"package": "python",
"severity": "High",
"rationale": "Inherited from Python 3.14; the available fix is outside the supported Python line."
},
{
"id": "CVE-2026-11972",
"package": "python",
"severity": "High",
"rationale": "Inherited from Python 3.14; the available fix is outside the supported Python line."
},
{
"id": "CVE-2026-15308",
"package": "python",
"severity": "High",
"rationale": "Inherited from Python 3.14; the available fix is outside the supported Python line."
},
{
"id": "CVE-2026-41992",
"package": "gzip",
Expand Down Expand Up @@ -190,24 +172,6 @@
"package": "perl-base",
"severity": "High",
"rationale": "Inherited from the pinned Python 3.14 Debian base; no supported-line fix is currently available."
},
{
"id": "CVE-2026-14456",
"package": "libssl3t64",
"severity": "High",
"rationale": "Inherited from the pinned Python 3.14 Debian base. The affected QUIC listener code is not enabled or used by these HTTP-only provider images, and no supported-line upstream fix is currently available."
},
{
"id": "CVE-2026-14456",
"package": "openssl",
"severity": "High",
"rationale": "Inherited from the pinned Python 3.14 Debian base. The affected QUIC listener code is not enabled or used by these HTTP-only provider images, and no supported-line upstream fix is currently available."
},
{
"id": "CVE-2026-14456",
"package": "openssl-provider-legacy",
"severity": "High",
"rationale": "Inherited from the pinned Python 3.14 Debian base. The affected QUIC listener code is not enabled or used by these HTTP-only provider images, and no supported-line upstream fix is currently available."
}
]
}
Expand Down
15 changes: 12 additions & 3 deletions docker/Dockerfile.annas-archive
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
ARG PYTHON_BASE=python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
ARG PYTHON_BASE=python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83

FROM ${PYTHON_BASE} AS build
FROM ${PYTHON_BASE} AS security-patched

RUN apt-get update && \
apt-get install --yes --no-install-recommends \
libssl3t64=3.5.7-1~deb13u2 \
openssl=3.5.7-1~deb13u2 \
openssl-provider-legacy=3.5.7-1~deb13u2 && \
rm -rf /var/lib/apt/lists/*

FROM security-patched AS build

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
Expand All @@ -13,7 +22,7 @@ COPY providers/annas_archive providers/annas_archive
RUN python -m pip wheel --wheel-dir /wheels ./packages/provider_contract && \
python -m pip wheel --find-links /wheels --wheel-dir /wheels ./providers/annas_archive

FROM ${PYTHON_BASE} AS runtime
FROM security-patched AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
Expand Down
15 changes: 12 additions & 3 deletions docker/Dockerfile.getcomics
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
ARG PYTHON_BASE=python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
ARG PYTHON_BASE=python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83

FROM ${PYTHON_BASE} AS build
FROM ${PYTHON_BASE} AS security-patched

RUN apt-get update && \
apt-get install --yes --no-install-recommends \
libssl3t64=3.5.7-1~deb13u2 \
openssl=3.5.7-1~deb13u2 \
openssl-provider-legacy=3.5.7-1~deb13u2 && \
rm -rf /var/lib/apt/lists/*

FROM security-patched AS build

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
Expand All @@ -13,7 +22,7 @@ COPY providers/getcomics providers/getcomics
RUN python -m pip wheel --wheel-dir /wheels ./packages/provider_contract && \
python -m pip wheel --find-links /wheels --wheel-dir /wheels ./providers/getcomics

FROM ${PYTHON_BASE} AS runtime
FROM security-patched AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
Expand Down
15 changes: 12 additions & 3 deletions docker/Dockerfile.libgen
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
ARG PYTHON_BASE=python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
ARG PYTHON_BASE=python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83

FROM ${PYTHON_BASE} AS build
FROM ${PYTHON_BASE} AS security-patched

RUN apt-get update && \
apt-get install --yes --no-install-recommends \
libssl3t64=3.5.7-1~deb13u2 \
openssl=3.5.7-1~deb13u2 \
openssl-provider-legacy=3.5.7-1~deb13u2 && \
rm -rf /var/lib/apt/lists/*

FROM security-patched AS build

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
Expand All @@ -13,7 +22,7 @@ COPY providers/libgen providers/libgen
RUN python -m pip wheel --wheel-dir /wheels ./packages/provider_contract && \
python -m pip wheel --find-links /wheels --wheel-dir /wheels ./providers/libgen

FROM ${PYTHON_BASE} AS runtime
FROM security-patched AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
Expand Down
11 changes: 9 additions & 2 deletions docker/Dockerfile.provider-smoke
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
ARG PYTHON_BASE=python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
FROM ${PYTHON_BASE}
ARG PYTHON_BASE=python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83
FROM ${PYTHON_BASE} AS runtime

RUN apt-get update && \
apt-get install --yes --no-install-recommends \
libssl3t64=3.5.7-1~deb13u2 \
openssl=3.5.7-1~deb13u2 \
openssl-provider-legacy=3.5.7-1~deb13u2 && \
rm -rf /var/lib/apt/lists/*

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
Expand Down
17 changes: 13 additions & 4 deletions docker/Dockerfile.synthetic
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
ARG PYTHON_BASE=python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
ARG PYTHON_BASE=python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83

FROM ${PYTHON_BASE} AS build
FROM ${PYTHON_BASE} AS security-patched

RUN apt-get update && \
apt-get install --yes --no-install-recommends \
libssl3t64=3.5.7-1~deb13u2 \
openssl=3.5.7-1~deb13u2 \
openssl-provider-legacy=3.5.7-1~deb13u2 && \
rm -rf /var/lib/apt/lists/*

FROM security-patched AS build

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
Expand All @@ -13,7 +22,7 @@ COPY providers/synthetic providers/synthetic

RUN python -m pip wheel --wheel-dir /wheels .

FROM ${PYTHON_BASE} AS runtime
FROM security-patched AS runtime

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
Expand All @@ -32,7 +41,7 @@ HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
ENTRYPOINT ["python", "-m", "uvicorn"]
CMD ["pullbox_provider_synthetic.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8780", "--no-access-log"]

FROM ${PYTHON_BASE} AS conformance
FROM security-patched AS conformance

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
Expand Down
8 changes: 4 additions & 4 deletions docker/compose.providers-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ services:
context: ..
dockerfile: docker/Dockerfile.getcomics
args:
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83
environment:
PULLBOX_PROVIDER_TOKEN: ${PULLBOX_PROVIDER_TOKEN:-test-provider-token-with-sufficient-entropy}
read_only: true
Expand All @@ -23,7 +23,7 @@ services:
context: ..
dockerfile: docker/Dockerfile.annas-archive
args:
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83
environment:
PULLBOX_PROVIDER_TOKEN: ${PULLBOX_PROVIDER_TOKEN:-test-provider-token-with-sufficient-entropy}
read_only: true
Expand All @@ -42,7 +42,7 @@ services:
context: ..
dockerfile: docker/Dockerfile.libgen
args:
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83
environment:
PULLBOX_PROVIDER_TOKEN: ${PULLBOX_PROVIDER_TOKEN:-test-provider-token-with-sufficient-entropy}
read_only: true
Expand All @@ -61,7 +61,7 @@ services:
context: ..
dockerfile: docker/Dockerfile.provider-smoke
args:
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6
PYTHON_BASE: public.ecr.aws/docker/library/python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83
depends_on:
getcomics:
condition: service_healthy
Expand Down
4 changes: 2 additions & 2 deletions docker/compose.synthetic-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ services:
dockerfile: docker/Dockerfile.synthetic
target: runtime
args:
PYTHON_BASE: ${PYTHON_BASE:-mirror.gcr.io/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6}
PYTHON_BASE: ${PYTHON_BASE:-mirror.gcr.io/library/python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83}
image: pullbox-provider-synthetic:dd0-test
environment:
PULLBOX_PROVIDER_TOKEN: ${PULLBOX_PROVIDER_TOKEN:-synthetic-conformance-token-00000000}
Expand All @@ -26,7 +26,7 @@ services:
dockerfile: docker/Dockerfile.synthetic
target: conformance
args:
PYTHON_BASE: ${PYTHON_BASE:-mirror.gcr.io/library/python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6}
PYTHON_BASE: ${PYTHON_BASE:-mirror.gcr.io/library/python:3.14-slim@sha256:83ff1d245a3d57d04152252d3ef9cb361494d0b3395abd65a5ebe91c401c8e83}
environment:
PULLBOX_PROVIDER_BASE_URL: http://synthetic:8780
PULLBOX_PROVIDER_TOKEN: ${PULLBOX_PROVIDER_TOKEN:-synthetic-conformance-token-00000000}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
rf"(?P<end>\d+(?:\.\d+)?){_TRAILING_RELEASE_LABELS}\s*$"
)
_HASH_ISSUE = re.compile(rf"\s+#\s*(?P<issue>\d+(?:\.\d+)?[A-Za-z]?){_TRAILING_RELEASE_LABELS}\s*$")
_TRAILING_ISSUE = re.compile(r"\s+(?P<issue>\d{1,5}(?:\.\d+)?[A-Za-z]?)\s*$")
_TRAILING_ISSUE = re.compile(
rf"\s+(?P<issue>\d{{1,5}}(?:\.\d+)?[A-Za-z]?){_TRAILING_RELEASE_LABELS}\s*$"
)
_SCENE_NO_ISSUE = re.compile(
r"^(?:(?P<group>[a-z][a-z0-9]{1,15})-(?P<group_series>[A-Z].+?)|(?P<series>.+?))"
r"[._\s]+(?i:No)\.?[._\s]*(?P<issue>\d{1,5}(?:\.\d+)?[A-Za-z]?)\s*$"
Expand Down
96 changes: 75 additions & 21 deletions providers/getcomics/src/pullbox_provider_getcomics/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,25 @@ async def search(
seen_candidate_ids=seen_candidate_ids,
resolver_profile=resolver_profile,
)
if len(candidates) >= limit and (
is_collection_intent(intent.issue_type)
or _has_requested_issue_coverage(candidates, intent)
if not is_collection_intent(intent.issue_type) and _has_requested_issue_coverage(
candidates, intent
):
return _prioritize_requested_issue_coverage(candidates, intent)[:limit]
if len(candidates) >= limit and is_collection_intent(intent.issue_type):
return candidates[:limit]

fallback = _standard_issue_fallback_query(intent)
if fallback is not None and not _has_requested_issue_coverage(candidates, intent):
fallbacks = _standard_issue_fallback_queries(intent)
if fallbacks and not _has_requested_issue_coverage(candidates, intent):
fallback_candidates: list[Candidate] = []
await self._append_search_candidates(
fallback,
candidates=fallback_candidates,
seen_candidate_ids=seen_candidate_ids,
resolver_profile=resolver_profile,
)
for fallback in fallbacks:
await self._append_search_candidates(
fallback,
candidates=fallback_candidates,
seen_candidate_ids=seen_candidate_ids,
resolver_profile=resolver_profile,
)
if _has_requested_issue_coverage(fallback_candidates, intent):
break
# Exact queries can return unrelated releases first. Prioritize only
# fallback packs that explicitly cover the requested issue; broad
# fallback noise must not displace a targeted exact-search result.
Expand Down Expand Up @@ -174,9 +178,12 @@ def _build_query(intent: SearchIntent) -> str:


def _build_queries(intent: SearchIntent) -> list[str]:
if not is_collection_intent(intent.issue_type):
return _standard_issue_exact_queries(intent)

title_fragment = collection_title_fragment(intent.issue_title)
explicit_title_volume = collection_title_number(intent.issue_title)
if not is_collection_intent(intent.issue_type) or title_fragment is None:
if title_fragment is None:
queries = [_build_query(intent)]
else:
queries = [f"{intent.series_title} {title_fragment}"[:700]]
Expand Down Expand Up @@ -215,14 +222,44 @@ def _build_queries(intent: SearchIntent) -> list[str]:
return queries[:5]


def _standard_issue_fallback_query(intent: SearchIntent) -> str | None:
"""Return one bounded range-pack fallback after an exact issue search misses."""
def _standard_issue_exact_queries(intent: SearchIntent) -> list[str]:
"""Return release-aware exact queries without losing legacy year behavior."""
if not intent.issue_number:
return [_build_query(intent)]

base = f"{intent.series_title} {intent.issue_number}"
queries: list[str] = []
preferred_year = intent.release_year
if preferred_year is None and intent.series_year is None:
preferred_year = intent.year
if preferred_year is not None:
queries.append(f"{base} {preferred_year}"[:700])
queries.append(base[:700])

compatibility_year = intent.series_year or intent.year
if compatibility_year is not None:
queries.append(f"{base} {compatibility_year}"[:700])
return list(dict.fromkeys(queries))[:3]


def _standard_issue_fallback_queries(intent: SearchIntent) -> list[str]:
"""Return bounded release-year and series-year range-pack fallbacks."""
if is_collection_intent(intent.issue_type) or not intent.issue_number:
return None
parts = [intent.series_title]
if intent.year:
parts.append(str(intent.year))
return " ".join(parts)[:700]
return []

years: list[int] = []
preferred_year = intent.release_year
if preferred_year is None and intent.series_year is None:
preferred_year = intent.year
if preferred_year is not None:
years.append(preferred_year)
compatibility_year = intent.series_year or intent.year
if compatibility_year is not None:
years.append(compatibility_year)
unique_years = list(dict.fromkeys(years))
if not unique_years:
return [intent.series_title[:700]]
return [f"{intent.series_title} {year}"[:700] for year in unique_years[:2]]


def _has_requested_issue_coverage(
Expand All @@ -235,6 +272,20 @@ def _has_requested_issue_coverage(
)


def _prioritize_requested_issue_coverage(
candidates: list[Candidate],
intent: SearchIntent,
) -> list[Candidate]:
"""Keep a later exact query from being displaced by earlier search noise."""
covering = [
candidate for candidate in candidates if _candidate_covers_intent(candidate, intent)
]
noncovering = [
candidate for candidate in candidates if not _candidate_covers_intent(candidate, intent)
]
return [*covering, *noncovering]


def _candidate_covers_intent(candidate: Candidate, intent: SearchIntent) -> bool:
"""Return whether a candidate covers this issue for the requested series."""
if intent.issue_number is None:
Expand All @@ -243,10 +294,13 @@ def _candidate_covers_intent(candidate: Candidate, intent: SearchIntent) -> bool
return False
if _normalized_series_title(candidate.parsed.series_title) not in _intent_series_titles(intent):
return False
expected_years = {
year for year in (intent.release_year, intent.series_year, intent.year) if year is not None
}
return not (
intent.year is not None
expected_years
and candidate.parsed.year is not None
and candidate.parsed.year != intent.year
and candidate.parsed.year not in expected_years
Comment thread
DeusExTaco marked this conversation as resolved.
)


Expand Down
Loading
Loading