Skip to content

Conversation

@felicijus
Copy link

@felicijus felicijus commented Oct 15, 2025

Description

  • Extracted VirusTotal functionality into virustotal.ps1 and split logic into smaller functions:
    • Get-VirusTotalApiKey, ConvertTo-VirusTotalUrlId, Get-VirusTotalResultByHash, Get-VirusTotalResultByUrl, Submit-ToVirusTotal, Check-VirusTotalUrl, virustotal_check_app.
    • Added hash_for_url helper to virustotal.ps1 to map manifest URLs to hashes. (need to think about whereto put that - circular import)
  • Integrated VirusTotal pre-checks into download/install/update flows:
    • download.ps1: Invoke-ScoopDownload gains a new parameter to enable VirusTotal pre-checks and will skip downloads from URLs flagged as malicious/suspicious.
    • install.ps1: install_app accepts a new parameter to enable VirusTotal checks and forwards it to Invoke-ScoopDownload.
    • update (libexec/scoop-update.ps1): added support for VirusTotal pre-checks and forwards the option during update/install.
    • Added CLI flags and config support:
    • scoop-install.ps1 / scoop-update.ps1: added -w / --virustotal-check flags.
    • libexec/scoop-config.ps1: documented the USE_VIRUSTOTAL config key.
  • Minor refactors and string/style cleanups (quotes, function naming consistency).

Motivation and Context

Centralize VirusTotal interactions in a reusable library so install/update/download workflows can optionally pre-validate remote files and block downloads flagged as malicious or suspicious.

Look:
image

Relates to virus scanning feature and prior virustotal command refactors.

Closes #6521

How Has This Been Tested?

  • Manual smoke tests:
    • scoop virustotal runs using the new lib.
    • install/update flows with and without -w/--virustotal-check to ensure safe_urls branching and abort-on-fail behavior.
    • Verified abort when no URL passes VirusTotal checks.
    • Verified guidance/error when VIRUSTOTAL_API_KEY is not configured.
  • added hash.ps1 to avoid circular import

Checklist:

  • I have read the Contributing Guide.
  • I have ensured that I am targeting the develop branch.
  • I have updated the documentation accordingly.
  • I have updated the tests accordingly.
  • I have added an entry in the CHANGELOG.

Summary by CodeRabbit

  • New Features

    • Added VirusTotal pre-download scanning with a new --virustotal-check (-w) CLI flag; can be enabled per-run or via config.
    • VirusTotal checks now run during install, download, and update flows; option propagates to installers and dependency installs.
  • Refactor

    • Extracted VirusTotal functionality into a dedicated library and simplified CLI wrappers.
    • Added helper utilities for hashing and remote file metadata used by download/install paths.

@coderabbitai
Copy link

coderabbitai bot commented Oct 15, 2025

Walkthrough

Adds a VirusTotal integration and threads a new check_virustotal flag through install/download/update CLI and library code, introducing helper scripts for hashing and file info and replacing in-file virustotal logic with a reusable lib/virustotal.ps1.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Documented refactor of VirusTotal into a library and the addition of pre-download VirusTotal checks.
CLI & Config
libexec/scoop-config.ps1, libexec/scoop-download.ps1, libexec/scoop-install.ps1, libexec/scoop-update.ps1
Added use_virustotal config option and -w/--virustotal-check CLI flags; parse and propagate check_virustotal through command flows; updated usage/help text.
VirusTotal library
lib/virustotal.ps1
New comprehensive VirusTotal module with exported functions (ConvertTo-VirusTotalUrlId, Get-VirusTotalResultByHash/Url, Submit-ToVirusTotal, Get-VirusTotalApiKey, Check-VirusTotalUrl, virustotal_check_app, Test-UrlsWithVirusTotal) plus rate-limit, retry and error-handling logic.
Download & install pipeline
lib/download.ps1, lib/install.ps1, libexec/scoop-update.ps1
Added check_virustotal parameter to Invoke-ScoopDownload, Invoke-CachedAria2Download, install_app, and update; optionally pre-resolve and filter URLs via VirusTotal before downloading; removed legacy public hash helpers from lib/download.ps1.
Helpers
lib/helper/hash.ps1, lib/helper/file-information.ps1
New helper functions: hash_for_url(manifest, url, arch), get_hash([String] $multihash), and Get-RemoteFileSize($Uri).
Refactors & imports
bin/checkhashes.ps1, lib/autoupdate.ps1, libexec/scoop-virustotal.ps1, libexec/scoop-info.ps1
Added imports for helper scripts (lib/helper/hash.ps1, lib/helper/file-information.ps1); refactored scoop-virustotal to delegate to virustotal_check_app and simplified in-file logic; updated import lists.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as libexec/scoop-<br/>install/download/update
    participant Pipeline as lib/download.ps1<br/>lib/install.ps1
    participant VT as lib/virustotal.ps1
    participant API as VirusTotal API

    User->>CLI: scoop install/download -w<br/>(or USE_VIRUSTOTAL config)
    CLI->>Pipeline: call install_app / Invoke-ScoopDownload<br/>with check_virustotal=true

    alt check_virustotal enabled
        Pipeline->>VT: Test-UrlsWithVirusTotal(urls, manifest, arch)
        VT->>API: Query by URL or hash / Submit for scan
        API-->>VT: Analysis results / status
        VT-->>Pipeline: Filtered safe URLs or report (abort)
    end

    alt URLs safe (or VT disabled)
        Pipeline->>Pipeline: Download file(s) (aria2 or native)
        Pipeline->>Pipeline: Verify hash / continue install
        Pipeline-->>CLI: Success
    else Threat detected
        Pipeline-->>CLI: Abort with warning/report
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested reviewers

  • niheaven

Poem

🐰
I hopped through manifests and logs so deep,
Scanning URLs while others sleep.
With hashes, keys, and VT in tow,
I guard each download as I go.
Safe scoops for all — a rabbit's leap!

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat(virustotal): automatic Virustotal check as option" clearly and concisely describes the main change in the changeset. The title directly references the primary feature being implemented—automatic VirusTotal checks that can be toggled as an option—which is immediately recognizable from the raw summary showing extensive integration of VirusTotal functionality across multiple modules and CLI interfaces. The title is specific enough for a developer scanning commit history to understand the purpose without being overly verbose or generic.
Linked Issues Check ✅ Passed The code changes comprehensively implement the requirements from linked issue #6521. The implementation includes: extraction of VirusTotal functionality into a reusable library (lib/virustotal.ps1) with exported functions for API interactions [#6521]; addition of a configurable USE_VIRUSTOTAL option in libexec/scoop-config.ps1 [#6521]; integration of -w/--virustotal-check CLI flags in scoop-install.ps1 and scoop-update.ps1 [#6521]; threading of VirusTotal checks through download, install, and update flows to pre-validate files before processing [#6521]; and logic to skip or abort when URLs are flagged as malicious or suspicious [#6521]. Supporting refactors like hash.ps1 and file-information.ps1 helpers enable the feature without circular imports.
Out of Scope Changes Check ✅ Passed All changes in the pull request are directly related to implementing the VirusTotal feature or necessary supporting refactors. The modifications span library abstractions (lib/virustotal.ps1, lib/helper/hash.ps1, lib/helper/file-information.ps1), core download/install logic (lib/download.ps1, lib/install.ps1), CLI entry points (scoop-install.ps1, scoop-update.ps1, scoop-download.ps1, scoop-virustotal.ps1), and configuration (scoop-config.ps1). Minor style changes such as quote normalization and function relocations (hash functions moved from download.ps1 to a dedicated helper) are justified refactoring decisions to avoid circular imports and maintain modularity. No unrelated changes are present.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@felicijus felicijus marked this pull request as ready for review October 16, 2025 18:43
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libexec/scoop-virustotal.ps1 (1)

59-74: Ensure _ERR constants and $exit_code are initialized locally.

$_ERR_NO_INFO and $exit_code are still used here, but they are no longer defined in this script after delegating to lib\virustotal.ps1. Unless those variables are exported into the caller’s scope, $exit_code -bor $_ERR_NO_INFO will operate on $null and the final exit $exit_code can exit with $null. Please reintroduce the local initializations (e.g., $exit_code = 0) or import the shared values explicitly before use.

🧹 Nitpick comments (1)
lib/hash.ps1 (1)

16-28: Consider extracting hash types to a constant.

The list of supported hash types @('md5', 'sha1', 'sha256', 'sha512') is hardcoded. Consider extracting this to a module-level or script-level constant to improve maintainability and make it easier to extend support for additional hash algorithms in the future.

Example:

+$script:SupportedHashTypes = @('md5', 'sha1', 'sha256', 'sha512')
+
 function get_hash([String] $multihash) {
     $type, $hash = $multihash -split ':'
     if (!$hash) {
         # no type specified, assume sha256
         $type, $hash = 'sha256', $multihash
     }
 
-    if (@('md5', 'sha1', 'sha256', 'sha512') -notcontains $type) {
+    if ($script:SupportedHashTypes -notcontains $type) {
         return $null, "Hash type '$type' isn't supported."
     }
 
     return $type, $hash.ToLower()
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20a178b and 81531de.

📒 Files selected for processing (12)
  • CHANGELOG.md (1 hunks)
  • bin/checkhashes.ps1 (1 hunks)
  • lib/autoupdate.ps1 (1 hunks)
  • lib/download.ps1 (6 hunks)
  • lib/hash.ps1 (1 hunks)
  • lib/install.ps1 (2 hunks)
  • lib/virustotal.ps1 (1 hunks)
  • libexec/scoop-config.ps1 (1 hunks)
  • libexec/scoop-download.ps1 (1 hunks)
  • libexec/scoop-install.ps1 (3 hunks)
  • libexec/scoop-update.ps1 (8 hunks)
  • libexec/scoop-virustotal.ps1 (3 hunks)
🔇 Additional comments (12)
CHANGELOG.md (1)

7-8: Verify issue reference.

The changelog entry references issue #6525, but according to the PR description, the linked issue is #6521. Please verify which issue number is correct.

lib/hash.ps1 (1)

3-14: LGTM!

The function correctly matches URLs to their corresponding hashes in the manifest. The error handling for missing URLs is appropriate, and the null filtering ensures robustness.

bin/checkhashes.ps1 (1)

45-45: LGTM!

The import correctly exposes the get_hash function from the newly created hash module, supporting the refactoring effort to centralize hash-related utilities.

lib/autoupdate.ps1 (1)

3-4: LGTM!

The import makes the get_hash function available early in the autoupdate execution flow, consistent with the refactoring to centralize hash utilities.

libexec/scoop-config.ps1 (1)

95-97: Clarify timing of VirusTotal checks.

The documentation states "Scoop will always use VirusTotal to scan files after downloading", but based on the PR objectives mentioning "pre-download checks" and the code showing check_virustotal passed to download functions, it appears the checks may occur before or during download, not after. Please verify and update the documentation to accurately reflect when VirusTotal validation occurs.

libexec/scoop-download.ps1 (1)

24-24: LGTM!

The import correctly exposes hash_for_url functionality needed for URL hash verification in the download flow.

lib/install.ps1 (2)

9-9: LGTM!

The function signature correctly extends install_app with the new $check_virustotal parameter, maintaining backward compatibility with a sensible default of $false.


53-53: LGTM!

The parameter is correctly threaded through to Invoke-ScoopDownload, enabling VirusTotal checks during the download phase when requested.

libexec/scoop-install.ps1 (4)

27-27: LGTM!

The help text correctly documents the new VirusTotal check option with appropriate warning about potential performance impact ("may be slow").


47-47: LGTM!

The getopt call correctly recognizes both short (-w) and long (--virustotal-check) option formats for the new VirusTotal check feature.


52-52: LGTM!

The flag computation correctly prioritizes the CLI option over the configuration value, providing flexible control over VirusTotal checks per invocation or globally via config.


137-137: LGTM!

The check_virustotal parameter is correctly passed to install_app, completing the integration of VirusTotal checks into the installation flow.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
lib/virustotal.ps1 (2)

4-13: Consider exposing the exit code for command-line usage.

The script-scoped $exit_code (line 13) is updated by helper functions but remains invisible to callers. While this doesn't affect the Test-UrlsWithVirusTotal workflow (which aborts directly), it may prevent scoop virustotal command from exiting with the correct code.

Consider one of these approaches:

  1. Return the exit code from virustotal_check_app so callers can use it
  2. Expose it via $global:exit_code if multiple components need to read it
  3. Document that Test-UrlsWithVirusTotal is abort-only and exit codes are for other consumers

Option 1 (preferred for scoop virustotal command):

 function virustotal_check_app($app, $manifest, $architecture, $api_key, $scan) {
     [int]$index = 0
     $urls = script:url $manifest $architecture
     $urls | ForEach-Object {
         $url = $_
         $index++
         if ($urls.GetType().IsArray) {
             info "$app`: url $index"
         }
         $hash = hash_for_url $manifest $url $architecture
         Check-VirusTotalUrl $app $url $hash $api_key $scan
     }
+    return $script:exit_code
 }

Callers would then use:

$exitCode = virustotal_check_app $app $manifest $architecture $api_key $scan
exit $exitCode

62-82: Consider separating counts from engine lists for clarity.

The report properties 'FileReport.Malicious' and 'FileReport.Suspicious' contain either an integer 0 (no detections) or an array of engine names (when detections exist). This type mixing works with the current -eq 0 comparison in Test-UrlsWithVirusTotal (line 334) but is fragile and may confuse consumers expecting consistent types.

Consider adding separate count properties for clearer contracts:

         [PSCustomObject] @{
             'App.Name'              = $app
             'App.Url'               = $url
             'App.Hash'              = $hash
             'App.HashType'          = $null
             'App.Size'              = filesize $fileSize
             'FileReport.Url'        = $report_url
             'FileReport.Hash'       = $report_hash
-            'FileReport.Malicious'  = if ($maliciousResults) { $maliciousResults } else { 0 }
-            'FileReport.Suspicious' = if ($suspiciousResults) { $suspiciousResults } else { 0 }
+            'FileReport.Malicious'  = if ($maliciousResults) { $maliciousResults } else { @() }
+            'FileReport.Suspicious' = if ($suspiciousResults) { $suspiciousResults } else { @() }
+            'FileReport.MaliciousCount'  = $malicious
+            'FileReport.SuspiciousCount' = $suspicious
             'FileReport.Timeout'    = $timeout
             'FileReport.Undetected' = $undetected
             'UrlReport.Url'         = $null
         }

Then update the comparison to use counts:

-            if ($maliciousResults -eq 0 -and $suspiciousResults -eq 0) {
+            if ($file_report.'FileReport.MaliciousCount' -eq 0 -and $file_report.'FileReport.SuspiciousCount' -eq 0) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8003ac and f143dad.

📒 Files selected for processing (4)
  • lib/download.ps1 (6 hunks)
  • lib/virustotal.ps1 (1 hunks)
  • libexec/scoop-download.ps1 (3 hunks)
  • libexec/scoop-update.ps1 (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libexec/scoop-update.ps1
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-16T19:13:23.445Z
Learnt from: felicijus
PR: ScoopInstaller/Scoop#6525
File: lib/virustotal.ps1:215-221
Timestamp: 2025-10-16T19:13:23.445Z
Learning: In PowerShell, `-inotin` is a valid case-insensitive containment comparison operator that checks if a value is not contained in a collection. The operator family includes `-notin` (case-sensitive), `-inotin` (case-insensitive), and `-cnotin` (case-sensitive with culture-aware comparison).

Applied to files:

  • lib/virustotal.ps1
🔇 Additional comments (4)
lib/download.ps1 (2)

345-349: LGTM: aria2 path now correctly applies VT filtering.

The aria2 download path now filters URLs through Test-UrlsWithVirusTotal when $check_virustotal is enabled, preventing malicious URLs from being downloaded. This resolves the previous concern about unsafe URLs bypassing checks in the aria2 flow.


707-716: No issues found; regex changes are safe and protective.

The regex pattern expansion from [._-] to [\w._-] is additive and doesn't break filename extraction. The change allows capturing filenames with letters and digits in query parameters (e.g., ?var=value), and the fallback logic at lines 709–714 protects against over-extraction by reverting to AbsolutePath or Fragment when needed. Test cases validate all edge cases: plain URLs, query parameters, and fragment overrides. The change improves robustness without introducing breaking changes.

lib/virustotal.ps1 (1)

319-348: LGTM: Pre-download VT filtering is well-designed.

The Test-UrlsWithVirusTotal function correctly:

  • Retrieves the API key only when VT checks are enabled
  • Iterates through all URLs and checks each one
  • Collects only safe URLs (those with 0 malicious and 0 suspicious detections)
  • Aborts before download if no URLs pass validation

This provides strong protection against malicious downloads.

libexec/scoop-download.ps1 (1)

109-113: No issue found—Test-UrlsWithVirusTotal correctly aborts when filtering returns no safe URLs.

The verification confirms that Test-UrlsWithVirusTotal (lib/virustotal.ps1:343-345) aborts with a clear error message if no URLs pass the VirusTotal check. The foreach loop at libexec/scoop-download.ps1:115 will never receive an empty array because the script terminates before that point. The code handles this scenario correctly and safely.

…picious counts, undetected, unsafe and total
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (1)
lib/virustotal.ps1 (1)

5-13: Script-scoped exit code remains invisible to callers.

This issue was flagged in a previous review and is still unresolved. Callers of functions in this module cannot read $script:exit_code because script scope is private. Consider returning the exit code from virustotal_check_app or using $global:exit_code instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15d99e7 and 4b9e3f1.

📒 Files selected for processing (1)
  • lib/virustotal.ps1 (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-16T19:13:23.481Z
Learnt from: felicijus
PR: ScoopInstaller/Scoop#6525
File: lib/virustotal.ps1:215-221
Timestamp: 2025-10-16T19:13:23.481Z
Learning: In PowerShell, `-inotin` is a valid case-insensitive containment comparison operator that checks if a value is not contained in a collection. The operator family includes `-notin` (case-sensitive), `-inotin` (case-insensitive), and `-cnotin` (case-sensitive with culture-aware comparison).

Applied to files:

  • lib/virustotal.ps1
🔇 Additional comments (5)
lib/virustotal.ps1 (5)

15-21: LGTM!

The URL-to-ID conversion correctly implements VirusTotal's base64url encoding (RFC 4648 §5).


92-134: LGTM!

The URL report retrieval logic correctly handles both completed analyses and in-progress scans.


147-202: LGTM!

The submission logic with single-retry rate-limit handling is appropriate. The $retrying parameter prevents infinite recursion.


256-263: Remove the review comment - no scope issue exists.

The $matches automatic variable from the regex match at line 218 correctly persists to line 259. PowerShell's try-catch blocks do not create new variable scopes (only function calls do), and neither Get-VirusTotalResultByUrl nor Get-VirusTotalResultByHash perform -match operations that would overwrite $matches. The variable remains accessible in the nested try block as intended.


310-310: Disregard this review comment.

The use of script:url at line 310 is correct. The architecture uses dot-sourcing where the entry point (./libexec/scoop-virustotal.ps1) imports manifest.ps1 into the script scope, making the url function available to virustotal.ps1 and other library files via the script: scope prefix. This pattern is consistently used in lib/hash.ps1 as well, confirming it's intentional design rather than an error.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (7)
lib/virustotal.ps1 (7)

5-14: Script-scoped exit code remains invisible to callers.

This is the same issue flagged in previous reviews. All public helpers update $script:exit_code, but consumers (e.g., scoop-virustotal.ps1) cannot access it because script scope is private. Either expose it globally or return the exit code from virustotal_check_app.


88-88: Incorrect variable reference.

This issue from previous reviews is still present. The line uses $exit_code instead of $script:exit_code on the right-hand side.

Apply this diff:

-        $Script:exit_code = $exit_code -bor $script:_ERR_UNSAFE
+        $script:exit_code = $script:exit_code -bor $script:_ERR_UNSAFE

209-209: Incorrect variable reference.

This issue from previous reviews is still present. The line uses $_ERR_NO_API_KEY instead of $script:_ERR_NO_API_KEY.

Apply this diff:

-            "  scoop config virustotal_api_key <API key>") $_ERR_NO_API_KEY
+            "  scoop config virustotal_api_key <API key>") $script:_ERR_NO_API_KEY

239-239: Incorrect variable reference.

This issue from previous reviews is still present. The line uses $exit_code instead of $script:exit_code on the right-hand side.

Apply this diff:

-        $script:exit_code = $exit_code -bor $script:_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION

273-273: Incorrect variable reference.

This issue from previous reviews is still present. The line uses $exit_code instead of $script:exit_code on the right-hand side.

Apply this diff:

-        $script:exit_code = $exit_code -bor $script:_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION

294-294: Incorrect variable reference.

This issue from previous reviews is still present. The line uses $exit_code instead of $script:exit_code on the right-hand side.

Apply this diff:

-        $script:exit_code = $exit_code -bor $script:_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION

335-340: Add null safety check for FileReport.Unsafe.

This issue from previous reviews is still valid. The code assumes FileReport.Unsafe always exists and is numeric, but if the report structure varies or the check fails, this property might be null or missing.

Apply this diff:

-            if ($file_report.'FileReport.Unsafe' -eq 0) {
+            if ($null -ne $file_report.'FileReport.Unsafe' -and $file_report.'FileReport.Unsafe' -eq 0) {
                 info "$app`: Safe URL: $url"
                 $safe_urls += $url
             } else {
🧹 Nitpick comments (1)
lib/virustotal.ps1 (1)

323-349: Consider handling null/empty reports more explicitly.

When Check-VirusTotalUrl encounters errors (lines 248, 276, 282, 303), it returns nothing. The ForEach-Object on line 331 safely handles this (no-op), but you might want to track failed checks separately to provide clearer error messages to users about which URLs failed vs which were unsafe.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b9e3f1 and 9784acd.

📒 Files selected for processing (1)
  • lib/virustotal.ps1 (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-16T19:13:23.481Z
Learnt from: felicijus
PR: ScoopInstaller/Scoop#6525
File: lib/virustotal.ps1:215-221
Timestamp: 2025-10-16T19:13:23.481Z
Learning: In PowerShell, `-inotin` is a valid case-insensitive containment comparison operator that checks if a value is not contained in a collection. The operator family includes `-notin` (case-sensitive), `-inotin` (case-insensitive), and `-cnotin` (case-sensitive with culture-aware comparison).

Applied to files:

  • lib/virustotal.ps1
🔇 Additional comments (3)
lib/virustotal.ps1 (3)

15-21: LGTM!

The base64-url encoding logic is correct and follows the standard pattern for VirusTotal URL IDs.


92-134: LGTM!

The URL report query logic correctly handles cases where the hash is not available and provides appropriate user feedback.


310-310: ****

The script:url function is properly available. The entry point scoop-virustotal.ps1 explicitly dot-sources lib/manifest.ps1 (which defines the url function) before loading lib/virustotal.ps1, making the function accessible in the script scope at runtime. This is a standard module loading pattern consistently used throughout the codebase.

Likely an incorrect or invalid review comment.

if ($url_report.'UrlReport.Hash' -and ($file_report_not_found -eq $true) -and $hash) {
try {
$file_report = Get-VirusTotalResultByHash $url_report.'UrlReport.Hash' $url $app $api_key
if ($file_report.'FileReport.Hash' -ieq $matches['hash']) {
Copy link

@coderabbitai coderabbitai bot Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Incorrect hash comparison.

The code compares against $matches['hash'], but $matches was populated from the regex match at line 218 and may be out of scope or overwritten by nested try/catch blocks. Use the local $hash variable instead.

Apply this diff:

-                if ($file_report.'FileReport.Hash' -ieq $matches['hash']) {
+                if ($file_report.'FileReport.Hash' -ieq $hash) {
🤖 Prompt for AI Agents
In lib/virustotal.ps1 around line 259, the comparison uses $matches['hash']
which may be out of scope or overwritten; replace that usage with the local
$hash variable (the one populated from the regex at line 218) so the file report
is compared to the intended hash. Ensure the $hash variable is in scope at this
location and update the if condition to compare $file_report.'FileReport.Hash'
-ieq $hash.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scoop since this code was just copied to the lib\virustotal.ps1.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felicijus, understood! Since this is a refactoring PR and the code was copied as-is, I can see why fixing this would be out of scope.

Would you like me to open a follow-up issue to track this potential bug with the $matches['hash'] comparison? That way it can be addressed in a separate PR focused on bug fixes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lib/download.ps1 (1)

685-690: Consider removing the commented-out code.

Since Get-RemoteFileSize has been moved to lib/helper/file-information.ps1 and is now imported (line 5), the commented-out function definition is redundant and can be removed to reduce clutter.

Apply this diff:

-# function Get-RemoteFileSize ($Uri) {
-#     $response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
-#     if (!$response.Headers.StatusCode) {
-#         $response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
-#     }
-# }
-
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9784acd and 6da7522.

📒 Files selected for processing (9)
  • bin/checkhashes.ps1 (1 hunks)
  • lib/autoupdate.ps1 (1 hunks)
  • lib/download.ps1 (6 hunks)
  • lib/helper/file-information.ps1 (1 hunks)
  • lib/helper/hash.ps1 (1 hunks)
  • lib/virustotal.ps1 (1 hunks)
  • libexec/scoop-download.ps1 (3 hunks)
  • libexec/scoop-info.ps1 (1 hunks)
  • libexec/scoop-update.ps1 (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • libexec/scoop-info.ps1
🚧 Files skipped from review as they are similar to previous changes (2)
  • bin/checkhashes.ps1
  • lib/virustotal.ps1
🔇 Additional comments (20)
lib/helper/hash.ps1 (2)

3-14: LGTM!

The function correctly retrieves the hash for a given URL by matching indices between the URL and hash arrays from the manifest. The abort on missing URL is appropriate for catching manifest inconsistencies.


16-28: LGTM!

The function correctly parses multihash strings, defaults to SHA256 when no type is specified, validates against supported algorithms, and returns a consistent tuple format for success/failure cases.

lib/autoupdate.ps1 (1)

3-4: LGTM!

The import correctly sources the new centralized hash helper, enabling reuse of get_hash across multiple modules.

libexec/scoop-download.ps1 (5)

21-21: LGTM!

The help text clearly documents the new VirusTotal option and appropriately warns users about potential performance impact.


25-25: LGTM!

The import provides access to the centralized hash_for_url function used in the download flow.


35-41: LGTM!

The option parsing correctly handles both CLI flags and config-based defaults, with appropriate precedence (CLI > config > false).


107-107: LGTM!

Correctly propagates the VirusTotal check flag to the aria2 download path.


109-145: LGTM!

The conditional URL filtering correctly applies VirusTotal checks only when enabled, allowing the download flow to skip flagged URLs while preserving existing behavior when the feature is disabled.

lib/download.ps1 (5)

3-6: LGTM!

The imports correctly provide access to the refactored hash helpers, file information utilities, and new VirusTotal functionality.


10-60: LGTM!

The VirusTotal integration is correctly implemented with backward-compatible defaults, proper flag propagation to both aria2 and non-aria2 paths, and conditional URL filtering that preserves existing behavior when disabled.


343-539: LGTM!

The aria2 download function correctly applies VirusTotal filtering before building the download queue, ensuring that flagged URLs are never passed to aria2 when checks are enabled.


476-478: LGTM!

The quote style updates align with PowerShell best practices (preferring single quotes when no interpolation is needed).


698-718: LGTM!

The refinements to url_remote_filename improve URL parsing logic for edge cases involving query parameters, version-like filenames, and URL fragments.

libexec/scoop-update.ps1 (7)

14-14: LGTM!

The help text clearly documents the new VirusTotal option with appropriate performance warning.


25-25: LGTM!

The hash helper import provides access to hash_for_url used in the update flow.


34-43: LGTM!

Option parsing correctly handles the VirusTotal flag with proper precedence (CLI flag > config > default false).


264-264: LGTM!

The function signature update maintains backward compatibility with a sensible default.


306-344: LGTM!

The update flow correctly integrates VirusTotal checking, with proper URL filtering in both aria2 and non-aria2 paths, ensuring flagged URLs are excluded from downloads when checks are enabled.


390-396: LGTM!

The VirusTotal flag is correctly propagated to all installation paths, ensuring both the main app and dependencies are checked when enabled.


476-476: LGTM!

The flag is correctly passed from the CLI options to each individual update invocation, completing the integration.

Comment on lines +3 to +8
function Get-RemoteFileSize ($Uri) {
$response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
if (!$response.Headers.StatusCode) {
$response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the StatusCode check logic.

The condition on line 5 checks !$response.Headers.StatusCode, but StatusCode is not a header—it's a property of the response object itself ($response.StatusCode). This check will likely always evaluate to true (since Headers.StatusCode doesn't exist), causing the function to return Content-Length unconditionally.

Apply this diff to fix the logic:

 function Get-RemoteFileSize ($Uri) {
     $response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
-    if (!$response.Headers.StatusCode) {
+    if ($response.StatusCode -eq 200) {
         $response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function Get-RemoteFileSize ($Uri) {
$response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
if (!$response.Headers.StatusCode) {
$response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
}
}
function Get-RemoteFileSize ($Uri) {
$response = Invoke-WebRequest -Uri $Uri -Method HEAD -UseBasicParsing
if ($response.StatusCode -eq 200) {
$response.Headers.'Content-Length' | ForEach-Object { [int]$_ }
}
}
🤖 Prompt for AI Agents
In lib/helper/file-information.ps1 around lines 3 to 8, the condition
incorrectly checks $response.Headers.StatusCode (which doesn't exist) causing
the Content-Length to be returned unconditionally; update the check to inspect
the response.StatusCode property instead (for example ensure
$response.StatusCode -ge 200 -and $response.StatusCode -lt 300 or -eq 200) and
only read and cast $response.Headers.'Content-Length' to int when the status
code indicates success.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (6)
lib/virustotal.ps1 (6)

259-263: Compare to $hash, not $matches['hash'].

$matches may be stale/out of scope. Use the resolved $hash variable.

-                if ($file_report.'FileReport.Hash' -ieq $matches['hash']) {
+                if ($file_report.'FileReport.Hash' -ieq $hash) {

88-90: Fix exit-code accumulation scope.

RHS uses $exit_code (undefined/outer). Use the script-scoped variable.

-    if ($unsafe -gt 0) {
-        $Script:exit_code = $exit_code -bor $script:_ERR_UNSAFE
-    }
+    if ($unsafe -gt 0) {
+        $script:exit_code = $script:exit_code -bor $script:_ERR_UNSAFE
+    }

205-213: Wrong variable in abort: use script-scoped error code.

$_ERR_NO_API_KEY$script:_ERR_NO_API_KEY.

-    if (!$api_key) {
-        abort ("VirusTotal API key is not configured`n" +
-            "  You could get one from https://www.virustotal.com/gui/my-apikey and set with`n" +
-            "  scoop config virustotal_api_key <API key>") $_ERR_NO_API_KEY
-    }
+    if (!$api_key) {
+        abort ("VirusTotal API key is not configured`n" +
+            "  You could get one from https://www.virustotal.com/gui/my-apikey and set with`n" +
+            "  scoop config virustotal_api_key <API key>") $script:_ERR_NO_API_KEY
+    }

295-299: Fix exit-code scope in exception path (again).

-        $script:exit_code = $exit_code -bor $script:_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION

240-241: Fix exit-code scope in exception path.

-        $script:exit_code = $exit_code -bor $script:_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION

274-275: Fix exit-code scope in URL exception path.

-        $script:exit_code = $exit_code -bor $script:_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION
🧹 Nitpick comments (8)
lib/helper/hash.ps1 (2)

3-14: Guard against URL/hash index mismatch.

If URLs and hashes get out of sync, indexing may throw. Add a bounds check and clearer error.

 function hash_for_url($manifest, $url, $arch) {
@@
-    $index = [array]::IndexOf($urls, $url)
-    if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
-
-    @($hashes)[$index]
+    $index = [array]::IndexOf($urls, $url)
+    if ($index -eq -1) { abort "Couldn't find hash in manifest for '$url'." }
+    if ($index -ge $hashes.Length) { abort "Hash entry missing for URL index $index in manifest." }
+    @($hashes)[$index]
 }

16-28: Validate hex length per algorithm (optional).

Add simple length checks to catch malformed hashes early.

 function get_hash([String] $multihash) {
@@
-    return $type, $hash.ToLower()
+    $hash = $hash.ToLower()
+    $expectedLen = switch ($type) {
+        'md5'    { 32 }
+        'sha1'   { 40 }
+        'sha256' { 64 }
+        'sha512' { 128 }
+    }
+    if ($hash -notmatch '^[0-9a-f]+$' -or $hash.Length -ne $expectedLen) {
+        return $null, "Invalid $type hash format."
+    }
+    return $type, $hash
 }
libexec/scoop-download.ps1 (1)

106-108: Pass $bucket through aria2 path (avoid dynamic scope).

Invoke-CachedAria2Download uses $bucket for messages but doesn’t accept it. Thread it explicitly to avoid ambient variable reliance.

-        Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $use_cache $curr_check_hash $check_virustotal
+        Invoke-CachedAria2Download $app $version $manifest $bucket $architecture $cachedir $manifest.cookie $use_cache $curr_check_hash $check_virustotal

You’ll need the matching signature change in lib/download.ps1 (see comment there).

lib/download.ps1 (4)

20-28: Optional: Lazy-load VT module only when enabled.

Dot-sourcing virustotal.ps1 always loads VT helpers. Consider importing only when $check_virustotal is true to reduce startup overhead.


22-22: Pass $bucket into aria2 helper.

Signature currently omits $bucket but uses it internally. Add it to avoid dynamic scope leakage.

-        Invoke-CachedAria2Download $app $version $manifest $architecture $dir $cookies $use_cache $check_hash $check_virustotal
+        Invoke-CachedAria2Download $app $version $manifest $bucket $architecture $dir $cookies $use_cache $check_hash $check_virustotal

343-351: Add $bucket parameter to aria2 helper to match usage.

The function references $bucket in messages but doesn’t accept it.

-function Invoke-CachedAria2Download ($app, $version, $manifest, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true, $check_virustotal = $false) {
+function Invoke-CachedAria2Download ($app, $version, $manifest, $bucket, $architecture, $dir, $cookies = $null, $use_cache = $true, $check_hash = $true, $check_virustotal = $false) {

Also update both call sites (in this file and libexec/scoop-download.ps1) as shown in adjacent comments.


714-747: check_hash relies on ambient $url.

Passing $url explicitly would de-couple this helper from caller context and fix misleading output when $url isn’t in scope.

-function check_hash($file, $hash, $app_name) {
+function check_hash($file, $hash, $app_name, $url) {
@@
-    Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
+    Write-Host $(url_remote_filename $url) -ForegroundColor Cyan -NoNewline
@@
-        $msg += "URL:         $url`n"
+        $msg += "URL:         $url`n"

And pass $url at call sites:

  • Invoke-ScoopDownload: $ok, $err = check_hash "$dir\$fname" $manifest_hash $(show_app $app $bucket) $url
  • Invoke-CachedAria2Download: $ok, $err = check_hash $data.$url.source $manifest_hash $(show_app $app $bucket) $url
lib/virustotal.ps1 (1)

336-341: Null‑safety for FileReport.Unsafe.

When only a URL report is available, this property can be null. Guard the comparison.

-            if ($file_report.'FileReport.Unsafe' -eq 0) {
+            if ($null -ne $file_report.'FileReport.Unsafe' -and [int]$file_report.'FileReport.Unsafe' -eq 0) {
                 info "$app`: Safe URL: $url"
                 $safe_urls += $url
             } else {
                 warn "$app`: Unsafe URL: $url"
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6da7522 and e87839e.

📒 Files selected for processing (9)
  • bin/checkhashes.ps1 (1 hunks)
  • lib/autoupdate.ps1 (1 hunks)
  • lib/download.ps1 (5 hunks)
  • lib/helper/file-information.ps1 (1 hunks)
  • lib/helper/hash.ps1 (1 hunks)
  • lib/virustotal.ps1 (1 hunks)
  • libexec/scoop-download.ps1 (3 hunks)
  • libexec/scoop-info.ps1 (1 hunks)
  • libexec/scoop-update.ps1 (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • lib/helper/file-information.ps1
  • lib/autoupdate.ps1
  • libexec/scoop-info.ps1
  • libexec/scoop-update.ps1
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-16T19:13:23.481Z
Learnt from: felicijus
PR: ScoopInstaller/Scoop#6525
File: lib/virustotal.ps1:215-221
Timestamp: 2025-10-16T19:13:23.481Z
Learning: In PowerShell, `-inotin` is a valid case-insensitive containment comparison operator that checks if a value is not contained in a collection. The operator family includes `-notin` (case-sensitive), `-inotin` (case-insensitive), and `-cnotin` (case-sensitive with culture-aware comparison).

Applied to files:

  • lib/virustotal.ps1
🔇 Additional comments (2)
bin/checkhashes.ps1 (1)

45-45: Import looks good.

Dot-sourcing the new hash helper is correct and aligns with later get_hash usage.

lib/download.ps1 (1)

21-26: Thread VT filtering in meta downloader correctly.

VT filtering is good. Ensure $urls is built before filtering (already present here). No action needed.

Comment on lines +287 to +294
try {
$file_report = Get-VirusTotalResultByHash $url_report.'UrlReport.Hash' $url $app $api_key
$file_report.'App.Hash' = $hash
$file_report.'App.HashType' = $algo
$file_report.'UrlReport.Url' = $url_report.'UrlReport.Url'
$file_report
warn "$app`: Unable to check hash match for $url"
} catch [Exception] {
Copy link

@coderabbitai coderabbitai bot Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Spurious warning always prints.

This warn executes even on success right after outputting $file_report. Remove it or guard it.

-        $file_report
-        warn "$app`: Unable to check hash match for $url"
+        $file_report
+        # (optional) add warn only when a mismatch condition is detected elsewhere
🤖 Prompt for AI Agents
In lib/virustotal.ps1 around lines 287 to 294, the warn statement is executed
unconditionally even when Get-VirusTotalResultByHash succeeds; remove the
unconditional warn or move/guard it so it only runs when an actual hash mismatch
or error occurs (e.g., perform the hash match check after receiving $file_report
and only call warn if the hashes differ, or else move the warn into the catch
block to run on exceptions).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scoop since this code was just copied to the lib\virustotal.ps1.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felicijus, understood! Since this is pre-existing code being extracted, it makes sense to keep this PR focused on the refactoring work.

Would you like me to create a follow-up issue to track fixing this spurious warning? That way it won't be forgotten and can be addressed in a future PR dedicated to fixing the logic.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
lib/virustotal.ps1 (3)

6-9: Fix exit code scoping and make it observable.

Assignments inside functions create locals; the top-level $exit_code isn’t updated. Also, error-code constants should be script-scoped for consistency. Update scoping and usage.

Apply:

-$_ERR_UNSAFE = 2
-$_ERR_EXCEPTION = 4
-$_ERR_NO_INFO = 8
-$_ERR_NO_API_KEY = 16
+$script:_ERR_UNSAFE    = 2
+$script:_ERR_EXCEPTION = 4
+$script:_ERR_NO_INFO   = 8
+$script:_ERR_NO_API_KEY = 16

-$exit_code = 0
+$script:exit_code = 0
-    if ($unsafe -gt 0) {
-        $exit_code = $exit_code -bor $_ERR_UNSAFE
-    }
+    if ($unsafe -gt 0) {
+        $script:exit_code = $script:exit_code -bor $script:_ERR_UNSAFE
+    }
-        abort ("VirusTotal API key is not configured`n" +
-            "  You could get one from https://www.virustotal.com/gui/my-apikey and set with`n" +
-            "  scoop config virustotal_api_key <API key>") $_ERR_NO_API_KEY
+        abort ("VirusTotal API key is not configured`n" +
+            "  You could get one from https://www.virustotal.com/gui/my-apikey and set with`n" +
+            "  scoop config virustotal_api_key <API key>") $script:_ERR_NO_API_KEY
-        $exit_code = $exit_code -bor $_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION
-        $exit_code = $exit_code -bor $_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION
-        $exit_code = $exit_code -bor $_ERR_EXCEPTION
+        $script:exit_code = $script:exit_code -bor $script:_ERR_EXCEPTION

Optionally expose a getter so callers can read the final code:

function Get-VirusTotalExitCode { $script:exit_code }

Also applies to: 14-15, 88-91, 210-211, 240-241, 274-275, 295-296


259-263: Compare to $hash, not $matches['hash'].

$matches may be stale or unset; use the normalized $hash variable.

Apply:

-                if ($file_report.'FileReport.Hash' -ieq $matches['hash']) {
+                if ($file_report.'FileReport.Hash' -ieq $hash) {

292-294: Remove/guard spurious warning.

warn runs unconditionally even on success, misleading users.

Apply:

-        $file_report
-        warn "$app`: Unable to check hash match for $url"
+        $file_report
+        # warn only when a mismatch is detected elsewhere
🧹 Nitpick comments (2)
lib/virustotal.ps1 (2)

189-195: Optional: add jitter and a cap to backoff.

Simple linear sleep can herd; add jitter and cap to improve throughput under rate limits.

Example:

-            Start-Sleep -s (60 + $script:requests)
+            $base = 45
+            $jitter = Get-Random -Minimum 5 -Maximum 20
+            $delay = [Math]::Min(120, $base + $script:requests + $jitter)
+            Start-Sleep -Seconds $delay

31-33: Optional: set timeouts on HTTP calls.

Prevent hangs due to stalled connections.

Apply:

-    $response = Invoke-WebRequest -Uri $api_url -Method GET -Headers $headers -UseBasicParsing
+    $response = Invoke-WebRequest -Uri $api_url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 60
-    $response = Invoke-WebRequest -Uri $api_url -Method GET -Headers $headers -UseBasicParsing
+    $response = Invoke-WebRequest -Uri $api_url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 60
-        $response = Invoke-WebRequest -Uri $api_url -Method POST -Headers $headers -ContentType $content_type -Body $body -UseBasicParsing
+        $response = Invoke-WebRequest -Uri $api_url -Method POST -Headers $headers -ContentType $content_type -Body $body -UseBasicParsing -TimeoutSec 60

Also applies to: 100-101, 166-167

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e87839e and ba8bb50.

📒 Files selected for processing (2)
  • lib/virustotal.ps1 (1 hunks)
  • libexec/scoop-download.ps1 (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libexec/scoop-download.ps1
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-16T19:13:23.481Z
Learnt from: felicijus
PR: ScoopInstaller/Scoop#6525
File: lib/virustotal.ps1:215-221
Timestamp: 2025-10-16T19:13:23.481Z
Learning: In PowerShell, `-inotin` is a valid case-insensitive containment comparison operator that checks if a value is not contained in a collection. The operator family includes `-notin` (case-sensitive), `-inotin` (case-insensitive), and `-cnotin` (case-sensitive with culture-aware comparison).

Applied to files:

  • lib/virustotal.ps1
📚 Learning: 2025-08-31T01:39:35.684Z
Learnt from: z-Fng
PR: ScoopInstaller/Scoop#6473
File: libexec/scoop-download.ps1:135-135
Timestamp: 2025-08-31T01:39:35.684Z
Learning: In the Scoop codebase, `Invoke-ScoopDownload` in lib/download.ps1 calls `abort` when hash verification fails, which terminates script execution completely rather than trying remaining mirrors. The scoop-download.ps1 script should maintain consistency with this behavior by using `continue app_loop` to skip to the next app rather than trying remaining mirrors for the current app.

Applied to files:

  • lib/virustotal.ps1
🔇 Additional comments (3)
lib/virustotal.ps1 (3)

16-22: LGTM: stable URL ID encoding.

URL-safe Base64 conversion looks correct.


69-86: Good: preserve numeric counts and add engine lists.

Keeps Malicious/Suspicious totals while exposing engine names; avoids downstream breakage.


345-347: LGTM: abort only when no URLs pass.

Matches PR objective to proceed when at least one URL is safe.

Comment on lines +165 to +170
$body = "url=$encoded_url"
$result = Invoke-WebRequest -Uri $api_url -Method POST -Headers $headers -ContentType $content_type -Body $body -UseBasicParsing
if ($result.StatusCode -eq 200) {
$id = ((json_path $result '$.data.id') -split '-')[1]
$url_report_url = "https://www.virustotal.com/gui/url/$id"
$fileSize = Get-RemoteFileSize $url
Copy link

@coderabbitai coderabbitai bot Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Parse JSON from the response content, not the WebResponse object.

POST branch passes the IWR response object to json_path; use .Content.

Apply:

-        $result = Invoke-WebRequest -Uri $api_url -Method POST -Headers $headers -ContentType $content_type -Body $body -UseBasicParsing
-        if ($result.StatusCode -eq 200) {
-            $id = ((json_path $result '$.data.id') -split '-')[1]
+        $response = Invoke-WebRequest -Uri $api_url -Method POST -Headers $headers -ContentType $content_type -Body $body -UseBasicParsing
+        if ($response.StatusCode -eq 200) {
+            $json = $response.Content
+            $id = ((json_path $json '$.data.id') -split '-')[1]
🤖 Prompt for AI Agents
In lib/virustotal.ps1 around lines 165 to 170, the code passes the full
Invoke-WebRequest response object to json_path; update the call to use the
response body string (e.g. $result.Content) so json_path parses the JSON text,
e.g. replace json_path $result '$.data.id' with json_path $result.Content
'$.data.id' (or pipe $result.Content | ConvertFrom-Json and extract the field)
and keep the rest of the logic unchanged.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scoop since this code was just copied to the lib\virustotal.ps1.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felicijus, understood! Since this is a refactor/extraction PR and the code was copied as-is, it makes sense to keep the scope focused. The inconsistency can be addressed in a follow-up if needed. Thanks for clarifying!

Comment on lines +241 to +249
if ($_.Exception.Response.StatusCode -eq 404) {
$file_report_not_found = $true
warn "$app`: File report not found. Will search by url instead."
} else {
warn "$app`: VirusTotal file report query failed`: $($_.Exception.Message)"
if ($_.Exception.Response) {
warn "`tAPI returned $($_.Exception.Response.StatusCode)"
}
return
Copy link

@coderabbitai coderabbitai bot Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against null Exception.Response before reading StatusCode.

Accessing $_.Exception.Response.StatusCode can NPE when Response is null.

Apply:

-        if ($_.Exception.Response.StatusCode -eq 404) {
+        if ($_.Exception.Response -and $(_.Exception.Response.StatusCode) -eq 404) {

and:

-            if ($_.Exception.Response) {
+            if ($_.Exception.Response) {
                 warn "`tAPI returned $($_.Exception.Response.StatusCode)"
             }

Repeat the same pattern in the later catch block.

Also applies to: 275-283

🤖 Prompt for AI Agents
In lib/virustotal.ps1 around lines 241 to 249, the catch block reads
$_.Exception.Response.StatusCode without checking if Response is $null which can
cause a null reference; update the condition to first test that
$_.Exception.Response is not $null (e.g. use -and or a nested if) before
accessing StatusCode and adjust logging accordingly (check Response exists, then
check StatusCode==404, else log message and, if Response exists, log its
StatusCode); apply the identical null-guard pattern to the later catch block
around lines 275 to 283.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scoop since this code was just copied to the lib\virustotal.ps1.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felicijus Understood! Since this is refactored code and the issue pre-dates this PR, it makes sense to keep the scope focused on the extraction itself.

If you'd like to track this null-guard improvement for a future fix, I can open a follow-up issue for you. Just let me know!

@felicijus
Copy link
Author

felicijus commented Oct 19, 2025

✅ Done for now, not sure about testing since there are currently non for the Virustotal parts.
But all current test are still successful.

The other Issues still unresolved would be other bug fixes I guess... To keep this PR at least a bit contained.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant