From a607410e806bdbebe2391056d7dbba04436831fa Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Thu, 25 Jun 2026 21:17:45 +0100 Subject: [PATCH 1/9] PS: Add query for CWE-494. --- .../cwe-494/DownloadWithoutIntegrityCheck.ql | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql new file mode 100644 index 000000000000..8fe8962d0ede --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql @@ -0,0 +1,76 @@ +/** + * @name Unvalidated Artifact Download + * @description Download of artifact without integrity check. + * @kind problem + * @problem.severity warning + * @security-severity 7.5 + * @precision high + * @id powershell/download-without-integrity-check + * @tags security + * external/cwe/cwe-494 + * external/cwe/cwe-829 + */ + +import powershell +import semmle.code.powershell.dataflow.DataFlow +import semmle.code.powershell.dataflow.TaintTracking + +predicate invokeGitHub(DataFlow::CallNode call, DataFlow::Node out) { + exists(DataFlow::Node source, string s, DataFlow::Node uri | + call.getAName() = ["Invoke-WebRequest", "iwr", "Invoke-RestMethod", "irm", "curl", "wget"] and + uri = call.getNamedArgument("uri") and + TaintTracking::localTaint(source, uri) and + s.toLowerCase() + .matches([ + "%github%", + "%gitlab%", + "%bitbucket%", + "%sourceforge%", + "%powershellgallery%", + "%nuget%", + "%npmjs%", + "%pypi%", + "%repo1.maven%", + "%repo.maven.apache%", + "%blob.core.windows%", + "%amazonaws%", + "%googleapis%", + "%azure%", + "%visualstudio%", + "%jfrog%", + "%artifactory%" + ]) and + s = + [ + source.asExpr().getValue().asString(), + source.asExpr().getExpr().(ExpandableStringExpr).getUnexpandedValue() + ] and + out = call.getNamedArgument("outfile") + ) +} + +module Conf implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { invokeGitHub(_, source) } + + predicate isSink(DataFlow::Node sink) { + exists(DataFlow::CallNode hasher | + hasher.getAName() = ["Get-FileHash", "gfh"] and + sink = hasher.getNamedArgument(["path", "literalpath"]) + ) + } +} + +module Flow = DataFlow::Global; + +predicate isHashed(DataFlow::Node out) { + exists(Flow::PathNode source | + source.getNode() = out and + source.isSource() + ) +} + +from DataFlow::CallNode call, DataFlow::Node out +where + invokeGitHub(call, out) and + not isHashed(out) +select call, "This downloads an artifact from GitHub without checking its hash." From 5ecf511596b8ca621135873995f92f17d845d658 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Thu, 25 Jun 2026 21:17:59 +0100 Subject: [PATCH 2/9] PS: Add qhelp for CWE-494. --- .../DownloadWithoutIntegrityCheck.qhelp | 62 +++++++++++++++++++ .../DownloadWithoutIntegrityCheckBad.ps1 | 10 +++ .../DownloadWithoutIntegrityCheckGood.ps1 | 18 ++++++ 3 files changed, 90 insertions(+) create mode 100644 powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.qhelp create mode 100644 powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckBad.ps1 create mode 100644 powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckGood.ps1 diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.qhelp b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.qhelp new file mode 100644 index 000000000000..9af562220470 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.qhelp @@ -0,0 +1,62 @@ + + + + + +

+ Downloading an artifact (such as an executable, installer, archive, or script) and then + using it without verifying its integrity allows an attacker who can tamper with the artifact + to execute arbitrary code on the machine. Even when the artifact is retrieved from a trusted + source such as GitHub over HTTPS, the contents can still be replaced through a compromised + account, a malicious release asset, a poisoned cache or mirror, or a man-in-the-middle attack. +

+

+ Without an integrity check, there is no guarantee that the bytes that were downloaded are the + bytes that were intended, so the downloaded artifact must not be trusted. +

+
+ + +

+ Verify the integrity of every downloaded artifact before using it. Compute a cryptographic hash + of the downloaded file with Get-FileHash and compare it against a known-good hash + that is obtained through a trusted, out-of-band channel (for example, the signed release notes + or a published checksum file). Only use the artifact when the computed hash matches the expected + value; otherwise, discard it and fail. Where available, prefer verifying a digital signature of + the artifact in addition to, or instead of, a plain hash comparison. +

+
+ + +

+ In the following example, an executable is downloaded from GitHub and then run directly. Because + the artifact is never verified, a tampered download is executed without detection. +

+ + +

+ In the following example, the downloaded artifact's SHA-256 hash is compared against the expected + value before it is used. The artifact is removed and an error is raised if the integrity check + fails, so a tampered download is never executed. +

+ +
+ + +
  • + Common Weakness Enumeration: + CWE-494: Download of Code Without Integrity Check. +
  • +
  • + Common Weakness Enumeration: + CWE-829: Inclusion of Functionality from Untrusted Control Sphere. +
  • +
  • + Microsoft Learn: + Get-FileHash. +
  • +
    + +
    diff --git a/powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckBad.ps1 b/powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckBad.ps1 new file mode 100644 index 000000000000..d406ad64c79d --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckBad.ps1 @@ -0,0 +1,10 @@ +# BAD: An artifact is downloaded from GitHub and then executed without +# verifying that its contents match a known-good hash. If the release asset is +# replaced (for example, through a compromised account, a tampered mirror, or a +# man-in-the-middle attack), arbitrary code runs on the machine. +$uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe" +$outFile = Join-Path $env:TEMP "installer.exe" + +Invoke-WebRequest -Uri $uri -OutFile $outFile + +Start-Process -FilePath $outFile -Wait diff --git a/powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckGood.ps1 b/powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckGood.ps1 new file mode 100644 index 000000000000..fa7646ec2815 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-494/examples/powershell/DownloadWithoutIntegrityCheckGood.ps1 @@ -0,0 +1,18 @@ +# GOOD: The artifact is downloaded from GitHub and its SHA-256 hash is compared +# against the expected value (published by the author through a trusted channel, +# such as the signed release notes) before it is used. The artifact is only +# executed once its integrity has been confirmed. +$uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe" +$outFile = Join-Path $env:TEMP "installer.exe" +$expectedHash = "8c954cd9b6c8f8180a05f1de66ee555f0c44b4c6738120785d827521bb8d54df" + +Invoke-WebRequest -Uri $uri -OutFile $outFile + +$actualHash = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash + +if ($actualHash -ne $expectedHash) { + Remove-Item -Path $outFile -Force + throw "Integrity check failed: '$outFile' does not match the expected hash." +} + +Start-Process -FilePath $outFile -Wait From 48744dae2d005471d733d52e946d39d6899bd159 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Thu, 25 Jun 2026 21:18:24 +0100 Subject: [PATCH 3/9] PS: Add tests for CWE-494. --- .../DownloadWithoutIntegrityCheck.expected | 1 + .../DownloadWithoutIntegrityCheck.qlref | 2 ++ .../test/query-tests/security/cwe-494/test.ps1 | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected create mode 100644 powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.qlref create mode 100644 powershell/ql/test/query-tests/security/cwe-494/test.ps1 diff --git a/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected new file mode 100644 index 000000000000..bd67638cca6e --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected @@ -0,0 +1 @@ +| test.ps1:3:3:3:47 | Call to invoke-webrequest | This downloads an artifact from GitHub without checking its hash. | diff --git a/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.qlref b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.qlref new file mode 100644 index 000000000000..e967e6675f42 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.qlref @@ -0,0 +1,2 @@ +query: queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 new file mode 100644 index 000000000000..33cb996c4852 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 @@ -0,0 +1,18 @@ +function Test1($outFile) { + $uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe" + Invoke-WebRequest -Uri $uri -OutFile $outFile # $ Alert +} + +function Test2($outFile) { + $uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe" + $expectedHash = "8c954cd9b6c8f8180a05f1de66ee555f0c44b4c6738120785d827521bb8d54df" + + Invoke-WebRequest -Uri $uri -OutFile $outFile # GOOD + + $actualHash = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash + + if ($actualHash -ne $expectedHash) { + Remove-Item -Path $outFile -Force + throw "Integrity check failed: '$outFile' does not match the expected hash." + } +} \ No newline at end of file From f10abdaa35593fa5ff6d2a4b2ee84f9b6b9c5618 Mon Sep 17 00:00:00 2001 From: Chanel Young Date: Thu, 25 Jun 2026 22:36:48 -0700 Subject: [PATCH 4/9] added additional test cases --- .../cwe-494/DownloadWithoutIntegrityCheck.ql | 140 +++++++++++++----- .../DownloadWithoutIntegrityCheck.expected | 12 +- .../query-tests/security/cwe-494/test.ps1 | 112 ++++++++++++-- 3 files changed, 215 insertions(+), 49 deletions(-) diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql index 8fe8962d0ede..53fdab3fc8f1 100644 --- a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql @@ -15,62 +15,126 @@ import powershell import semmle.code.powershell.dataflow.DataFlow import semmle.code.powershell.dataflow.TaintTracking -predicate invokeGitHub(DataFlow::CallNode call, DataFlow::Node out) { - exists(DataFlow::Node source, string s, DataFlow::Node uri | - call.getAName() = ["Invoke-WebRequest", "iwr", "Invoke-RestMethod", "irm", "curl", "wget"] and - uri = call.getNamedArgument("uri") and - TaintTracking::localTaint(source, uri) and - s.toLowerCase() - .matches([ - "%github%", - "%gitlab%", - "%bitbucket%", - "%sourceforge%", - "%powershellgallery%", - "%nuget%", - "%npmjs%", - "%pypi%", - "%repo1.maven%", - "%repo.maven.apache%", - "%blob.core.windows%", - "%amazonaws%", - "%googleapis%", - "%azure%", - "%visualstudio%", - "%jfrog%", - "%artifactory%" - ]) and +/** Holds if `s` looks like a URL pointing at a trusted artifact host. */ +bindingset[s] +predicate isTrustedArtifactHost(string s) { + s.toLowerCase() + .matches([ + "%github%", + "%gitlab%", + "%bitbucket%", + "%sourceforge%", + "%powershellgallery%", + "%nuget%", + "%npmjs%", + "%pypi%", + "%repo1.maven%", + "%repo.maven.apache%", + "%blob.core.windows%", + "%amazonaws%", + "%googleapis%", + "%azure%", + "%visualstudio%", + "%jfrog%", + "%artifactory%" + ]) +} + +/** Holds if `node` is tainted by a string constant that looks like an artifact URL. */ +predicate isArtifactUrl(DataFlow::Node node) { + exists(DataFlow::Node source, string s | + TaintTracking::localTaint(source, node) and s = [ source.asExpr().getValue().asString(), source.asExpr().getExpr().(ExpandableStringExpr).getUnexpandedValue() ] and - out = call.getNamedArgument("outfile") + isTrustedArtifactHost(s) ) } +/** + * Holds if `call` downloads an artifact, where `url` is the argument that + * carries the (trusted-host) download URL. + * + * This covers cmdlets and their aliases (`Invoke-WebRequest`/`iwr`, + * `Invoke-RestMethod`/`irm`, `Start-BitsTransfer`), native download tools + * (`curl`, `wget`, `azcopy`, `aria2c`) and the .NET `WebClient`/`HttpClient` + * download methods. The URL may be passed as a named argument (e.g. `-Uri`, + * `-Source`), positionally, or as a method argument. + */ +predicate downloadCall(DataFlow::CallNode call, DataFlow::Node url) { + call.getAName() = + [ + // cmdlets and aliases + "Invoke-WebRequest", "iwr", "Invoke-RestMethod", "irm", "Start-BitsTransfer", + // native command-line download tools + "curl", "curl.exe", "wget", "wget.exe", "azcopy", "azcopy.exe", "aria2c", "aria2c.exe", + // .NET WebClient / HttpClient download methods + "DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync", "DownloadData", + "DownloadDataAsync", "DownloadDataTaskAsync", "DownloadString", "DownloadStringAsync", + "GetByteArrayAsync", "GetStreamAsync" + ] and + url = call.getAnArgument() and + isArtifactUrl(url) +} + +/** + * Gets the argument of `call` that names the file the artifact is written to, + * if any. Downloads that consume the response inline (e.g. `irm ... | iex`) + * have no such argument. + */ +DataFlow::Node getOutFileArg(DataFlow::CallNode call) { + downloadCall(call, _) and + ( + result = + call.getNamedArgument([ + "outfile", "destination", "outputfile", "outpath", "literalpath", "path", "o" + ]) + or + // WebClient.DownloadFile(url, destinationFile): the destination is the 2nd argument. + call.getAName() = ["DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync"] and + result = call.getArgument(1) + ) +} + +/** + * Holds if `check` verifies the integrity of the file referred to by `file`, + * by computing/comparing a hash or by checking a signature. + */ +predicate integrityCheck(DataFlow::CallNode check, DataFlow::Node file) { + check.getAName() = + [ + "Get-FileHash", "gfh", // hash a file + "certutil", "certutil.exe", // certutil -hashfile SHA256 + "ComputeHash", // [SHA256]::Create().ComputeHash(...) + "Get-AuthenticodeSignature", "Test-FileCatalog", // signature / catalog checks + "cosign", "cosign.exe", "gpg", "gpg.exe" // external signature verification + ] and + file = check.getAnArgument() +} + module Conf implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { invokeGitHub(_, source) } + predicate isSource(DataFlow::Node source) { source = getOutFileArg(_) } - predicate isSink(DataFlow::Node sink) { - exists(DataFlow::CallNode hasher | - hasher.getAName() = ["Get-FileHash", "gfh"] and - sink = hasher.getNamedArgument(["path", "literalpath"]) - ) - } + predicate isSink(DataFlow::Node sink) { integrityCheck(_, sink) } } module Flow = DataFlow::Global; -predicate isHashed(DataFlow::Node out) { +/** Holds if the downloaded file `out` flows to an integrity check. */ +predicate isVerified(DataFlow::Node out) { exists(Flow::PathNode source | source.getNode() = out and source.isSource() ) } -from DataFlow::CallNode call, DataFlow::Node out +from DataFlow::CallNode call where - invokeGitHub(call, out) and - not isHashed(out) -select call, "This downloads an artifact from GitHub without checking its hash." + downloadCall(call, _) and + // Report unless the file that was downloaded is later verified. A download + // with no output-file argument cannot be hash-verified, so it is reported. + not exists(DataFlow::Node out | out = getOutFileArg(call) and isVerified(out)) +select call, + "This downloads an artifact without verifying its integrity (e.g. a hash or signature check)." diff --git a/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected index bd67638cca6e..1f70910ff01f 100644 --- a/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected +++ b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected @@ -1 +1,11 @@ -| test.ps1:3:3:3:47 | Call to invoke-webrequest | This downloads an artifact from GitHub without checking its hash. | +| test.ps1:22:3:22:47 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:40:3:40:103 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:45:3:45:89 | Call to iwr | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:50:8:50:101 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:56:3:56:75 | Call to irm | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:66:3:66:95 | Call to downloadfile | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:71:3:71:118 | Call to downloadfile | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:76:3:76:116 | Call to start-bitstransfer | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:81:3:81:91 | Call to curl | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:86:3:86:75 | Call to wget | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | +| test.ps1:91:3:91:108 | Call to azcopy | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | diff --git a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 index 33cb996c4852..03332a51c8a5 100644 --- a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 +++ b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 @@ -1,18 +1,110 @@ -function Test1($outFile) { +# CWE-494 - Download of a GitHub release artifact without an integrity check. +# +# Each function below is one download idiom. It documents, as inline +# expectations, which idioms the query catches and which it still misses: +# +# # $ Alert -> the query reports here (expected true positive) +# # $ MISSING: Alert -> the query SHOULD report here but does not yet +# (known false negative) +# # $ SPURIOUS: Alert -> the query reports here but should not +# (known false positive) +# +# When the query is improved to close a gap, flip MISSING -> Alert (or remove +# SPURIOUS) and re-accept the .expected file. + +# --------------------------------------------------------------------------- +# COVERED today: a recognised cmdlet with a NAMED -Uri and a NAMED -OutFile. +# --------------------------------------------------------------------------- + +function Iwr_NamedUri_OutFile($outFile) { + # BAD - baseline; download from github with no hash check. $uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe" Invoke-WebRequest -Uri $uri -OutFile $outFile # $ Alert } -function Test2($outFile) { +function Iwr_NamedUri_OutFile_Hashed($outFile) { + # GOOD - Get-FileHash barrier on the downloaded file (correctly NOT flagged). $uri = "https://github.com/example/project/releases/download/v1.2.3/installer.exe" - $expectedHash = "8c954cd9b6c8f8180a05f1de66ee555f0c44b4c6738120785d827521bb8d54df" + $expected = "8c954cd9b6c8f8180a05f1de66ee555f0c44b4c6738120785d827521bb8d54df" + Invoke-WebRequest -Uri $uri -OutFile $outFile + $actual = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash + if ($actual -ne $expected) { Remove-Item $outFile -Force; throw "hash mismatch" } +} + +# --------------------------------------------------------------------------- +# MISSING: argument-shape variants of Invoke-WebRequest / Invoke-RestMethod. +# --------------------------------------------------------------------------- + +function Iwr_PositionalUri($outFile) { + # BAD - URL passed positionally, not as -Uri. + Invoke-WebRequest "https://github.com/example/project/releases/download/v1/app.exe" -OutFile $outFile # $ Alert +} + +function Iwr_Alias_Positional($outFile) { + # BAD - iwr alias + positional URL. + iwr "https://github.com/example/project/releases/download/v1/app.zip" -OutFile $outFile # $ Alert +} + +function Iwr_NoOutFile_PipedToIex() { + # BAD - no -OutFile; content executed inline. + $r = Invoke-WebRequest -Uri "https://github.com/example/project/releases/download/v1/bootstrap.ps1" # $ Alert + Invoke-Expression $r.Content +} - Invoke-WebRequest -Uri $uri -OutFile $outFile # GOOD +function Irm_NoOutFile() { + # BAD - Invoke-RestMethod returns content, no -OutFile. + irm "https://github.com/example/project/releases/download/v1/install.ps1" | Invoke-Expression # $ Alert +} - $actualHash = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash +# --------------------------------------------------------------------------- +# MISSING: download sinks that are not in the cmdlet name list at all. +# --------------------------------------------------------------------------- - if ($actualHash -ne $expectedHash) { - Remove-Item -Path $outFile -Force - throw "Integrity check failed: '$outFile' does not match the expected hash." - } -} \ No newline at end of file +function WebClient_DownloadFile($outFile) { + # BAD - System.Net.WebClient.DownloadFile. + $wc = New-Object System.Net.WebClient + $wc.DownloadFile("https://github.com/example/project/releases/download/v1/app.exe", $outFile) # $ Alert +} + +function WebClient_Inline($outFile) { + # BAD - inline (New-Object Net.WebClient).DownloadFile(...). + (New-Object Net.WebClient).DownloadFile("https://github.com/example/project/releases/download/v1/app.exe", $outFile) # $ Alert +} + +function Bits_Transfer($outFile) { + # BAD - Start-BitsTransfer. + Start-BitsTransfer -Source "https://github.com/example/project/releases/download/v1/app.exe" -Destination $outFile # $ Alert +} + +function Curl_Native($outFile) { + # BAD - native curl with -o. + curl -sL "https://github.com/example/project/releases/download/v1/app.tar.gz" -o $outFile # $ Alert +} + +function Wget_Native() { + # BAD - native wget piped to tar (no output file at all). + wget "https://github.com/example/project/releases/download/v1/app.tar.gz" # $ Alert +} + +function AzCopy($outFile) { + # BAD - azcopy / aria2c. + azcopy copy "https://github.com/Azure/azure-storage-azcopy/releases/download/v10.31.0/azcopy.zip" $outFile # $ Alert +} + +function Wrapper_Function($outFile) { + # BAD - custom download wrapper; the real Invoke-WebRequest lives in a + # dot-sourced helper. + Download-File "https://github.com/example/project/releases/download/v1/app.exe" $outFile # $ MISSING: Alert +} + +# --------------------------------------------------------------------------- +# Verified by means other than Get-FileHash - should NOT be flagged. +# --------------------------------------------------------------------------- + +function Verified_With_CertUtil($outFile) { + # GOOD - certutil hash verification (now recognised as an integrity barrier). + $uri = "https://github.com/example/project/releases/download/v1/app.exe" + Invoke-WebRequest -Uri $uri -OutFile $outFile + $h = certutil -hashfile $outFile SHA256 + if ($h -notmatch "ABC123") { throw "hash mismatch" } +} From 067cac3f9ba1fb1c0ec197df9a3b6c53b6c20799 Mon Sep 17 00:00:00 2001 From: Chanel Young Date: Thu, 25 Jun 2026 22:59:37 -0700 Subject: [PATCH 5/9] added false negative cases, refactor to use classes instead of predicates, isAdditionalFlowStep to fix fp case --- .../cwe-494/DownloadWithoutIntegrityCheck.ql | 138 ++++++++++-------- .../query-tests/security/cwe-494/test.ps1 | 49 +++++++ 2 files changed, 130 insertions(+), 57 deletions(-) diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql index 53fdab3fc8f1..f5afae87647c 100644 --- a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql @@ -40,22 +40,26 @@ predicate isTrustedArtifactHost(string s) { ]) } -/** Holds if `node` is tainted by a string constant that looks like an artifact URL. */ -predicate isArtifactUrl(DataFlow::Node node) { - exists(DataFlow::Node source, string s | - TaintTracking::localTaint(source, node) and - s = - [ - source.asExpr().getValue().asString(), - source.asExpr().getExpr().(ExpandableStringExpr).getUnexpandedValue() - ] and - isTrustedArtifactHost(s) - ) +/** A data-flow node that is tainted by a string constant looking like an artifact URL. */ +class ArtifactUrl extends DataFlow::Node { + ArtifactUrl() { + exists(DataFlow::Node source, string s | + TaintTracking::localTaint(source, this) and + s = + [ + source.asExpr().getValue().asString(), + source.asExpr().getExpr().(ExpandableStringExpr).getUnexpandedValue() + ] and + isTrustedArtifactHost(s) and + // Exclude API metadata endpoints (e.g. api.github.com/.../releases/latest), + // which return JSON metadata rather than a downloadable artifact. + not s.toLowerCase().matches(["%api.github.com%", "%api.bitbucket.org%"]) + ) + } } /** - * Holds if `call` downloads an artifact, where `url` is the argument that - * carries the (trusted-host) download URL. + * A call that downloads an artifact from a trusted host. * * This covers cmdlets and their aliases (`Invoke-WebRequest`/`iwr`, * `Invoke-RestMethod`/`irm`, `Start-BitsTransfer`), native download tools @@ -63,61 +67,82 @@ predicate isArtifactUrl(DataFlow::Node node) { * download methods. The URL may be passed as a named argument (e.g. `-Uri`, * `-Source`), positionally, or as a method argument. */ -predicate downloadCall(DataFlow::CallNode call, DataFlow::Node url) { - call.getAName() = - [ - // cmdlets and aliases - "Invoke-WebRequest", "iwr", "Invoke-RestMethod", "irm", "Start-BitsTransfer", - // native command-line download tools - "curl", "curl.exe", "wget", "wget.exe", "azcopy", "azcopy.exe", "aria2c", "aria2c.exe", - // .NET WebClient / HttpClient download methods - "DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync", "DownloadData", - "DownloadDataAsync", "DownloadDataTaskAsync", "DownloadString", "DownloadStringAsync", - "GetByteArrayAsync", "GetStreamAsync" - ] and - url = call.getAnArgument() and - isArtifactUrl(url) -} +class DownloadCall extends DataFlow::CallNode { + ArtifactUrl url; -/** - * Gets the argument of `call` that names the file the artifact is written to, - * if any. Downloads that consume the response inline (e.g. `irm ... | iex`) - * have no such argument. - */ -DataFlow::Node getOutFileArg(DataFlow::CallNode call) { - downloadCall(call, _) and - ( + DownloadCall() { + this.getAName() = + [ + // cmdlets and aliases + "Invoke-WebRequest", "iwr", "Invoke-RestMethod", "irm", "Start-BitsTransfer", + // native command-line download tools + "curl", "curl.exe", "wget", "wget.exe", "azcopy", "azcopy.exe", "aria2c", "aria2c.exe", + // .NET WebClient / HttpClient download methods + "DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync", "DownloadData", + "DownloadDataAsync", "DownloadDataTaskAsync", "DownloadString", "DownloadStringAsync", + "GetByteArrayAsync", "GetStreamAsync" + ] and + url = this.getAnArgument() + } + + /** + * Gets the argument that names the file the artifact is written to, if any. + * Downloads that consume the response inline (e.g. `irm ... | iex`) have no + * such argument. + */ + DataFlow::Node getOutFileArg() { result = - call.getNamedArgument([ + this.getNamedArgument([ "outfile", "destination", "outputfile", "outpath", "literalpath", "path", "o" ]) or // WebClient.DownloadFile(url, destinationFile): the destination is the 2nd argument. - call.getAName() = ["DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync"] and - result = call.getArgument(1) - ) + this.getAName() = ["DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync"] and + result = this.getArgument(1) + } } /** - * Holds if `check` verifies the integrity of the file referred to by `file`, - * by computing/comparing a hash or by checking a signature. + * A call that verifies the integrity of a file, by computing/comparing a hash + * or by checking a signature. */ -predicate integrityCheck(DataFlow::CallNode check, DataFlow::Node file) { - check.getAName() = - [ - "Get-FileHash", "gfh", // hash a file - "certutil", "certutil.exe", // certutil -hashfile SHA256 - "ComputeHash", // [SHA256]::Create().ComputeHash(...) - "Get-AuthenticodeSignature", "Test-FileCatalog", // signature / catalog checks - "cosign", "cosign.exe", "gpg", "gpg.exe" // external signature verification - ] and - file = check.getAnArgument() +class IntegrityCheck extends DataFlow::CallNode { + IntegrityCheck() { + this.getAName() = + [ + "Get-FileHash", "gfh", // hash a file + "certutil", "certutil.exe", // certutil -hashfile SHA256 + "ComputeHash", // [SHA256]::Create().ComputeHash(...) + "Get-AuthenticodeSignature", "Test-FileCatalog", // signature / catalog checks + "cosign", "cosign.exe", "gpg", "gpg.exe" // external signature verification + ] + } + + /** Gets an argument referring to the file whose integrity is being checked. */ + DataFlow::Node getFile() { result = this.getAnArgument() } } module Conf implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { source = getOutFileArg(_) } + predicate isSource(DataFlow::Node source) { source = any(DownloadCall c).getOutFileArg() } + + predicate isSink(DataFlow::Node sink) { sink = any(IntegrityCheck c).getFile() } - predicate isSink(DataFlow::Node sink) { integrityCheck(_, sink) } + /** + * Bridges a file path to the bytes/text read from it, so that hashing the + * *contents* of the downloaded file (e.g. `ComputeHash([IO.File]::ReadAllBytes($f))`) + * is recognised as verifying `$f`. + */ + predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(DataFlow::CallNode read | + read.getAName() = + [ + "ReadAllBytes", "ReadAllText", "ReadAllLines", "OpenRead", "ReadAsByteArrayAsync", + "Get-Content", "gc", "cat", "type" + ] and + node1 = read.getAnArgument() and + node2 = read + ) + } } module Flow = DataFlow::Global; @@ -130,11 +155,10 @@ predicate isVerified(DataFlow::Node out) { ) } -from DataFlow::CallNode call +from DownloadCall call where - downloadCall(call, _) and // Report unless the file that was downloaded is later verified. A download // with no output-file argument cannot be hash-verified, so it is reported. - not exists(DataFlow::Node out | out = getOutFileArg(call) and isVerified(out)) + not exists(DataFlow::Node out | out = call.getOutFileArg() and isVerified(out)) select call, "This downloads an artifact without verifying its integrity (e.g. a hash or signature check)." diff --git a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 index 03332a51c8a5..1b9fd54230c0 100644 --- a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 +++ b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 @@ -108,3 +108,52 @@ function Verified_With_CertUtil($outFile) { $h = certutil -hashfile $outFile SHA256 if ($h -notmatch "ABC123") { throw "hash mismatch" } } + +# --------------------------------------------------------------------------- +# TRUE NEGATIVES: these must NOT be flagged. Either the download is verified, +# or it is not a trusted-host artifact download at all. No inline expectation +# means "no alert expected here". +# --------------------------------------------------------------------------- + +function Verified_With_ComputeHash($outFile) { + # GOOD - hash computed via [SHA256]::Create().ComputeHash over the bytes read + # from the downloaded file, then compared. + $uri = "https://github.com/example/project/releases/download/v1/app.exe" + Invoke-WebRequest -Uri $uri -OutFile $outFile + $sha = [System.Security.Cryptography.SHA256]::Create() + $bytes = [System.IO.File]::ReadAllBytes($outFile) + $hash = [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace("-", "") + if ($hash -ne "ABC123") { throw "hash mismatch" } +} + +function Verified_With_Signature($outFile) { + # GOOD - Authenticode signature check on the downloaded file. + $uri = "https://github.com/example/project/releases/download/v1/app.exe" + Invoke-WebRequest -Uri $uri -OutFile $outFile + $sig = Get-AuthenticodeSignature -FilePath $outFile + if ($sig.Status -ne "Valid") { throw "bad signature" } +} + +function Verified_With_Cosign($outFile) { + # GOOD - external signature verification via cosign. + $uri = "https://github.com/example/project/releases/download/v1/app.tar.gz" + Invoke-WebRequest -Uri $uri -OutFile $outFile + cosign verify-blob --signature "$outFile.sig" $outFile +} + +function NonArtifact_Host($outFile) { + # GOOD - download from a host that is not a recognised artifact host. + Invoke-WebRequest -Uri "https://example.com/data/report.json" -OutFile $outFile +} + +function GitHub_Api_DataOnly() { + # GOOD - a GitHub API metadata call, not an artifact download. The response is + # only inspected, never written to disk or executed. + $r = Invoke-RestMethod -Uri "https://api.github.com/repos/example/project/releases/latest" + return $r.tag_name +} + +function Local_Copy($outFile) { + # GOOD - copying a local file; no network download, no trusted-host URL. + Copy-Item -Path "C:\artifacts\app.exe" -Destination $outFile +} From de1b9c0ed405ab1e139201c845cc6e9a08dbc55b Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 26 Jun 2026 10:45:28 +0100 Subject: [PATCH 6/9] PS: Avoid calling 'toLowerCase' multiple times. --- .../cwe-494/DownloadWithoutIntegrityCheck.ql | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql index f5afae87647c..8ef4a86eca1a 100644 --- a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql @@ -18,26 +18,25 @@ import semmle.code.powershell.dataflow.TaintTracking /** Holds if `s` looks like a URL pointing at a trusted artifact host. */ bindingset[s] predicate isTrustedArtifactHost(string s) { - s.toLowerCase() - .matches([ - "%github%", - "%gitlab%", - "%bitbucket%", - "%sourceforge%", - "%powershellgallery%", - "%nuget%", - "%npmjs%", - "%pypi%", - "%repo1.maven%", - "%repo.maven.apache%", - "%blob.core.windows%", - "%amazonaws%", - "%googleapis%", - "%azure%", - "%visualstudio%", - "%jfrog%", - "%artifactory%" - ]) + s.matches([ + "%github%", + "%gitlab%", + "%bitbucket%", + "%sourceforge%", + "%powershellgallery%", + "%nuget%", + "%npmjs%", + "%pypi%", + "%repo1.maven%", + "%repo.maven.apache%", + "%blob.core.windows%", + "%amazonaws%", + "%googleapis%", + "%azure%", + "%visualstudio%", + "%jfrog%", + "%artifactory%" + ]) } /** A data-flow node that is tainted by a string constant looking like an artifact URL. */ @@ -49,11 +48,11 @@ class ArtifactUrl extends DataFlow::Node { [ source.asExpr().getValue().asString(), source.asExpr().getExpr().(ExpandableStringExpr).getUnexpandedValue() - ] and + ].toLowerCase() and isTrustedArtifactHost(s) and // Exclude API metadata endpoints (e.g. api.github.com/.../releases/latest), // which return JSON metadata rather than a downloadable artifact. - not s.toLowerCase().matches(["%api.github.com%", "%api.bitbucket.org%"]) + not s.matches(["%api.github.com%", "%api.bitbucket.org%"]) ) } } @@ -93,8 +92,8 @@ class DownloadCall extends DataFlow::CallNode { DataFlow::Node getOutFileArg() { result = this.getNamedArgument([ - "outfile", "destination", "outputfile", "outpath", "literalpath", "path", "o" - ]) + "outfile", "destination", "outputfile", "outpath", "literalpath", "path", "o" + ]) or // WebClient.DownloadFile(url, destinationFile): the destination is the 2nd argument. this.getAName() = ["DownloadFile", "DownloadFileAsync", "DownloadFileTaskAsync"] and From 3fc4217ae48e11f96991e551db6af6028de228f1 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 26 Jun 2026 12:23:31 +0100 Subject: [PATCH 7/9] PS: Switch to taint-tracking (it was supposed to be taint-tracking all along) and replace ad-hoc additional steps with flow summaries. --- .../powershell/frameworks/System.IO.model.yml | 11 ++++++++++- .../cwe-494/DownloadWithoutIntegrityCheck.ql | 19 +------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml b/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml index 85dfd5c90802..96f035099a16 100644 --- a/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml +++ b/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml @@ -36,4 +36,13 @@ extensions: pack: microsoft/powershell-all extensible: summaryModel data: - - ["system.io.path!", "Method[getfullpath]", "Argument[0]", "ReturnValue", "taint"] \ No newline at end of file + - ["system.io.path!", "Method[getfullpath]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[readallbytes]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[readallbytesasync]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[appendtext]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[createtext]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[readalllines]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[readalllinesasync]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[readalltext]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.file!", "Method[readalltextasync]", "Argument[0]", "ReturnValue", "taint"] + - ["system.io.fileinfo", "Method[createtext]", "Argument[0]", "ReturnValue", "taint"] \ No newline at end of file diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql index 8ef4a86eca1a..095724184e4d 100644 --- a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql @@ -125,26 +125,9 @@ module Conf implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source = any(DownloadCall c).getOutFileArg() } predicate isSink(DataFlow::Node sink) { sink = any(IntegrityCheck c).getFile() } - - /** - * Bridges a file path to the bytes/text read from it, so that hashing the - * *contents* of the downloaded file (e.g. `ComputeHash([IO.File]::ReadAllBytes($f))`) - * is recognised as verifying `$f`. - */ - predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { - exists(DataFlow::CallNode read | - read.getAName() = - [ - "ReadAllBytes", "ReadAllText", "ReadAllLines", "OpenRead", "ReadAsByteArrayAsync", - "Get-Content", "gc", "cat", "type" - ] and - node1 = read.getAnArgument() and - node2 = read - ) - } } -module Flow = DataFlow::Global; +module Flow = TaintTracking::Global; /** Holds if the downloaded file `out` flows to an integrity check. */ predicate isVerified(DataFlow::Node out) { From 067711ab4d13ec21140e59ce9c3572ed4ee446eb Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 26 Jun 2026 12:24:40 +0100 Subject: [PATCH 8/9] PS: Undo a change from f10abdaa35593fa5ff6d2a4b2ee84f9b6b9c5618 which causes too many results. --- .../security/cwe-494/DownloadWithoutIntegrityCheck.ql | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql index 095724184e4d..f9c1d98aeab4 100644 --- a/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql +++ b/powershell/ql/src/queries/security/cwe-494/DownloadWithoutIntegrityCheck.ql @@ -137,10 +137,9 @@ predicate isVerified(DataFlow::Node out) { ) } -from DownloadCall call +from DownloadCall call, DataFlow::Node out where - // Report unless the file that was downloaded is later verified. A download - // with no output-file argument cannot be hash-verified, so it is reported. - not exists(DataFlow::Node out | out = call.getOutFileArg() and isVerified(out)) + out = call.getOutFileArg() and + not isVerified(out) select call, "This downloads an artifact without verifying its integrity (e.g. a hash or signature check)." From 9bb98fed73d939bba7e095d2c0888036d6da23cf Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 26 Jun 2026 17:48:03 +0100 Subject: [PATCH 9/9] PS: Accept test changes. --- .../cwe-494/DownloadWithoutIntegrityCheck.expected | 4 ---- powershell/ql/test/query-tests/security/cwe-494/test.ps1 | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected index 1f70910ff01f..e44db9cf3629 100644 --- a/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected +++ b/powershell/ql/test/query-tests/security/cwe-494/DownloadWithoutIntegrityCheck.expected @@ -1,11 +1,7 @@ | test.ps1:22:3:22:47 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | | test.ps1:40:3:40:103 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | | test.ps1:45:3:45:89 | Call to iwr | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | -| test.ps1:50:8:50:101 | Call to invoke-webrequest | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | -| test.ps1:56:3:56:75 | Call to irm | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | | test.ps1:66:3:66:95 | Call to downloadfile | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | | test.ps1:71:3:71:118 | Call to downloadfile | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | | test.ps1:76:3:76:116 | Call to start-bitstransfer | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | | test.ps1:81:3:81:91 | Call to curl | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | -| test.ps1:86:3:86:75 | Call to wget | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | -| test.ps1:91:3:91:108 | Call to azcopy | This downloads an artifact without verifying its integrity (e.g. a hash or signature check). | diff --git a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 index 1b9fd54230c0..df443d37aeb0 100644 --- a/powershell/ql/test/query-tests/security/cwe-494/test.ps1 +++ b/powershell/ql/test/query-tests/security/cwe-494/test.ps1 @@ -47,13 +47,13 @@ function Iwr_Alias_Positional($outFile) { function Iwr_NoOutFile_PipedToIex() { # BAD - no -OutFile; content executed inline. - $r = Invoke-WebRequest -Uri "https://github.com/example/project/releases/download/v1/bootstrap.ps1" # $ Alert + $r = Invoke-WebRequest -Uri "https://github.com/example/project/releases/download/v1/bootstrap.ps1" # $ MISSING: Alert Invoke-Expression $r.Content } function Irm_NoOutFile() { # BAD - Invoke-RestMethod returns content, no -OutFile. - irm "https://github.com/example/project/releases/download/v1/install.ps1" | Invoke-Expression # $ Alert + irm "https://github.com/example/project/releases/download/v1/install.ps1" | Invoke-Expression # $ MISSING: Alert } # --------------------------------------------------------------------------- @@ -83,12 +83,12 @@ function Curl_Native($outFile) { function Wget_Native() { # BAD - native wget piped to tar (no output file at all). - wget "https://github.com/example/project/releases/download/v1/app.tar.gz" # $ Alert + wget "https://github.com/example/project/releases/download/v1/app.tar.gz" # $ MISSING: Alert } function AzCopy($outFile) { # BAD - azcopy / aria2c. - azcopy copy "https://github.com/Azure/azure-storage-azcopy/releases/download/v10.31.0/azcopy.zip" $outFile # $ Alert + azcopy copy "https://github.com/Azure/azure-storage-azcopy/releases/download/v10.31.0/azcopy.zip" $outFile # $ MISSING: Alert } function Wrapper_Function($outFile) {