| Title: | Static Analysis Security Testing (SAST) for R Packages |
|---|---|
| Description: | A static analysis tool that flags security-relevant files and code in R source packages for human review without executing anything it scans. Models package lifecycle execution semantics -- it reports not just what a package does, but when it runs, so code that executes on install or load is distinguishable from code that runs only when called. Reads every surface a package can execute, including 'configure' scripts, help-file macros, and vignettes, and states what it could not read, so a clean result can be checked rather than trusted. Detection rules are data, shipped in a versioned and hash-verified 'SQLite' database. |
| Authors: | Tyler Smith [aut, cre] (ORCID: <https://orcid.org/0000-0003-4692-2206>) |
| Maintainer: | Tyler Smith <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.4.0 |
| Built: | 2026-10-17 10:42:14 UTC |
| Source: | https://github.com/tylerjssmith/pkgaudit |
Dispatches on the segment's language, which the extractor set. This is a separate axis from the source's type: a help file and an R script both yield R segments, and one literate file can yield several languages.
analyze_segment(segment, rules)analyze_segment(segment, rules)
segment |
A |
rules |
Named list of rules as returned by |
A list of patterns, matches, coverage and errors data
frames, each with the columns audit_package() documents, less the phase
columns.
A method must build its return value with new_findings(), which holds every
analyzer to the same frame shape. UseMethod() ends the generic, so this
cannot be enforced after dispatch.
A method must not evaluate the code it is given. It reads untrusted text and reports what it finds; nothing in a scan is ever run.
extract_segments(), the other axis of dispatch, new_findings(),
and vignette("internals").
# Adding a language outside pkgaudit: a method for segments the extractor # labeled "python", reporting each line that calls eval(). analyze_segment.python <- function(segment, rules) { at <- grep("\\beval\\(", segment$lines) if (length(at) == 0L) return(new_findings()) new_findings(matches = data.frame( rule = "py_eval", file_context = segment$file_context, line_number = at, column_number = NA_integer_, preview = trimws(segment$lines[at]), message = "eval() in Python code", attck = NA_character_)) } registerS3method("analyze_segment", "python", analyze_segment.python)# Adding a language outside pkgaudit: a method for segments the extractor # labeled "python", reporting each line that calls eval(). analyze_segment.python <- function(segment, rules) { at <- grep("\\beval\\(", segment$lines) if (length(at) == 0L) return(new_findings()) new_findings(matches = data.frame( rule = "py_eval", file_context = segment$file_context, line_number = at, column_number = NA_integer_, preview = trimws(segment$lines[at]), message = "eval() in Python code", attck = NA_character_)) } registerS3method("analyze_segment", "python", analyze_segment.python)
Finds security-relevant file and code contexts, code patterns, and shell and make matches for review before an R source package is trusted.
audit_package(path = ".", rules = load_rules(), .origin = NULL)audit_package(path = ".", rules = load_rules(), .origin = NULL)
path |
Path to an R source package root directory. Defaults to the current directory. |
rules |
Named list of rules. Defaults to the rules bundled with the
package as returned by |
.origin |
Internal. Used by |
Every findings frame carries one logical column per phase – at_autoconf,
at_build, at_check, at_install_src, at_install_bin, at_load,
at_attach, at_unload and at_detach. Value is TRUE when that finding's
code runs then. A finding can belong to several phases.
A file in a language pkgaudit cannot read is left unscanned rather than
scanned badly, so a file's absence from patterns and matches is not
evidence that it is clean; see coverage and errors.
preview is a display-only excerpt of the line, whitespace collapsed and a
long line windowed on the match, so column_number does not index into it.
A new_pkgaudit() object: a named list with class pkgaudit holding
five data frames and a metadata list. Every findings frame carries the
nine phase columns described in Details and joins to the others on
file_context. Paths are relative to the package root.
rule, file_context, message.
rule, file_context, line_number, column_number,
code_context, guarded, indirect, preview, message, attck.
code_context is where the code sits within its file: top_level,
in_function, an enclosing lifecycle hook, or the part of a help file
it came from.
rule, file_context, line_number, column_number,
preview, message, attck: regular-expression matches in the shell
and Make-like file contexts.
file_context, language, status, reason,
first_line, last_line, lines, bytes, rule. One row per file
the package carries that is, or could be, code, plus one per span of
an unanalyzed language inside a literate file – a python chunk in a
vignette is its own row. status is parsed, matched, exportable,
unexamined or error; reason says what stood in the way, and is
NA, no_analyzer, no_extractor, no_rule, serialized, binary,
symlink, too_large, unreadable or unparseable.
step, file_context, rule, message. Recoverable
failures, collected rather than aborting the scan.
pkg_name, pkg_version, pkg_path, pkg_is_tarball,
pkg_sha256, pkgaudit_version, pkgaudit_rules_version,
pkgaudit_rules_sha256, scanned. The two rules fields are NA for a
rules list that did not come from load_rules().
vignette("pkgaudit") for a worked scan, and vignette("rules")
for the rules, phases and contexts behind a finding.
# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) exdir <- file.path(tempdir(), "untrustedpkg-example") utils::untar(tarball, exdir = exdir) rules <- load_rules() result <- audit_package(file.path(exdir, "untrustedpkg"), rules = rules) result$file_contexts result$patterns print(result)# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) exdir <- file.path(tempdir(), "untrustedpkg-example") utils::untar(tarball, exdir = exdir) rules <- load_rules() result <- audit_package(file.path(exdir, "untrustedpkg"), rules = rules) result$file_contexts result$patterns print(result)
Finds security-relevant file and code contexts, code patterns, and shell and make matches for review before an R source package tarball is trusted.
audit_tarball( path, rules = load_rules(), temp_dir = tempdir(), max_entries = 100000L, max_bytes = 2 * 1024^3, max_ratio = 256 )audit_tarball( path, rules = load_rules(), temp_dir = tempdir(), max_entries = 100000L, max_bytes = 2 * 1024^3, max_ratio = 256 )
path |
Path to a gzip-compressed or uncompressed R source package
tarball ( |
rules |
Named list of rules. Defaults to the rules bundled with the
package as returned by |
temp_dir |
Directory used for extraction. A unique subdirectory is
created here and removed after auditing regardless of success or failure.
Defaults to |
max_entries |
Maximum number of entries to read before failing closed. Default 100,000. |
max_bytes |
Maximum uncompressed bytes to read before failing closed. Default 2 GB. Raise for ecosystems with larger artifacts, e.g. Bioconductor annotation and experiment-data packages. |
max_ratio |
Maximum uncompressed:compressed ratio before failing closed,
or |
Extracts a source package tarball to a temporary directory, applies
audit_package(), removes the temporary directory, and returns the result.
Before extracting anything, the tarball is validated with validate_tar(),
which fails closed. After extraction, the extracted directory is re-checked
and rejected if it contains any symlink, as defense in depth against a
validate_tar()/untar() disagreement.
After extraction, the tarball filename must be consistent with the top-level
directory it produced (e.g. foo_0.1.0.tar.gz must extract to foo/);
otherwise the tarball is rejected.
The audited DESCRIPTION is then checked against the tarball filename: if
the Package name or Version disagrees with the name and version implied
by the filename (foo and 0.1.0 for foo_0.1.0.tar.gz), a
pkgaudit_provenance_mismatch warning is issued, which a caller can catch
by class. The DESCRIPTION values are authoritative and are what the
returned object reports.
The same pkgaudit object as audit_package(): a named list with
class pkgaudit containing five data frames and a metadata list.
# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) print(result)# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) print(result)
Renders a pkgaudit object as a SARIF 2.1.0 document, the format code
scanning tools publish results in, so a scan can be read in an editor or
loaded by any SARIF consumer.
emit_sarif(object, pretty = TRUE)emit_sarif(object, pretty = TRUE)
object |
A |
pretty |
Logical; if |
Every finding in file_contexts, patterns and matches becomes a result,
located by the path and, where there is one, the line and column. Rule ids
are labeled by the kind of rule – pattern/curl, match/curl,
file/configure – because a rule name is unique only within its kind.
A result's message is a one-line title – the rule, the code context it sits
in, and the phases it runs in – because a consumer displays it as the
heading of a finding. A rule's own text is the same for every finding the
rule produces, so it describes the rule rather than the finding and is
written as the descriptor's fullDescription and help.
level is note for every result: pkgaudit does not rank findings, so
nothing is mapped onto SARIF's severity field. When a finding's code executes
is carried in the title and in properties.phases, and a note is never a
claim that a finding is minor.
partialFingerprints identifies a finding by its rule, its file, the code
context it sits in, and the text of the line – not by line number, which
shifts whenever anything above it is edited. Two occurrences a consumer could
not otherwise tell apart are numbered, since a fingerprint repeated within a
run makes several findings read as one. The coverage frame becomes the
artifacts array, so a consumer can see which files were never read, and
errors become execution notifications on the invocation.
Requires jsonlite, a suggested dependency.
A length-one character vector holding the SARIF document. Nothing is
written; writeLines() it where you want it.
# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) writeLines(emit_sarif(result), file.path(tempdir(), "pkgaudit.sarif"))# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) writeLines(emit_sarif(result), file.path(tempdir(), "pkgaudit.sarif"))
Writes the parts of a package that pkgaudit does not analyze – C, C++, Fortran, Rust, Python and JavaScript among them, and the vignette chunks written in those languages – into a directory another static analyzer can be pointed at.
export_unscanned( object, dir, source = NULL, overwrite = FALSE, max_bytes = .max_export_bytes )export_unscanned( object, dir, source = NULL, overwrite = FALSE, max_bytes = .max_export_bytes )
object |
A |
dir |
Directory to write into. Required, and created if absent. Naming it is how the caller consents to being written to, so there is no default. |
source |
The package to read from. Defaults to |
overwrite |
Logical; if |
max_bytes |
Largest file to copy, in bytes; defaults to 64 MB. A larger
one is recorded in the manifest and left behind. This is a separate limit
from the 10 MB one |
A whole file is copied verbatim. A span – a vignette chunk in a language
with no analyzer – is written into a file of its own, blank-padded so its
code sits at the same line numbers it occupies in the source. A finding
another tool reports at line 40 of intro.python.py is therefore at line 40
of intro.Rmd, with no offset to apply. All spans of one language in one
source share a file, since that is how they run.
What gets exported is read from the coverage frame, so the scan's own
account of what it could not read is the single source of truth.
Invisibly, a manifest data frame with one row per exportable span:
path (relative to dir, NA if not written), file_context,
language, first_line, last_line, written, and note.
This is the only function that writes where a caller sends it, and both the content and the file names come from an untrusted package. Therefore:
every target path is resolved and must lie under dir;
a path component that is ., .., or that contains a separator or a
control character is refused;
symlinks are never followed, and content is read and rewritten rather than copied, so a link pointing out of the package cannot pull a file in;
nothing is written executable, and nothing is removed.
A refused span is recorded in the manifest rather than dropped, since a file that cannot be exported safely is one worth knowing about. Exporting still executes nothing.
# untrustedpkg is a small package shipped with pkgaudit to be scanned. It is # R and shell throughout, so there is nothing to hand on and the manifest # comes back empty; a package carrying compiled code yields one row per # exported file. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) exdir <- file.path(tempdir(), "untrustedpkg-example") utils::untar(tarball, exdir = exdir) result <- audit_package(file.path(exdir, "untrustedpkg")) manifest <- export_unscanned(result, file.path(tempdir(), "for-semgrep")) nrow(manifest)# untrustedpkg is a small package shipped with pkgaudit to be scanned. It is # R and shell throughout, so there is nothing to hand on and the manifest # comes back empty; a package carrying compiled code yields one row per # exported file. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) exdir <- file.path(tempdir(), "untrustedpkg-example") utils::untar(tarball, exdir = exdir) result <- audit_package(file.path(exdir, "untrustedpkg")) manifest <- export_unscanned(result, file.path(tempdir(), "for-semgrep")) nrow(manifest)
Dispatches on the file-context rule's type, which is the only place a new
variety of file is named.
extract_segments(source)extract_segments(source)
source |
A |
A list of two elements:
List of segments from new_segment(), each holding
lines aligned to the lines of the source file.
Data frame with columns step, file_context, rule,
message.
The scanning size limit is enforced here, before dispatch, so no method can skip it. A method must not re-implement it.
A method reads untrusted bytes and must never evaluate them. Return the code
as text in a segment; deciding what it means is analyze_segment()'s job.
analyze_segment(), the other axis of dispatch, and
vignette("internals").
# Adding a file format outside pkgaudit: a method for a new rule `type`, # here one that treats a .txt file as if it held R code. extract_segments.txt <- function(source) { list(segments = list(new_segment(language = "R", lines = readLines(source$path), file_context = source$file_context)), errors = NULL) } registerS3method("extract_segments", "txt", extract_segments.txt)# Adding a file format outside pkgaudit: a method for a new rule `type`, # here one that treats a .txt file as if it held R code. extract_segments.txt <- function(source) { list(segments = list(new_segment(language = "R", lines = readLines(source$path), file_context = source$file_context)), errors = NULL) } registerS3method("extract_segments", "txt", extract_segments.txt)
format.pkgaudit() renders the scan metadata and finding counts as a
character vector of lines; print.pkgaudit() writes those lines and returns
the object invisibly.
## S3 method for class 'pkgaudit' format(x, path = TRUE, ...) ## S3 method for class 'pkgaudit' print(x, path = TRUE, ...)## S3 method for class 'pkgaudit' format(x, path = TRUE, ...) ## S3 method for class 'pkgaudit' print(x, path = TRUE, ...)
x |
A |
path |
Logical; if |
... |
Ignored, for S3 compatibility. |
format.pkgaudit() returns a character vector of lines.
print.pkgaudit() returns x invisibly.
summary.pkgaudit() for a sectioned report of the findings
themselves rather than their counts.
# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) print(result) print(result, path = FALSE) # omit the local Path: line for sharing# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) print(result) print(result, path = FALSE) # omit the local Path: line for sharing
Hashes a package manifest, a canonically ordered list of every included file's relative path and SHA-256 content hash.
hash_manifest(path, exclude = c("^\\.git/", "^\\.Rproj\\.user/"))hash_manifest(path, exclude = c("^\\.git/", "^\\.Rproj\\.user/"))
path |
Path to the directory to hash. |
exclude |
Character vector of regular expressions matched against paths
relative to |
A directory, unlike a tarball, has no single canonical byte stream to hash. The manifest is returned alongside the hash so two disagreeing scans can be diffed to find which file differs.
A directory hash is a weaker provenance claim than a tarball hash: its scope
depends on exclude, and empty directories and symlinks are not represented,
so trees differing only in those hash identically. For vetting untrusted
code, prefer hashing the tarball.
A list with:
SHA-256 of the manifest text (character scalar).
The manifest text (character scalar), one line per file,
formatted as "<hash> <relative path>".
Number of files included (integer).
The exclusion patterns applied, for recording in metadata.
Relative paths of symlinks and files under symlinked directories, excluded from the manifest (character).
Relative paths that could not be hashed, excluded from the manifest (character).
# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) exdir <- file.path(tempdir(), "untrustedpkg-example") utils::untar(tarball, exdir = exdir) hash_manifest(file.path(exdir, "untrustedpkg"))$hash# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) exdir <- file.path(tempdir(), "untrustedpkg-example") utils::untar(tarball, exdir = exdir) hash_manifest(file.path(exdir, "untrustedpkg"))$hash
Loads the file-context, code-context, pattern, and match rules, and the
lifecycle phases of every context, from the bundled SQLite database as a
named list suitable for passing to audit_package() or audit_tarball().
load_rules(db_path = .db_path())load_rules(db_path = .db_path())
db_path |
Path to the rules database. Defaults to the database bundled with the installed package. |
Before reading, the database is verified against its SHA-256 sidecar so that a tampered or corrupted rules file is rejected rather than silently trusted.
A named list of five data frames. In the four rule frames each row
is a rule, the common columns are name and version, and message is
what a finding reports; phases is keyed by context instead.
name, version, type, message, path,
recursive, report, filename, code_context, assume_called.
type selects how a matched file is read. report is TRUE for a rule
whose matches are findings in their own right. code_context names the
code-context rules that can apply inside these files, space-separated –
NA where none can, "computed" where only top_level and
in_function can. assume_called says whether code inside a function
definition is taken to run when the code around it runs, and is NA
wherever code_context is.
name, version, language, message, kind,
xpath, segment. kind is "xpath" for a rule matched against the
parse tree and "segment" for one matched against a label the extractor
stamped; exactly one of the two columns is set accordingly.
name, version, language, message, attck,
functions, xpath. functions is the space-separated names the rule
matches as a bare call, which is how find_indirect() attributes an
indirect call back to it; empty for a rule matching on more than the
callee.
name, version, language, message, attck, regex.
A match rule is evaluated only against code in its language.
context, version, and one logical column per lifecycle
phase. One row per file-context or code-context rule – pattern and
match rules carry no phases of their own. The computed contexts
top_level and in_function have none, and need none: they inherit.
The list carries a "provenance" attribute – db_path, version and
sha256 – which audit_package() reports in a scan's metadata. The
sha256 is the hash computed during verification, not re-read from the
sidecar afterwards, so it records what this call measured.
The sidecar ships beside the database, so anyone who can rewrite one can
rewrite the other. It is an integrity check, not an authenticity check: it
catches a corrupted download or a database swapped for another, and it cannot
catch a deliberate edit of an installed copy. The out-of-band anchor is the
hash published in the README, which CI compares against the shipped database
on every push and pull request; checking an installed copy against that
detects a tampered install, and rules_version() reports what a scan
actually used.
Verification is time-of-check to time-of-use: the database is hashed and then re-opened by path to query it, so a file swapped in between is not detected. That does not weaken the bundled default, since an attacker able to win the race already has write access to the installed package. When loading rules from a path other parties can write, treat the check as protection against corruption or a substituted database given an authentic sidecar, not against an attacker modifying the file concurrently.
rules <- load_rules() vapply(rules, nrow, integer(1))rules <- load_rules() vapply(rules, nrow, integer(1))
analyze_segment() methodEach frame is reduced to its canonical columns and an omitted one is empty.
rbind() accepts a stray column silently, so conforming here is what keeps a
malformed frame out of the result.
new_findings( patterns = NULL, matches = NULL, coverage = NULL, errors = .empty_errors() )new_findings( patterns = NULL, matches = NULL, coverage = NULL, errors = .empty_errors() )
patterns, matches, coverage
|
Data frames with the columns
|
errors |
Data frame with columns |
A list of the four frames, each conformed to its canonical columns.
analyze_segment(), whose methods return this.
# An analyzer that found nothing still returns all four frames. str(new_findings(), max.level = 1)# An analyzer that found nothing still returns all four frames. str(new_findings(), max.level = 1)
A contiguous, line-aligned run of code in one language, classed by that
language so analyze_segment() can dispatch on it. An extract_segments()
method returns these.
new_segment( language, lines, file_context, context = NA_character_, file_rule = NA_character_, code_contexts = NULL, guarded_lines = integer(0L) )new_segment( language, lines, file_context, context = NA_character_, file_rule = NA_character_, code_contexts = NULL, guarded_lines = integer(0L) )
language |
The language of the code, which selects the analyzer. |
lines |
Character vector of the segment's lines, blank-padded to the line numbers they occupy in the file so a finding's line is the real one. |
file_context |
Package-root-relative path of the file it came from. |
context |
The segment's label, matched by a |
file_rule |
The file-context rule that claimed the file. |
code_contexts |
The code-context rules that can apply, from the source. |
guarded_lines |
Integer vector of line numbers whose code ships but the
lifecycle does not run – a |
A pkgaudit_segment object.
extract_segments(), whose methods return these.
seg <- new_segment(language = "R", lines = c("", "system('id')"), file_context = "R/f.R") class(seg)seg <- new_segment(language = "R", lines = c("", "system('id')"), file_context = "R/f.R") class(seg)
Returns the version string of the rules database currently shipped with the package. Findings reports should always record the rules version to ensure reproducibility across audit cycles.
rules_version(db_path = .db_path())rules_version(db_path = .db_path())
db_path |
Path to the SQLite rules database. Defaults to the database bundled with the installed package. |
Like load_rules(), this verifies the database against its SHA-256 sidecar
before reading; see its Security considerations.
A character string giving the rules database version (e.g.,
"0.1.0").
rules_version()rules_version()
Takes a pkgaudit object produced by audit_package() or audit_tarball()
and returns summaries of it: how often each rule fired and in which lifecycle
phase, how much of the package was read, and the errors, if any.
print.summary.pkgaudit() writes them as a sectioned report.
## S3 method for class 'pkgaudit' summary(object, path = TRUE, phase = NULL, ...) ## S3 method for class 'summary.pkgaudit' print(x, path = x$path, ...)## S3 method for class 'pkgaudit' summary(object, path = TRUE, phase = NULL, ...) ## S3 method for class 'summary.pkgaudit' print(x, path = x$path, ...)
object |
A |
path |
Logical; if |
phase |
Character vector of lifecycle phases to report, e.g.
|
... |
Ignored, for S3 compatibility. |
x |
A |
The report opens with the same metadata block as print.pkgaudit(), then
gives the R Patterns, Shell / Make Matches, Coverage and Errors
sections; a section with nothing to report says so. The file_contexts
summary is returned for programmatic use but is not part of the report.
Both findings tables are grouped by phase and rule alone. Where a finding
sits is on the object's own frames; the report answers what runs, and when.
An occurrence executes in every phase its context does, so it contributes one
row per phase and n can sum to more than the number of occurrences. Those
executing in no phase are gathered under none.
Coverage is counts by status and location, and deliberately no percentage:
coverage never reaches 100%. Which files went unexamined is in the object's
coverage frame.
summary.pkgaudit() returns a summary.pkgaudit object: a named list
of five summary data frames, the scan metadata, the recorded path, and
the recorded phase – the phases the frames were filtered to, or NULL
for an unfiltered summary. print() reads it to head the report with the
phases asked for, so an empty section is not mistaken for a clean scan.
file_context: each file context found, once.
phase, rule, n, attck: how often each pattern rule
matched, split by the phase its code executes in.
phase, rule, n, attck: as patterns, across the
shell and Make-like files.
status, top_level, type, files, lines: how much
of the package was read, grouped by where the files sit and what kind
they are. type is what a file was read as, or its extension.
step, file_context, rule, error: the object's
errors rows, renamed for display.
print.summary.pkgaudit() returns x invisibly.
phase restricts the report to the phases named, and is the only way to
narrow it: the summary has already been expanded by phase, so it cannot be
subset afterwards. A filtered report names its phases in the header, so it
cannot be mistaken for a full scan.
print.pkgaudit() for the finding counts alone.
# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) summary(result) summary(result, path = FALSE) # omit the local Path: line for sharing summary(result, phase = "at_load") # only what runs when the package loads summary(result, phase = "none") # ships, but runs at no phase s <- summary(result) s$patterns # pattern frequencies as a data frame s$matches # match frequencies as a data frame# untrustedpkg is a small package shipped with pkgaudit to be scanned. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) result <- audit_tarball(tarball) summary(result) summary(result, path = FALSE) # omit the local Path: line for sharing summary(result, phase = "at_load") # only what runs when the package loads summary(result, phase = "none") # ships, but runs at no phase s <- summary(result) s$patterns # pattern frequencies as a data frame s$matches # match frequencies as a data frame
Fail closed: refuse the whole archive rather than extracting a filtered
subset, so a partially-validated archive is never written to disk. This is
the check audit_tarball() runs before extracting an untrusted tarball.
validate_tar( tarfile, max_entries = 100000L, max_bytes = 2 * 1024^3, max_ratio = 256 )validate_tar( tarfile, max_entries = 100000L, max_bytes = 2 * 1024^3, max_ratio = 256 )
tarfile |
Path to a |
max_entries |
Maximum number of entries to read before failing closed. Default 100,000. |
max_bytes |
Maximum uncompressed bytes to read before failing closed. Default 2 GB. Raise for ecosystems with larger artifacts, e.g. Bioconductor annotation and experiment-data packages. |
max_ratio |
Maximum uncompressed:compressed ratio before failing closed,
or |
Rejects link entries (symlink, hard link), non-standard typeflags (GNU
long-name, PAX), path traversal, absolute and drive-qualified paths, paths
containing backslashes or control characters, empty paths, unparseable size
fields, and archives that do not extract to exactly one top-level directory.
It also enforces the entry-count, uncompressed-size, and expansion-ratio caps
applied while reading (max_entries, max_bytes, max_ratio). Reads gzip
and uncompressed tar only, judged by the file's magic bytes rather than its
name: a bzip2, xz, zstd or compress stream is refused whatever it is called.
A refusal is signaled as a pkgaudit_invalid_tarball condition (a subclass
of error), so it stops by default but can be caught by class.
Invisibly, a data frame of the archive's entries with columns
name, type, linkname, and size.
# Returns the archive's entries, or stops with a "Refusing archive" error if # the tarball is malicious or malformed. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) entries <- validate_tar(tarball) entries# Returns the archive's entries, or stops with a "Refusing archive" error if # the tarball is malicious or malformed. tarball <- system.file( "extdata", "untrustedpkg", "untrustedpkg_0.1.0.tar.gz", package = "pkgaudit" ) entries <- validate_tar(tarball) entries