Skip to content
Open
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
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# oysteR (development version)
* feat: Add support for custom OSS Index URL via parameter, environment variable, or config file

# oysteR 0.1.4 _2025-10-08_
* bug: Incorrectly states how many packages were found in the database (see #62)
* feat: Pass tokens as arguments
Expand Down
10 changes: 6 additions & 4 deletions R/audit.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#' @param token If NULL, looks at OSSINDEX_USER & OSSINDEX_TOKEN, env variables. If those
#' aren't available, try `"~/.ossindex/.oss-index-config"`
#' @param verbose Default \code{TRUE}.
#' @param ossindex_url Optional custom OSS Index URL. If NULL, looks at OSSINDEX_URL env variable,
#' then `"~/.ossindex/.oss-index-config"`, then defaults to "https://ossindex.sonatype.org/api/v3/component-report"
#'
#' @export
#' @examples
Expand All @@ -34,7 +36,7 @@
#' version = c("1.4-5", "1.4.1")
#' audit(pkg, version, type = "cran")
#' }
audit = function(pkg, version, type, verbose = TRUE, token = NULL) {
audit = function(pkg, version, type, verbose = TRUE, token = NULL, ossindex_url = NULL) {
if (is.null(pkg)) {
pkg = character(0)
}
Expand All @@ -56,7 +58,7 @@ audit = function(pkg, version, type, verbose = TRUE, token = NULL) {
pkgs = tibble::tibble(package = pkg, version = version, type = type)[!is_cached, ]

## Call OSS index on remaining
results = call_oss_index(purls, verbose = verbose, token = token)
results = call_oss_index(purls, verbose = verbose, token = token, ossindex_url = ossindex_url)
audit = dplyr::bind_cols(pkgs, results)

# Update cache and combine
Expand Down Expand Up @@ -91,7 +93,7 @@ audit = function(pkg, version, type, verbose = TRUE, token = NULL) {
#' # This calls installed.packages()
#' pkgs = audit_installed_r_pkgs()
#' }
audit_installed_r_pkgs = function(verbose = TRUE, token = NULL) {
audit_installed_r_pkgs = function(verbose = TRUE, token = NULL, ossindex_url = NULL) {
pkgs = get_r_pkgs(verbose = verbose)
audit(pkg = pkgs$package, version = pkgs$version, type = "cran", verbose = verbose, token = token)
audit(pkg = pkgs$package, version = pkgs$version, type = "cran", verbose = verbose, token = token, ossindex_url = ossindex_url)
}
4 changes: 2 additions & 2 deletions R/call_os_index.R
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ globalVariables("vulnerabilities")
#' @importFrom purrr map map_dbl
#' @importFrom dplyr %>%
#' @keywords internal
call_oss_index = function(purls, verbose, token) {
call_oss_index = function(purls, verbose, token, ossindex_url = NULL) {
if (length(purls) == 0L) {
return(no_purls_case())
}
Expand All @@ -107,7 +107,7 @@ call_oss_index = function(purls, verbose, token) {
}

max_size = 128
os_index_url = "https://ossindex.sonatype.org/api/v3/component-report"
os_index_url = get_ossindex_url(ossindex_url)
token = get_token(token, verbose)
authenticate = httr::authenticate(token$user, token$token, type = "basic")
user_agent = get_user_agent()
Expand Down
4 changes: 2 additions & 2 deletions R/expect_secure.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
#' # Typically used inside testthat
#' oysteR::expect_secure("oysteR")
#' }
expect_secure = function(pkg, repo = "https://cran.rstudio.com", verbose = FALSE) {
expect_secure = function(pkg, repo = "https://cran.rstudio.com", verbose = FALSE, ossindex_url = NULL) {
## Need to set the repo, as testthat seems to strip this out?
repos = getOption("repos")
on.exit(options(repos = repos))
options(repos = c(CRAN = repo))

## Look up vulnerabilities
pkg_loc = system.file("DESCRIPTION", package = pkg)
aud = audit_description(dirname(pkg_loc), verbose = verbose)
aud = audit_description(dirname(pkg_loc), verbose = verbose, ossindex_url = ossindex_url)
no_of_vul = sum(aud$no_of_vulnerabilities)

## Report results
Expand Down
19 changes: 11 additions & 8 deletions R/file_audits.R
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ get_pkg_deps = function(pkgs) {
audit_description = function(
dir = ".",
fields = c("Depends", "Imports", "Suggests"),
verbose = TRUE
verbose = TRUE,
ossindex_url = NULL
) {
## Read DESCRIPTION and extract fields
fname = check_file_exists(dir, "DESCRIPTION")
Expand All @@ -58,7 +59,7 @@ audit_description = function(
pkgs = inst_pkgs[rownames(inst_pkgs) %in% all_dep, "Version"]
versions = as.vector(pkgs)
pkgs = names(pkgs)
audit(pkgs, versions, type = "CRAN", verbose = verbose)
audit(pkgs, versions, type = "CRAN", verbose = verbose, ossindex_url = ossindex_url)
}

#' Audit an renv.lock File
Expand All @@ -70,6 +71,7 @@ audit_description = function(
#'
#' @param dir The file path of an renv.lock file.
#' @param verbose Default \code{TRUE}.
#' @param ossindex_url Optional custom OSS Index URL. If NULL, uses default or configured URL.
#'
#' @importFrom jsonlite read_json
#' @importFrom dplyr %>% mutate
Expand All @@ -82,11 +84,11 @@ audit_description = function(
#' # Looks for renv.lock file in dir
#' audit_renv_lock(dir = ".")
#' }
audit_renv_lock = function(dir = ".", verbose = TRUE) {
audit_renv_lock = function(dir = ".", verbose = TRUE, ossindex_url = NULL) {
fname = check_file_exists(dir, "renv.lock")
renv_lock = jsonlite::read_json(fname)
renv_pkgs = purrr::map_chr(renv_lock$Packages, purrr::pluck, "Version")
audit(pkg = names(renv_pkgs), version = renv_pkgs, type = "cran", verbose = verbose)
audit(pkg = names(renv_pkgs), version = renv_pkgs, type = "cran", verbose = verbose, ossindex_url = ossindex_url)
}

#' Audit a requirements.txt File
Expand All @@ -111,12 +113,12 @@ audit_renv_lock = function(dir = ".", verbose = TRUE) {
#' # Looks for a requirements.txt file in dir
#' audit_description(dir = ".")
#' }
audit_req_txt = function(dir = ".", verbose = TRUE) {
audit_req_txt = function(dir = ".", verbose = TRUE, ossindex_url = NULL) {
fname = check_file_exists(dir, "requirements.txt")
audit = readLines(fname) %>%
strsplit(">=|==|>") %>%
map_dfr(~ tibble::tibble(package = .x[1], version = .x[2])) %>%
mutate(audit(pkg = .data$package, version = .data$version, type = "pypi", verbose = verbose))
mutate(audit(pkg = .data$package, version = .data$version, type = "pypi", verbose = verbose, ossindex_url = ossindex_url))
return(audit)
}

Expand All @@ -129,6 +131,7 @@ audit_req_txt = function(dir = ".", verbose = TRUE) {
#' @param dir The directory containing a Conda environment yaml file.
#' @param fname The file name of conda environment yaml file.
#' @param verbose Default \code{TRUE}.
#' @param ossindex_url Optional custom OSS Index URL. If NULL, uses default or configured URL.
#'
#' @importFrom purrr keep map map_dfr pluck discard
#' @importFrom rlang .data
Expand All @@ -139,7 +142,7 @@ audit_req_txt = function(dir = ".", verbose = TRUE) {
#' # Looks for a environment.yml file in dir
#' audit_conda(dir = ".")
#' }
audit_conda = function(dir = ".", fname = "environment.yml", verbose = TRUE) {
audit_conda = function(dir = ".", fname = "environment.yml", verbose = TRUE, ossindex_url = NULL) {
# check if file exists if it does create file path
# allow for fname because conda envs are not always title `environment.yml`
env_fname = check_file_exists(dir, fname)
Expand All @@ -166,7 +169,7 @@ audit_conda = function(dir = ".", fname = "environment.yml", verbose = TRUE) {
}

all_deps = dplyr::bind_rows(conda_deps, pip_deps)
aud = audit(all_deps$package, all_deps$version, all_deps$type, verbose = verbose)
aud = audit(all_deps$package, all_deps$version, all_deps$type, verbose = verbose, ossindex_url = ossindex_url)

return(aud)
}
24 changes: 24 additions & 0 deletions R/get_token.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,27 @@ get_token = function(token, verbose = TRUE) {
}
token
}

get_ossindex_url = function(ossindex_url = NULL) {
# If URL is explicitly provided, use it
if (!is.null(ossindex_url)) {
return(ossindex_url)
}

# Check environment variable
env_url = Sys.getenv("OSSINDEX_URL", NA)
if (!is.na(env_url)) {
return(env_url)
}

# Check config file
if (file.exists("~/.ossindex/.oss-index-config")) {
config = yaml::read_yaml("~/.ossindex/.oss-index-config")
if (!is.null(config$ossi$Url)) {
return(config$ossi$Url)
}
}

# Default URL
return("https://ossindex.sonatype.org/api/v3/component-report")
}
26 changes: 25 additions & 1 deletion README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Set the following environment variables in your `.Renviron` file:

- `OSSINDEX_USER` (this is set to your email address)
- `OSSINDEX_TOKEN` (this is set to your API token)

Or create a config file at `~/.ossindex/.oss-index-config` and add
```
# This config file is picked up by other Sonatype apps
Expand All @@ -86,6 +86,30 @@ ossi:

These will be used by `{oysteR}` to authenticate with OSS Index, bumping up the amount of requests you can make.

### Custom OSS Index URL

If you need to use a custom OSS Index server (e.g., for enterprise deployments), you can override the default URL in three ways:

1. **Pass as a parameter** (highest priority):
```{r, eval = FALSE}
audit_installed_r_pkgs(ossindex_url = "https://custom.ossindex.org/api/v3/component-report")
```

2. **Set environment variable** in your `.Renviron` file:
```
OSSINDEX_URL=https://custom.ossindex.org/api/v3/component-report
```

3. **Add to config file** at `~/.ossindex/.oss-index-config`:
```
ossi:
Username: XXXX
Token: YYY
Url: https://custom.ossindex.org/api/v3/component-report
```

The parameter takes precedence over the environment variable, which takes precedence over the config file.

## Contributing

We care a lot about making the world a safer place, and that's why we continue to work on this and other plugins for Sonatype OSS Index. If you as well want to speed up the pace of software development by working on this project, jump on in! Before you start work, create a [new issue](https://github.com/sonatype-nexus-community/oysteR/issues), or comment on an existing issue, to let others know you are!
Expand Down
35 changes: 35 additions & 0 deletions tests/testthat/test-get_token.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,38 @@ test_that("Check tokens", {
l = list(username = "ABC", token = "ANC")
expect_equal(get_token(l), l)
})

test_that("Check default OSS Index URL", {
# Default URL should be returned when no custom URL is provided
default_url = "https://ossindex.sonatype.org/api/v3/component-report"
expect_equal(get_ossindex_url(), default_url)
})

test_that("Check custom OSS Index URL parameter", {
# Custom URL passed as parameter should be returned
custom_url = "https://custom.ossindex.org/api/v3/component-report"
expect_equal(get_ossindex_url(custom_url), custom_url)
})

test_that("Check OSS Index URL from environment variable", {
# Set environment variable
custom_url = "https://env.ossindex.org/api/v3/component-report"
withr::with_envvar(
c(OSSINDEX_URL = custom_url),
{
expect_equal(get_ossindex_url(), custom_url)
}
)
})

test_that("Parameter takes precedence over environment variable", {
# Parameter should override environment variable
param_url = "https://param.ossindex.org/api/v3/component-report"
env_url = "https://env.ossindex.org/api/v3/component-report"
withr::with_envvar(
c(OSSINDEX_URL = env_url),
{
expect_equal(get_ossindex_url(param_url), param_url)
}
)
})
Loading