Mostly safe — a couple of notes worth reading.
Scanned 5/3/2026, 6:42:45 PM·Cached result·Fast Scan·45 rules·How we decide ↗
AIVSS Score
Low
Severity Breakdown
0
critical
9
high
20
medium
11
low
MCP Server Information
Findings
This package has a moderate security risk with a B grade and a safety score of 60/100. While no critical vulnerabilities were found, it contains 9 high-severity issues—primarily XML external entity (XXE) risks and server configuration flaws—along with 20 medium-severity findings like ANSI escape injection and resource exhaustion risks. The lack of critical findings is positive, but the volume of high and medium issues suggests potential stability and security gaps that should be reviewed before installation.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 40 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasn’t shipped yet.
Showing 1–30 of 40 findings
40 findings
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 116 | response.raise_for_status() |
| 117 | |
| 118 | # Parse XML response |
| 119 | root = ET.fromstring(response.content) |
| 120 | |
| 121 | # Check for OAI-PMH errors |
| 122 | error_elem = root.find(f'.//{{{OAI_NS}}}error') |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 105 | continue |
| 106 | |
| 107 | response.raise_for_status() |
| 108 | root = ET.fromstring(response.content) |
| 109 | papers: List[Paper] = [] |
| 110 | result_nodes = self._find_top_level_results(root) |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 77 | summary_response = self.session.get(self.EUTILS_SUMMARY_URL, params=summary_params, timeout=30) |
| 78 | summary_response.raise_for_status() |
| 79 | summary_root = ET.fromstring(summary_response.content) |
| 80 | |
| 81 | # Step 3: Parse each summary record |
| 82 | for docsum in summary_root.findall('.//DocSum'): |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 33 | 'retmode': 'xml' |
| 34 | } |
| 35 | fetch_response = requests.get(self.FETCH_URL, params=fetch_params) |
| 36 | fetch_root = ET.fromstring(fetch_response.content) |
| 37 | |
| 38 | papers = [] |
| 39 | for article in fetch_root.findall('.//PubmedArticle'): |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 58 | search_response = self.session.get(self.EUTILS_SEARCH_URL, params=search_params, timeout=30) |
| 59 | search_response.raise_for_status() |
| 60 | search_root = ET.fromstring(search_response.content) |
| 61 | |
| 62 | # Get PMC IDs |
| 63 | pmcids = [id_elem.text for id_elem in search_root.findall('.//Id') if id_elem.text] |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 88 | raise requests.RequestException("dblp API unavailable") |
| 89 | |
| 90 | # Parse XML response |
| 91 | root = ET.fromstring(response.content) |
| 92 | |
| 93 | # dblp XML structure: result > hits > hit > info |
| 94 | hits = root.findall('.//hit') |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
XML parser configured without entity expansion disabled. XXE (XML external entity) attacks can read local files, exfiltrate data, and cause SSRF when the parser resolves external entities.
Evidence
| 22 | 'sort': sort, |
| 23 | } |
| 24 | search_response = requests.get(self.SEARCH_URL, params=search_params) |
| 25 | search_root = ET.fromstring(search_response.content) |
| 26 | ids = [id.text for id in search_root.findall('.//Id') if id.text] |
| 27 | if not ids: |
| 28 | return [] |
Remediation
Use defusedxml in Python (drop-in replacement for stdlib XML modules). In Java, set XMLConstants.FEATURE_SECURE_PROCESSING=true and disable external-DTD features on the factory before parsing.
TLS certificate verification is disabled on an outbound HTTP client. Any MITM in the network path can intercept and modify requests / responses — credentials, tokens, and tool output flow over a channel with no integrity guarantee. Python requests / httpx: drop `verify=False`. If the peer is using a private CA, set `verify="/path/to/ca-bundle.pem"` or configure the system trust store. Node TS axios / fetch: drop `rejectUnauthorized: false` from the agent / `httpsAgent` options. Same private-CA
Evidence
| 83 | # Search on Sci-Hub |
| 84 | search_url = f"{self.base_url}/{identifier}" |
| 85 | response = self.session.get(search_url, verify=False, timeout=20) |
| 86 | |
| 87 | if response.status_code != 200: |
| 88 | return None |
| 89 | |
| 90 | soup = BeautifulSoup(response.content, 'html.parser') |
| 91 | |
| 92 | # Check for article not found |
| 93 | if "article not found" in response.text.lower(): |
Remediation
Drop the verify-disable flag. If the peer presents a private CA: - Python: pass `verify="/path/to/ca.pem"` or trust the system store - Node: `new https.Agent({ ca: fs.readFileSync("ca.pem") })` - Go: load the CA via `x509.NewCertPool().AppendCertsFromPEM(...)` and set `tls.Config.RootCAs` Self-signed certificates: import the cert into the OS trust chain rather than disabling verification per-call.
TLS certificate verification is disabled on an outbound HTTP client. Any MITM in the network path can intercept and modify requests / responses — credentials, tokens, and tool output flow over a channel with no integrity guarantee. Python requests / httpx: drop `verify=False`. If the peer is using a private CA, set `verify="/path/to/ca-bundle.pem"` or configure the system trust store. Node TS axios / fetch: drop `rejectUnauthorized: false` from the agent / `httpsAgent` options. Same private-CA
Evidence
| 51 | return None |
| 52 | |
| 53 | # Download the PDF |
| 54 | response = self.session.get(pdf_url, verify=False, timeout=30) |
| 55 | |
| 56 | if response.status_code != 200: |
| 57 | logging.error(f"Failed to download PDF, status {response.status_code}") |
| 58 | return None |
| 59 | |
| 60 | if response.headers.get('Content-Type') != 'application/pdf': |
Remediation
Drop the verify-disable flag. If the peer presents a private CA: - Python: pass `verify="/path/to/ca.pem"` or trust the system store - Node: `new https.Agent({ ca: fs.readFileSync("ca.pem") })` - Go: load the CA via `x509.NewCertPool().AppendCertsFromPEM(...)` and set `tls.Config.RootCAs` Self-signed certificates: import the cert into the OS trust chain rather than disabling verification per-call.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 704 | else: |
| 705 | query = "climate change" |
| 706 | |
| 707 | print(f"Searching OpenAIRE for: {query}") |
| 708 | papers = searcher.search(query, max_results=5) |
| 709 | |
| 710 | print(f"Found {len(papers)} papers:") |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 241 | ] |
| 242 | |
| 243 | for query in test_queries[:1]: # Test first query only |
| 244 | print(f"\nSearching BASE for: '{query}'") |
| 245 | papers = searcher.search(query, max_results=3) |
| 246 | print(f"Found {len(papers)} papers") |
| 247 | for i, paper in enumerate(papers): |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 463 | ] |
| 464 | |
| 465 | for query in test_queries[:1]: # Test first query only |
| 466 | print(f"\nSearching DOAJ for: '{query}'") |
| 467 | papers = searcher.search(query, max_results=3) |
| 468 | print(f"Found {len(papers)} papers") |
| 469 | for i, paper in enumerate(papers): |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 393 | else: |
| 394 | query = "machine learning" |
| 395 | |
| 396 | print(f"Searching CiteSeerX for: {query}") |
| 397 | papers = searcher.search(query, max_results=5) |
| 398 | |
| 399 | print(f"Found {len(papers)} papers:") |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 171 | ] |
| 172 | |
| 173 | for query in test_queries[:1]: # Test first query only |
| 174 | print(f"\nSearching ChemRxiv for: '{query}'") |
| 175 | papers = searcher.search(query, max_results=3) |
| 176 | print(f"Found {len(papers)} preprints") |
| 177 | for i, paper in enumerate(papers): |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 374 | else: |
| 375 | query = "machine learning" |
| 376 | |
| 377 | print(f"Searching dblp for: {query}") |
| 378 | papers = searcher.search(query, max_results=5) |
| 379 | |
| 380 | print(f"Found {len(papers)} papers:") |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure — useful recon for attackers.
Evidence
| 478 | try: |
| 479 | return pubmed_searcher.download_pdf(paper_id, save_path) |
| 480 | except NotImplementedError as e: |
| 481 | return str(e) |
| 482 | |
| 483 | |
| 484 | @mcp.tool() |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure — useful recon for attackers.
Evidence
| 727 | try: |
| 728 | return crossref_searcher.download_pdf(paper_id, save_path) |
| 729 | except NotImplementedError as e: |
| 730 | return str(e) |
| 731 | |
| 732 | |
| 733 | @mcp.tool() |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Full exception detail or stack trace returned to the caller. Leaking tracebacks exposes internal paths, library versions, and query structure — useful recon for attackers.
Evidence
| 255 | } |
| 256 | except Exception as e: |
| 257 | logger.error(f"Error requesting API: {e}") |
| 258 | return {"error": "general_error", "message": str(e)} |
| 259 | |
| 260 | return { |
| 261 | "error": "max_retries_exceeded", |
Remediation
Log the full exception server-side with a correlation ID; return only {"error_id": id, "message": "internal error"} to the caller. Never enable Flask debug mode in production.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 80 | def download_pdf(self, paper_id: str, save_path: str) -> str: |
| 81 | pdf_url = f"https://arxiv.org/pdf/{paper_id}.pdf" |
| 82 | response = requests.get(pdf_url) |
| 83 | os.makedirs(save_path, exist_ok=True) |
| 84 | output_file = f"{save_path}/{paper_id}.pdf" |
| 85 | with open(output_file, 'wb') as f: |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 32 | 'id': ','.join(ids), |
| 33 | 'retmode': 'xml' |
| 34 | } |
| 35 | fetch_response = requests.get(self.FETCH_URL, params=fetch_params) |
| 36 | fetch_root = ET.fromstring(fetch_response.content) |
| 37 | |
| 38 | papers = [] |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 21 | 'retmode': 'xml', |
| 22 | 'sort': sort, |
| 23 | } |
| 24 | search_response = requests.get(self.SEARCH_URL, params=search_params) |
| 25 | search_root = ET.fromstring(search_response.content) |
| 26 | ids = [id.text for id in search_root.findall('.//Id') if id.text] |
| 27 | if not ids: |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage — e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | # Multi-stage build for smaller image |
| 2 | FROM python:3.12-slim AS builder |
| 3 | |
| 4 | WORKDIR /app |
| 5 | COPY pyproject.toml README.md LICENSE ./ |
| 6 | COPY paper_search_mcp/ paper_search_mcp/ |
| 7 | |
| 8 | RUN pip install --no-cache-dir build \ |
| 9 | && python -m build --wheel \ |
| 10 | && pip install --no-cache-dir dist/*.whl |
| 11 | |
| 12 | FROM python:3.12-slim |
| 13 | |
| 14 | WORKDIR /app |
| 15 | COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages |
| 16 | COPY --from=builder /usr/local/bin/paper-search-mcp /usr/local/bin/paper-searc |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 16 | uses: actions/checkout@v4 |
| 17 | |
| 18 | - name: Set up Python ${{ matrix.python-version }} |
| 19 | uses: actions/setup-python@v5 |
| 20 | with: |
| 21 | python-version: ${{ matrix.python-version }} |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 21 | python-version: ${{ matrix.python-version }} |
| 22 | |
| 23 | - name: Install uv |
| 24 | uses: astral-sh/setup-uv@v4 |
| 25 | |
| 26 | - name: Install dependencies |
| 27 | run: uv pip install --system -e ".[dev]" 2>/dev/null || uv pip install --system -e . |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 37 | id-token: write |
| 38 | steps: |
| 39 | - name: Checkout code |
| 40 | uses: actions/checkout@v4 |
| 41 | |
| 42 | - name: Set up Python |
| 43 | uses: actions/setup-python@v5 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 13 | python-version: ['3.10', '3.12', '3.13'] |
| 14 | steps: |
| 15 | - name: Checkout code |
| 16 | uses: actions/checkout@v4 |
| 17 | |
| 18 | - name: Set up Python ${{ matrix.python-version }} |
| 19 | uses: actions/setup-python@v5 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 40 | uses: actions/checkout@v4 |
| 41 | |
| 42 | - name: Set up Python |
| 43 | uses: actions/setup-python@v5 |
| 44 | with: |
| 45 | python-version: '3.10' |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 51 | run: uv build |
| 52 | |
| 53 | - name: Publish to PyPI |
| 54 | uses: pypa/gh-action-pypi-publish@release/v1 |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
GitHub Actions `uses:` reference is not pinned to a 40-character commit SHA. Tags (`@v4`) and branches (`@main`) are mutable — a compromised maintainer or a tag rewrite can substitute malicious code into your CI pipeline silently. Pin to a SHA: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`. For readability, include the version as a trailing comment: `# v4.1.1`. Tools like `pinact` / `ratchet` automate this. Allowed unpinned forms (excluded by the rule): - Local actions `.
Evidence
| 45 | python-version: '3.10' |
| 46 | |
| 47 | - name: Install uv |
| 48 | uses: astral-sh/setup-uv@v4 |
| 49 | |
| 50 | - name: Build package |
| 51 | run: uv build |
Remediation
Pin every `uses:` to a 40-character commit SHA. Trailing comment with the version helps reviewers: `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v4.1.1` Automate the migration with `pinact` (https://github.com/suzuki-shunsuke/pinact) or `ratchet` (https://github.com/sethvargo/ratchet). Add a `pinact run --check` pre-commit hook so future PRs stay pinned. Re-pin when the action releases a new version — Dependabot can do this automatically with `version-update-strategy: inc
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 152 | pub_date = datetime.strptime(date_str, '%Y-%m-%d') |
| 153 | except ValueError: |
| 154 | try: |
| 155 | pub_date = datetime.strptime(pub_year, '%Y') |
| 156 | except ValueError: |
| 157 | pass |
| 158 | |
| 159 | # Extract URLs |
| 160 | url = item.get('fullTextUrlList', {}).get('fullTextUrl', []) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 353 | if year_match: |
| 354 | try: |
| 355 | year = int(year_match.group()) |
| 356 | return datetime(year, 1, 1) |
| 357 | except ValueError: |
| 358 | pass |
| 359 | |
| 360 | return None |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 193 | published_date = None |
| 194 | if year and year.isdigit(): |
| 195 | try: |
| 196 | published_date = datetime(int(year), 1, 1) |
| 197 | except ValueError: |
| 198 | pass |
| 199 | |
| 200 | # Extract venue |
| 201 | venue = info.get('venue', '') |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 237 | pub_date = datetime.strptime(date_str, '%Y-%m-%d') |
| 238 | except ValueError: |
| 239 | try: |
| 240 | pub_date = datetime.strptime(year, '%Y') |
| 241 | except ValueError: |
| 242 | pass |
| 243 | |
| 244 | # Construct URLs |
| 245 | url = f"https://www.ncbi.nlm.nih.gov/pmc/articles/{pmcid}/" |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 555 | published_date = datetime.fromisoformat(date_str.replace('Z', '+00:00')) |
| 556 | except ValueError: |
| 557 | try: |
| 558 | published_date = datetime.strptime(date_str[:10], '%Y-%m-%d') |
| 559 | except ValueError: |
| 560 | pass |
| 561 | |
| 562 | # Extract URLs |
| 563 | url = '' |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 204 | return head.url if head.url else candidate |
| 205 | # Even if content-type is not pdf, if redirect lands at a PDF URL, use it |
| 206 | if head.status_code == 200: |
| 207 | return candidate |
| 208 | except Exception: |
| 209 | pass |
| 210 | return "" |
| 211 | |
| 212 | def _parse_doc(self, doc: Dict[str, Any]) -> Optional[Paper]: |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 275 | published_date = None |
| 276 | if year_str and year_str.isdigit(): |
| 277 | try: |
| 278 | published_date = datetime(int(year_str), 1, 1) |
| 279 | except ValueError: |
| 280 | pass |
| 281 | |
| 282 | # Construct paper ID (use dblp key if available, otherwise generate) |
| 283 | paper_id = "" |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 386 | if not last_updated: |
| 387 | date_str = line.split(":")[0].strip() |
| 388 | try: |
| 389 | last_updated = datetime.strptime(date_str, "%Y-%m-%d") |
| 390 | except ValueError: |
| 391 | pass |
| 392 | elif history_found and ( |
| 393 | line.strip().startswith("Short URL") |
| 394 | or line.strip().startswith("License") |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 268 | except (ValueError, TypeError): |
| 269 | # Try just year |
| 270 | try: |
| 271 | published_date = datetime(int(year), 1, 1) |
| 272 | except (ValueError, TypeError): |
| 273 | pass |
| 274 | |
| 275 | # Extract journal information |
| 276 | journal = bibjson.get('journal', {}) |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 205 | # Try just year |
| 206 | year = published_date[:4] |
| 207 | if year.isdigit(): |
| 208 | pub_date = datetime(int(year), 1, 1) |
| 209 | except Exception: |
| 210 | pass |
| 211 | |
| 212 | # Extract URLs |
| 213 | url = item.get('url', '') |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 121 | published_date = None |
| 122 | if pub_date_str: |
| 123 | try: |
| 124 | published_date = datetime.strptime(pub_date_str, "%Y-%m-%d") |
| 125 | except ValueError: |
| 126 | pass |
| 127 | |
| 128 | # Categories / Concepts |
| 129 | concepts = [ |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.