Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Type: Package
Package: audit.base
Title: Base package for Posit Checks
Version: 0.7.8
Version: 0.7.9
Authors@R:
person("Jumping", "Rivers", , "info@jumpingrivers.com", role = c("aut", "cre"))
Description: Base package for sharing classes between posit audit
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# audit.base 0.7.9 _2026-10-16_
* fix: Posit versions are compared as numbers, so a patch release newer than the list or missing from it is reported as current or out of date, not "really old", and 2030.x versions are recognised. The scrape fallback warning gives the reason, and `get_quarto_posit_version_msg()` returns one message when `type` is left at its default (#62)

# audit.base 0.7.8 _2026-10-16_
* fix: `audit_posit_version()` and `get_quarto_posit_version_msg()` say the Posit version couldn't be found when it's `NA`, instead of erroring (jumpingrivers/audit.workbench#38)

Expand Down
2 changes: 1 addition & 1 deletion R/get_posit_remote_versions.R
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ get_all_remote_versions = function(page) {
versions = page %>%
rvest::html_nodes("h2") %>%
rvest::html_text()
v = stringr::str_extract(versions, "202[0-9]\\.[0-9]{2}\\.[0-9]{1,2}")
v = stringr::str_extract(versions, "20[0-9]{2}\\.[0-9]{2}\\.[0-9]{1,2}")
tibble::tibble(version = v[!is.na(v)], cve = "")
}
72 changes: 29 additions & 43 deletions R/posit_versions.R
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ get_posit_versions = function(
remote = TRUE
) {
type = match.arg(type)
versions = if (isTRUE(remote) && type != "drivers") {
try(get_posit_remote_versions(type), silent = TRUE)
} else {
NULL
versions = NULL
if (isTRUE(remote) && type != "drivers") {
versions = tryCatch(
get_posit_remote_versions(type),
error = function(e) {
cli::cli_alert_warning(
"Unable to scrape NEWS page for {type}: {conditionMessage(e)}. Falling back to cache"
)
NULL
}
)
}
if ("try-error" %in% class(versions) || is.null(versions)) {
if (type != "drivers") {
cli::cli_alert_warning(
"Unable to scrape NEWS page for {type}. Falling back to cache"
)
}
if (is.null(versions)) {
fname = system.file(
"extdata",
"versions",
Expand All @@ -35,7 +37,8 @@ get_posit_versions = function(
versions = utils::read.csv(fname, header = TRUE, comment.char = "#", colClasses = "character")
tibble::as_tibble(versions)
}
versions = dplyr::arrange(versions, dplyr::desc(.data$version))
# Newest first, compared as numbers so 2024.12.10 sorts above 2024.12.2
versions = versions[order(numeric_version(versions$version), decreasing = TRUE), ]
# Versions without a CVE have an empty string; make these NA so they aren't counted
versions$cve = dplyr::na_if(trimws(as.character(versions$cve)), "")
versions
Expand Down Expand Up @@ -64,7 +67,7 @@ audit_posit_version = function(
return(invisible(NULL))
}
versions = get_posit_versions(type = type)
row_number = lookup_version(posit_version, type = type)
row_number = lookup_version(posit_version, versions)

if (is.na(row_number)) {
cli::cli_alert_info(
Expand All @@ -85,45 +88,28 @@ audit_posit_version = function(
invisible(NULL)
}

lookup_version = function(posit_version, type) {
versions = get_posit_versions(type = type)
version_as_date = version_to_date(posit_version)
all_dates = unlist(purrr::map(versions$version, version_to_date))

if (is.na(version_as_date) || version_as_date < min(all_dates)) {
# Older than DB
row_number = NA_integer_
} else if (version_as_date > max(all_dates)) {
# Newer than DB
row_number = 1L
} else {
# Return matching version; multiple CVEs, so just pick 1
# Rtns an NA when not in the DB
# Some versions add on additional info, .e.g. 2023.06.0.pro1 - delete
posit_version = stringr::str_extract(
posit_version,
"^(202[0-9])\\.([01][0-9])\\.[0-9]"
)
row_number = which(versions$version == posit_version)[1]
# Row of `versions` (sorted newest first) that `posit_version` sits at:
# 1 when it's at or above the newest entry, otherwise one more than the number of
# strictly newer versions. NA when it can't be parsed or predates the list.
lookup_version = function(posit_version, versions) {
version = parse_posit_version(posit_version)
all_versions = numeric_version(versions$version)
if (is.na(version) || version < min(all_versions)) {
return(NA_integer_)
}
row_number
sum(all_versions > version) + 1L
}

version_to_date = function(version) {
# Old style version
if (is_missing_version(version) || !is_new_version(version)) {
# Year.month.patch, ignoring suffixes such as .pro1; NA for old style versions (1.9.0)
parse_posit_version = function(version) {
if (is_missing_version(version)) {
return(NA)
}
as_date = stringr::str_match_all(version, "^(202[0-9])\\.([01][0-9])")[[1]]
as.Date(paste(as_date[1, 2], as_date[1, 3], "01", sep = "-"))
version = stringr::str_extract(version, "^20[0-9]{2}\\.[01][0-9]\\.[0-9]+")
if (is.na(version)) NA else numeric_version(version)
}

# NA or character(0) when the version couldn't be found
is_missing_version = function(version) {
length(version) != 1L || is.na(version)
}

# Old style versions are 1.9.0 (2021-07)
is_new_version = function(version) {
stringr::str_detect(version, pattern = "^202[0-9]\\.[01][0-9].[0-9]")
}
10 changes: 6 additions & 4 deletions R/quarto-helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,24 @@ get_quarto_posit_version_msg = function(
out,
type = c("connect", "workbench", "drivers")
) {
type = match.arg(type)
posit_version = out$posit_version
if (is_missing_version(posit_version)) {
return(glue::glue("We couldn't find which version of Posit {type} is installed."))
}
row_number = lookup_version(posit_version, type)
versions = get_posit_versions(type = type)
row_number = lookup_version(posit_version, versions)
status = version_status(row_number)
if (status == "unknown") {
msg = "Posit {type} (v{posit_version}), isn't in our database.
This could be because we've missed it or it's really old."
} else if (status == "outdated") {
versions = get_posit_versions(type = type)
newer_versions = versions[seq_len(row_number - 1), ] # nolint
no_of_versions = length(unique(newer_versions$version)) # nolint
no_of_cves = count_cves(newer_versions) # nolint
msg = "Posit {type} is out of date (v{posit_version}).
There are {no_of_versions} newer versions that fix {no_of_cves} CVEs.
The latest version is v{versions[1, 1]}."
The latest version is v{versions$version[1]}."
} else {
msg = "Posit {type} is up to date."
}
Expand All @@ -107,7 +108,8 @@ get_quarto_posit_version_status = function(
out,
type = c("connect", "workbench", "drivers")
) {
version_status(lookup_version(out$posit_version, type))
type = match.arg(type)
version_status(lookup_version(out$posit_version, get_posit_versions(type = type)))
}

version_status = function(row_number) {
Expand Down
40 changes: 23 additions & 17 deletions tests/testthat/test-posit_versions.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,6 @@ test_that("Testing check server", {
regexp = "up to date"
)

# Version not in DB
expect_equal(lookup_version(posit_version = "2029.01.01", type), 1)
# Really old version not in the DB
expect_equal(
lookup_version(posit_version = "2010.01.01", type),
NA_integer_
)

expect_equal(
lookup_version(posit_version = "Not in DB", type),
NA_integer_
)

# No version in DB
v = lookup_version(posit_version = "2022.10.31", type)
expect_true(is.na(v))

expect_message(audit_posit_version("2022.10.0", type), regexp = "out of date")
})

Expand Down Expand Up @@ -81,3 +64,26 @@ test_that("A version that couldn't be found is reported, not an error", {
expect_match(msg, "couldn't find")
}
})

test_that("lookup_version() places a version against the list", {
versions = tibble::tibble(version = c("2026.10.0", "2026.09.0", "2026.08.2", "2026.08.0"))
cases = tibble::tribble(
~posit_version, ~row,
"2026.10.0", 1L,
"2026.10.1", 1L, # patch after the newest entry
"2026.10.0.pro1", 1L,
"2030.01.0", 1L,
"2026.08.3", 3L, # patch missing from the list
"2026.08.0", 4L,
"2025.01.0", NA, # older than the list
"1.9.0", NA,
"Not a version", NA
)
for (i in seq_len(nrow(cases))) {
expect_identical(
lookup_version(cases$posit_version[i], versions),
cases$row[i],
label = cases$posit_version[i]
)
}
})
Loading