├── .Rbuildignore
├── .github
├── CONTRIBUTING.md
├── issue_template.md
└── pull_request_template.md
├── .gitignore
├── .travis.yml
├── DESCRIPTION
├── LICENSE
├── LICENSE.md
├── NAMESPACE
├── NEWS.md
├── R
├── addins.R
├── check_namespace.R
├── defender-package.R
├── system_calls.R
└── utils.R
├── README-NOT.md
├── README.Rmd
├── README.md
├── _pkgdown.yml
├── codecov.yml
├── codemeta.json
├── defender.Rproj
├── inst
└── rstudio
│ └── addins.dcf
├── man
├── check_namespace.Rd
├── dangerous_imports.Rd
├── defender-package.Rd
├── figures
│ ├── logo.png
│ └── supergb.png
├── summarize_system_calls.Rd
└── system_calls.Rd
└── tests
├── testthat.R
└── testthat
├── test-check_namespace.R
├── test-system_calls.R
└── test-utils.R
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^\.github$
2 | ^.*\.Rproj$
3 | ^\.Rproj\.user$
4 | ^\.travis\.yml$
5 | ^codecov\.yml$
6 | ^LICENSE\.md$
7 | ^README\.Rmd$
8 | ^README-.*\.png$
9 | ^_pkgdown\.yml$
10 | ^docs$
11 | ^codemeta\.json$
12 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTING #
2 |
3 | ### Fixing typos
4 |
5 | Small typos or grammatical errors in documentation may be edited directly using
6 | the GitHub web interface, so long as the changes are made in the _source_ file.
7 |
8 | * YES: you edit a roxygen comment in a `.R` file below `R/`.
9 | * NO: you edit an `.Rd` file below `man/`.
10 |
11 | ### Prerequisites
12 |
13 | Before you make a substantial pull request, you should always file an issue and
14 | make sure someone from the team agrees that it’s a problem. If you’ve found a
15 | bug, create an associated issue and illustrate the bug with a minimal
16 | [reprex](https://www.tidyverse.org/help/#reprex).
17 |
18 | ### Pull request process
19 |
20 | * We recommend that you create a Git branch for each pull request (PR).
21 | * Look at the Travis and AppVeyor build status before and after making changes.
22 | The `README` should contain badges for any continuous integration services used
23 | by the package.
24 | * We recommend the tidyverse [style guide](http://style.tidyverse.org).
25 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to
26 | apply these styles, but please don't restyle code that has nothing to do with
27 | your PR.
28 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2).
29 | * We use [testthat](https://cran.r-project.org/package=testthat). Contributions
30 | with test cases included are easier to accept.
31 | * For user-facing changes, add a bullet to the top of `NEWS.md` below the
32 | current development version header describing the changes made followed by your
33 | GitHub username, and links to relevant issue(s)/PR(s).
34 |
35 | ### Code of Conduct
36 |
37 | Please note that the defender project is released with a
38 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this
39 | project you agree to abide by its terms.
40 |
41 | ### See rOpenSci [contributing guide](https://ropensci.github.io/dev_guide/contributingguide.html)
42 | for further details.
43 |
44 | ### Discussion forum
45 |
46 | Check out our [discussion forum](https://discuss.ropensci.org) if
47 |
48 | * you have a question, an use case, or otherwise not a bug or feature request for the software itself.
49 | * you think your issue requires a longer form discussion.
50 |
51 | ### Prefer to Email?
52 |
53 | Email the person listed as maintainer in the `DESCRIPTION` file of this repo.
54 |
55 | Though note that private discussions over email don't help others - of course email is totally warranted if it's a sensitive problem of any kind.
56 |
57 | ### Thanks for contributing!
58 |
59 | This contributing guide is adapted from the tidyverse contributing guide available at https://raw.githubusercontent.com/r-lib/usethis/master/inst/templates/tidy-contributing.md
60 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Session Info
6 |
7 | ```r
8 |
9 | ```
10 |
11 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ## Description
8 |
9 |
10 | ## Related Issue
11 |
14 |
15 | ## Example
16 |
18 |
19 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 | .Rhistory
3 | .RData
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r
2 |
3 | language: R
4 | sudo: false
5 | cache: packages
6 |
7 | after_success:
8 | - Rscript -e 'covr::codecov()'
9 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: defender
2 | Title: Check Package for Potential Security Violations
3 | Version: 0.0.1.9000
4 | Authors@R: c(
5 | person("Ildiko", "Czeller", email = "czeildi@gmail.com", role = c("aut", "cre")),
6 | person("Karthik", "Ram", email = "karthik.ram@gmail.com", role = c("aut"),
7 | comment = c(ORCID = " 0000-0002-0233-1757")),
8 | person("Bob", "Rudis", email = "bob@rud.is", role = c("aut"),
9 | comment = c(ORCID = "0000-0001-5670-2640")),
10 | person("Kara", "Woo", role = "aut")
11 | )
12 | Maintainer: ropensci
13 | Description: Check an R package for potential security risks and violations
14 | via static code analysis.
15 | Depends: R (>= 3.1.0)
16 | License: MIT + file LICENSE
17 | Encoding: UTF-8
18 | LazyData: true
19 | Suggests:
20 | covr,
21 | testthat,
22 | knitr,
23 | roxygen2,
24 | rstudioapi,
25 | shiny,
26 | miniUI
27 | RoxygenNote: 6.1.1
28 | Roxygen: list(markdown = TRUE)
29 | Imports:
30 | magrittr,
31 | utils,
32 | withr
33 | URL: https://docs.ropensci.org/defender/, https://github.com/ropenscilabs/defender
34 | BugReports: https://github.com/ropenscilabs/defender/issues
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2018
2 | COPYRIGHT HOLDER: Ildi Czeller
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2018 Ildi Czeller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | export(check_namespace)
4 | export(dangerous_imports)
5 | export(summarize_system_calls)
6 | export(system_calls)
7 | importFrom(magrittr,"%>%")
8 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | # defender 0.0.0.9000
2 |
3 | * Added a `NEWS.md` file to track changes to the package.
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/R/addins.R:
--------------------------------------------------------------------------------
1 | system_calls_addin <- function() {
2 | ui <- miniUI::miniPage(
3 | miniUI::gadgetTitleBar("List system calls"),
4 | miniUI::miniContentPanel(
5 | shiny::textInput("evil_path", "path:", value = ".")
6 | )
7 | )
8 |
9 | server <- function(input, output, session) {
10 | shiny::observeEvent(input$done, {
11 | rstudioapi::sendToConsole(
12 | paste0("defender::summarize_system_calls('", input$evil_path, "')")
13 | )
14 | shiny::stopApp()
15 | })
16 | }
17 |
18 | viewer <- shiny::dialogViewer("system calls")
19 | shiny::runGadget(ui, server, viewer = viewer)
20 | }
21 |
22 | check_namespace_addin <- function() {
23 | ui <- miniUI::miniPage(
24 | miniUI::gadgetTitleBar("List system imports"),
25 | miniUI::miniContentPanel(
26 | shiny::textInput("evil_path", "path:", value = ".")
27 | )
28 | )
29 |
30 | server <- function(input, output, session) {
31 | shiny::observeEvent(input$done, {
32 | rstudioapi::sendToConsole(
33 | paste0("defender::check_namespace('", input$evil_path, "')")
34 | )
35 | shiny::stopApp()
36 | })
37 | }
38 |
39 | viewer <- shiny::dialogViewer("list system imports")
40 | shiny::runGadget(ui, server, viewer = viewer)
41 | }
42 |
--------------------------------------------------------------------------------
/R/check_namespace.R:
--------------------------------------------------------------------------------
1 | #' Read a package NAMESPACE and check for dangerous imports
2 | #'
3 | #' Given a path to a package source tree, return a data.frame of Imports (both whole
4 | #' packages and fully qualified references).
5 | #'
6 | #' @md
7 | #' @param pkg_path path to package source tree
8 | #' @param imports_to_flag character vector of dangerous items to find
9 | #' @export
10 | #' @examples \dontrun{
11 | #' check_namespace("../testevil")
12 | #' check_namespace(
13 | #' "../testevil",
14 | #' dangerous_imports(additional_dangerous_imports = "sys::exec_background")
15 | #' )
16 | #' }
17 | check_namespace <- function(pkg_path, imports_to_flag = dangerous_imports()) {
18 | assert_path_exists(pkg_path)
19 | assert_is_package(pkg_path)
20 |
21 | imports_list <- parse_ns_file(pkg_path)$imports
22 |
23 | parsed_imports <- parse_all_imports(imports_list)
24 | all_imports <- summarize_imports(
25 | parsed_imports[["imported_packages"]], parsed_imports[["imported_functions"]]
26 | )
27 |
28 | all_imports %>%
29 | subset(.$import %in% imports_to_flag) %>%
30 | `row.names<-`(NULL)
31 | }
32 |
33 | parse_all_imports <- function(imports_list) {
34 | whole_pkg_imports <- extract_whole_pkg_imports(imports_list)
35 | fqrs <- extract_fully_qualified_references(imports_list)
36 |
37 | imported_packages <- c(
38 | whole_pkg_imports,
39 | extract_pkgs_from_fully_qualified_references(fqrs)
40 | ) %>%
41 | unique()
42 | imported_functions <- transform_fully_qualified_refereces(fqrs)
43 |
44 | list(
45 | "imported_packages" = imported_packages,
46 | "imported_functions" = imported_functions
47 | )
48 | }
49 |
50 | summarize_imports <- function(imported_packages, imported_functions) {
51 | if (length(imported_packages) > 0) {
52 | pkgs <- data.frame(type = "package", import = as.character(imported_packages), stringsAsFactors = FALSE)
53 | } else {
54 | pkgs <- data.frame(type = character(0), import = character(0))
55 | }
56 |
57 | if (length(imported_functions) > 0) {
58 | funs <- data.frame(type = "function", import = as.character(imported_functions), stringsAsFactors = FALSE)
59 | } else {
60 | funs <- data.frame(type = character(0), import = character(0))
61 | }
62 |
63 | all_imports <- rbind(pkgs, funs)
64 | if (nrow(all_imports) > 0) {
65 | all_imports$package <- vapply(
66 | strsplit(all_imports$import, "::"),
67 | function(x) x[[1]],
68 | "character"
69 | )
70 | } else {
71 | all_imports$package <- character(0)
72 | }
73 | all_imports
74 | }
75 |
76 | parse_ns_file <- function(pkg_path) {
77 | parseNamespaceFile(
78 | basename(pkg_path), dirname(pkg_path),
79 | mustExist = FALSE
80 | )
81 | }
82 |
83 | extract_whole_pkg_imports <- function(imports) {
84 | unlist(imports[lengths(imports) == 1], use.names = FALSE)
85 | }
86 |
87 | extract_fully_qualified_references <- function(imports) {
88 | imports[(lengths(imports) == 2)]
89 | }
90 |
91 | extract_pkgs_from_fully_qualified_references <- function(fqrs) {
92 | sapply(fqrs, function(x) x[[1]])
93 | }
94 |
95 | transform_fully_qualified_refereces <- function(fqrs) {
96 | sapply(fqrs, function(x) {
97 | sprintf("%s::%s", x[[1]], x[[2]])
98 | })
99 | }
100 |
--------------------------------------------------------------------------------
/R/defender-package.R:
--------------------------------------------------------------------------------
1 | utils::globalVariables(c("token", "text", "."))
2 |
3 | #' @importFrom magrittr %>%
4 | #' @keywords internal
5 | "_PACKAGE"
6 |
--------------------------------------------------------------------------------
/R/system_calls.R:
--------------------------------------------------------------------------------
1 | #' summarize occurrences of system calls in project
2 | #'
3 | #' Summarize and show occurrences of system calls in the R files in a given
4 | #' folder. There could be more occurrences. These calls should be further checked
5 | #' for maliciousness.
6 | #'
7 | #' @param path character, relative or absolute path for local code directory
8 | #' @param calls_to_flag character vector of functions names to flag as
9 | #' potentially dangerous system calls
10 | #'
11 | #' @return data.frame with a row for each system call
12 | #' @export
13 | #'
14 | #' @examples \dontrun{
15 | #' # git clone git@github.com:ropenscilabs/testevil.git
16 | #' summarize_system_calls("testevil")
17 | #' summarize_system_calls("testevil", system_calls("exec_background"))
18 | #' }
19 | summarize_system_calls <- function(path = ".", calls_to_flag = system_calls()) {
20 | assert_path_exists(path)
21 |
22 | r_paths <- get_r_script_paths(path = path)
23 | summaries_by_file <- lapply(r_paths, digest_system_calls, path, calls_to_flag)
24 | Reduce(rbind, summaries_by_file)
25 | }
26 |
27 | digest_system_calls <- function(r_file, path, calls_to_flag = system_calls()) {
28 | result <- find_system_calls(parse(r_file), calls_to_flag)
29 | if (nrow(result) == 0L) {
30 | return(result)
31 | }
32 | result$path <- sub(paste0("^", path, "/"), "", r_file)
33 | subset(result, select = c("path", "line1", "text", "function_name")) %>%
34 | magrittr::set_names(c("path", "line_number", "call", "function_name"))
35 | }
36 |
37 | find_system_calls <- function(expr, calls_to_flag = system_calls()) {
38 | parsed_data <- withr::with_options(
39 | c("keep.source" = TRUE),
40 | utils::getParseData(expr, includeText = TRUE)
41 | )
42 |
43 | base_fun_row_indexes <- which(
44 | (parsed_data$token == "SYMBOL_FUNCTION_CALL") & (parsed_data$text %in% calls_to_flag)
45 | )
46 | base_sys_calls <- parsed_data[base_fun_row_indexes - 1, ]
47 | base_sys_calls$function_name <- parsed_data[base_fun_row_indexes, ]$text
48 |
49 | pkg_fun_row_indexes <- which(
50 | (parsed_data$token == "expr") & (parsed_data$text %in% calls_to_flag[grepl("::", calls_to_flag)])
51 | )
52 | pkg_sys_calls <- parsed_data[pkg_fun_row_indexes - 1, ]
53 | pkg_sys_calls$function_name <- parsed_data[pkg_fun_row_indexes, ]$text
54 |
55 | rbind(base_sys_calls, pkg_sys_calls) %>%
56 | `row.names<-`(NULL)
57 | }
58 |
--------------------------------------------------------------------------------
/R/utils.R:
--------------------------------------------------------------------------------
1 | #' Dangerous Imports in Namespace
2 | #'
3 | #' Character vector of all imports considered dangerous to be used with
4 | #' \code{\link{check_namespace}}.
5 | #'
6 | #' @param additional_dangerous_imports character vector of user-defined
7 | #' dangerous imports
8 | #'
9 | #' @return character vector of all imports considered dangerous
10 | #' @export
11 | #'
12 | #' @examples dangerous_imports()
13 | dangerous_imports <- function(additional_dangerous_imports = character(0)) {
14 | c(pkgs_doing_system_calls(), "processx::run", additional_dangerous_imports) %>%
15 | unique()
16 | }
17 |
18 | #' Functions making System Calls
19 | #'
20 | #' Character vector of functions making system calls to check for in R source
21 | #' files.
22 | #'
23 | #' @param additional_system_calls character vector of user-defined
24 | #' system calls
25 | #'
26 | #' @return character vector of all system calls
27 | #' @export
28 | #'
29 | #' @examples system_calls("exec_background")
30 | system_calls <- function(additional_system_calls = character(0)) {
31 | c(
32 | base_system_calls(),
33 | "processx::run", "sys::exec_internal",
34 | additional_system_calls
35 | ) %>%
36 | unique()
37 | }
38 |
39 | base_system_calls <- function() {
40 | c("system", "system2")
41 | }
42 |
43 | pkgs_doing_system_calls <- function() {
44 | c("sys", "processx")
45 | }
46 |
47 | get_r_script_paths <- function(path = ".") {
48 | list.files(path = path, pattern = "[rR]$", recursive = TRUE, full.names = TRUE)
49 | }
50 |
51 | assert_path_exists <- function(path) {
52 | if (!dir.exists(path)) {
53 | stop(
54 | paste0("Path ", path, " not found, you may want to clone the repository first."),
55 | call. = FALSE
56 | )
57 | }
58 | }
59 |
60 | assert_is_package <- function(path) {
61 | if (!"DESCRIPTION" %in% list.files(path)) {
62 | stop(paste0(path, "is not a package directory"), call. = FALSE)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/README-NOT.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | rOpenSci Unconf 18 Project : defender
4 | =====================================
5 |
6 | [](https://www.repostatus.org/#abandoned)
7 |
8 |
9 |
10 |
11 | defender
12 | ========================================================
13 |
14 |
15 | [](https://travis-ci.org/ropenscilabs/defender) [](https://codecov.io/github/ropenscilabs/defender?branch=master) [](https://www.tidyverse.org/lifecycle/)
16 |
17 |
18 | The goal of defender is to do static code analysis on other R packages to check for potential security risks and best practices. It provides checks on multiple levels:
19 |
20 | 1. \[x\] static code analysis without installing the package
21 | 2. \[ \] more thorough but potentially dangerous checks with installation / in Docker container
22 |
23 | The checks do not tell you whether something is harmful but rather they flag code that you should double-check before running / loading the package.
24 |
25 | Installation
26 | ------------
27 |
28 | You can install defender from github with:
29 |
30 | ``` r
31 | # install.packages("devtools")
32 | devtools::install_github("ropenscilabs/defender")
33 | ```
34 |
35 | Example
36 | -------
37 |
38 | ### System calls in R scripts
39 |
40 | You can check for system calls in any directory locally available:
41 |
42 | ``` r
43 | defender::summarize_system_calls("../testevil")
44 | #> path line_number call
45 | #> 1 inst/root_sys.R 1 system2("ls")
46 | #> 2 inst/root_sys.R 4 system("ls")
47 | #> 3 R/exported.R 7 system2("ls")
48 | #> 4 R/internal.R 4 system("ls")
49 | #> 5 R/internal.R 8 system("ls -la")
50 | #> 6 R/processx.R 3 processx::run("ls")
51 | #> 7 R/sys.R 8 sys::exec_internal("ls")
52 | #> 8 R/system_hidden.R 2 system2("lm")
53 | #> function_name
54 | #> 1 system2
55 | #> 2 system
56 | #> 3 system2
57 | #> 4 system
58 | #> 5 system
59 | #> 6 processx::run
60 | #> 7 sys::exec_internal
61 | #> 8 system2
62 | ```
63 |
64 | You can also include additional elements to flag as dangerous:
65 |
66 | ``` r
67 | sc <- defender::system_calls("poll")
68 | defender::summarize_system_calls("../testevil", calls_to_flag = sc)
69 | #> path line_number call
70 | #> 1 inst/root_sys.R 1 system2("ls")
71 | #> 2 inst/root_sys.R 4 system("ls")
72 | #> 3 R/exported.R 7 system2("ls")
73 | #> 4 R/internal.R 4 system("ls")
74 | #> 5 R/internal.R 8 system("ls -la")
75 | #> 6 R/processx.R 9 poll("ls")
76 | #> 7 R/processx.R 3 processx::run("ls")
77 | #> 8 R/sys.R 8 sys::exec_internal("ls")
78 | #> 9 R/system_hidden.R 2 system2("lm")
79 | #> function_name
80 | #> 1 system2
81 | #> 2 system
82 | #> 3 system2
83 | #> 4 system
84 | #> 5 system
85 | #> 6 poll
86 | #> 7 processx::run
87 | #> 8 sys::exec_internal
88 | #> 9 system2
89 | ```
90 |
91 | ### System-related imports in NAMESPACE
92 |
93 | You can check the NAMESPACE file in a package for dangerous imports:
94 |
95 | ``` r
96 | defender::check_namespace("../testevil")
97 | #> type import package
98 | #> 1 package sys sys
99 | #> 2 package processx processx
100 | #> 3 function processx::run processx
101 | ```
102 |
103 | You can also include additional elements to flag as dangerous:
104 |
105 | ``` r
106 | di <- defender::dangerous_imports("processx::poll")
107 | defender::check_namespace("../testevil", imports_to_flag = di)
108 | #> type import package
109 | #> 1 package sys sys
110 | #> 2 package processx processx
111 | #> 3 function processx::poll processx
112 | #> 4 function processx::run processx
113 | ```
114 |
115 | Collaborators
116 | -------------
117 |
118 | - Ildi Czeller @czeildi
119 | - Karthik Ram @karthik
120 | - Bob Rudis @hrbrmstr
121 | - Kara Woo @karawoo
122 |
--------------------------------------------------------------------------------
/README.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | output: github_document
3 | ---
4 |
5 |
6 | # rOpenSci Unconf 18 Project : defender
7 |
8 |
9 |
10 |
11 | ```{r, echo = FALSE}
12 | knitr::opts_chunk$set(
13 | collapse = TRUE,
14 | comment = "#>",
15 | fig.path = "README-"
16 | )
17 | ```
18 |
19 | # defender
20 |
21 |
22 | [](https://travis-ci.org/ropenscilabs/defender)
23 | [](https://codecov.io/github/ropenscilabs/defender?branch=master)
24 | [](https://www.tidyverse.org/lifecycle/)
25 |
26 |
27 | The goal of defender is to do static code analysis on other R packages to check for potential security risks and best practices. It provides checks on multiple levels:
28 |
29 | 1. [x] static code analysis without installing the package
30 | 2. [ ] more thorough but potentially dangerous checks with installation / in Docker container
31 |
32 | The checks do not tell you whether something is harmful but rather they flag code that you should double-check before running / loading the package.
33 |
34 | ## Installation
35 |
36 | You can install defender from github with:
37 |
38 | ```{r gh-installation, eval = FALSE}
39 | # install.packages("devtools")
40 | devtools::install_github("ropenscilabs/defender")
41 | ```
42 |
43 | ## Example
44 |
45 | ### System calls in R scripts
46 |
47 | You can check for system calls in any directory locally available:
48 |
49 | ```{r system-calls-example, eval = FALSE}
50 | defender::summarize_system_calls("../testevil")
51 | ```
52 |
53 | You can also include additional elements to flag as dangerous:
54 |
55 | ```{r system-calls-example-2, eval = FALSE}
56 | sc <- defender::system_calls("poll")
57 | defender::summarize_system_calls("../testevil", calls_to_flag = sc)
58 | ```
59 |
60 | ### System-related imports in NAMESPACE
61 |
62 | You can check the NAMESPACE file in a package for dangerous imports:
63 |
64 | ```{r namespace-example, eval = FALSE}
65 | defender::check_namespace("../testevil")
66 | ```
67 | You can also include additional elements to flag as dangerous:
68 |
69 | ```{r namespace-example-2, eval = FALSE}
70 | di <- defender::dangerous_imports("processx::poll")
71 | defender::check_namespace("../testevil", imports_to_flag = di)
72 | ```
73 |
74 | ## Collaborators
75 |
76 | - Ildi Czeller @czeildi
77 | - Karthik Ram @karthik
78 | - Bob Rudis @hrbrmstr
79 | - Kara Woo @karawoo
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # defender
2 |
3 | [](https://www.repostatus.org/#abandoned)
4 |
5 | This repository has been archived. The former README is now in [README-NOT.md](README-NOT.md).
6 |
--------------------------------------------------------------------------------
/_pkgdown.yml:
--------------------------------------------------------------------------------
1 | url: https://docs.ropensci.org/defender/
2 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 |
--------------------------------------------------------------------------------
/codemeta.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": [
3 | "https://doi.org/10.5063/schema/codemeta-2.0",
4 | "http://schema.org"
5 | ],
6 | "@type": "SoftwareSourceCode",
7 | "identifier": "defender",
8 | "description": "Check an R package for potential security risks and violations \n via static code analysis.",
9 | "name": "defender: Check Package for Potential Security Violations",
10 | "codeRepository": "https://github.com/ropenscilabs/defender",
11 | "issueTracker": "https://github.com/ropenscilabs/defender/issues",
12 | "license": "https://spdx.org/licenses/MIT",
13 | "version": "0.0.1.9000",
14 | "programmingLanguage": {
15 | "@type": "ComputerLanguage",
16 | "name": "R",
17 | "version": "3.5.0",
18 | "url": "https://r-project.org"
19 | },
20 | "runtimePlatform": "R version 3.5.0 (2018-04-23)",
21 | "author": [
22 | {
23 | "@type": "Person",
24 | "givenName": "Ildiko",
25 | "familyName": "Czeller",
26 | "email": "czeildi@gmail.com"
27 | },
28 | {
29 | "@type": "Person",
30 | "givenName": "Karthik",
31 | "familyName": "Ram",
32 | "email": "karthik.ram@gmail.com",
33 | "@id": "https://orcid.org/ 0000-0002-0233-1757"
34 | },
35 | {
36 | "@type": "Person",
37 | "givenName": "Bob",
38 | "familyName": "Rudis",
39 | "email": "bob@rud.is",
40 | "@id": "https://orcid.org/0000-0001-5670-2640"
41 | },
42 | {
43 | "@type": "Person",
44 | "givenName": "Kara",
45 | "familyName": "Woo"
46 | }
47 | ],
48 | "maintainer": [
49 | {
50 | "@type": "Person",
51 | "givenName": "Ildiko",
52 | "familyName": "Czeller",
53 | "email": "czeildi@gmail.com"
54 | }
55 | ],
56 | "softwareSuggestions": [
57 | {
58 | "@type": "SoftwareApplication",
59 | "identifier": "covr",
60 | "name": "covr",
61 | "provider": {
62 | "@id": "https://cran.r-project.org",
63 | "@type": "Organization",
64 | "name": "Comprehensive R Archive Network (CRAN)",
65 | "url": "https://cran.r-project.org"
66 | },
67 | "sameAs": "https://CRAN.R-project.org/package=covr"
68 | },
69 | {
70 | "@type": "SoftwareApplication",
71 | "identifier": "testthat",
72 | "name": "testthat",
73 | "provider": {
74 | "@id": "https://cran.r-project.org",
75 | "@type": "Organization",
76 | "name": "Comprehensive R Archive Network (CRAN)",
77 | "url": "https://cran.r-project.org"
78 | },
79 | "sameAs": "https://CRAN.R-project.org/package=testthat"
80 | },
81 | {
82 | "@type": "SoftwareApplication",
83 | "identifier": "knitr",
84 | "name": "knitr",
85 | "provider": {
86 | "@id": "https://cran.r-project.org",
87 | "@type": "Organization",
88 | "name": "Comprehensive R Archive Network (CRAN)",
89 | "url": "https://cran.r-project.org"
90 | },
91 | "sameAs": "https://CRAN.R-project.org/package=knitr"
92 | },
93 | {
94 | "@type": "SoftwareApplication",
95 | "identifier": "roxygen2",
96 | "name": "roxygen2",
97 | "provider": {
98 | "@id": "https://cran.r-project.org",
99 | "@type": "Organization",
100 | "name": "Comprehensive R Archive Network (CRAN)",
101 | "url": "https://cran.r-project.org"
102 | },
103 | "sameAs": "https://CRAN.R-project.org/package=roxygen2"
104 | },
105 | {
106 | "@type": "SoftwareApplication",
107 | "identifier": "rstudioapi",
108 | "name": "rstudioapi",
109 | "provider": {
110 | "@id": "https://cran.r-project.org",
111 | "@type": "Organization",
112 | "name": "Comprehensive R Archive Network (CRAN)",
113 | "url": "https://cran.r-project.org"
114 | },
115 | "sameAs": "https://CRAN.R-project.org/package=rstudioapi"
116 | },
117 | {
118 | "@type": "SoftwareApplication",
119 | "identifier": "shiny",
120 | "name": "shiny",
121 | "provider": {
122 | "@id": "https://cran.r-project.org",
123 | "@type": "Organization",
124 | "name": "Comprehensive R Archive Network (CRAN)",
125 | "url": "https://cran.r-project.org"
126 | },
127 | "sameAs": "https://CRAN.R-project.org/package=shiny"
128 | },
129 | {
130 | "@type": "SoftwareApplication",
131 | "identifier": "miniUI",
132 | "name": "miniUI",
133 | "provider": {
134 | "@id": "https://cran.r-project.org",
135 | "@type": "Organization",
136 | "name": "Comprehensive R Archive Network (CRAN)",
137 | "url": "https://cran.r-project.org"
138 | },
139 | "sameAs": "https://CRAN.R-project.org/package=miniUI"
140 | }
141 | ],
142 | "softwareRequirements": [
143 | {
144 | "@type": "SoftwareApplication",
145 | "identifier": "R",
146 | "name": "R",
147 | "version": ">= 3.1.0"
148 | },
149 | {
150 | "@type": "SoftwareApplication",
151 | "identifier": "magrittr",
152 | "name": "magrittr",
153 | "provider": {
154 | "@id": "https://cran.r-project.org",
155 | "@type": "Organization",
156 | "name": "Comprehensive R Archive Network (CRAN)",
157 | "url": "https://cran.r-project.org"
158 | },
159 | "sameAs": "https://CRAN.R-project.org/package=magrittr"
160 | },
161 | {
162 | "@type": "SoftwareApplication",
163 | "identifier": "utils",
164 | "name": "utils"
165 | },
166 | {
167 | "@type": "SoftwareApplication",
168 | "identifier": "withr",
169 | "name": "withr",
170 | "provider": {
171 | "@id": "https://cran.r-project.org",
172 | "@type": "Organization",
173 | "name": "Comprehensive R Archive Network (CRAN)",
174 | "url": "https://cran.r-project.org"
175 | },
176 | "sameAs": "https://CRAN.R-project.org/package=withr"
177 | }
178 | ],
179 | "releaseNotes": "https://github.com/ropenscilabs/defender/blob/master/NEWS.md",
180 | "readme": "https://github.com/ropenscilabs/defender/blob/master/README.md",
181 | "fileSize": "53.069KB",
182 | "contIntegration": [
183 | "https://travis-ci.org/ropenscilabs/defender",
184 | "https://codecov.io/github/ropenscilabs/defender?branch=master"
185 | ],
186 | "keywords": [
187 | "unconf18",
188 | "security",
189 | "r",
190 | "r-package",
191 | "rstats",
192 | "unconf"
193 | ],
194 | "relatedLink": "https://docs.ropensci.org/defender/"
195 | }
196 |
--------------------------------------------------------------------------------
/defender.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 |
3 | RestoreWorkspace: No
4 | SaveWorkspace: No
5 | AlwaysSaveHistory: Default
6 |
7 | EnableCodeIndexing: Yes
8 | UseSpacesForTab: Yes
9 | NumSpacesForTab: 2
10 | Encoding: UTF-8
11 |
12 | RnwWeave: Sweave
13 | LaTeX: pdfLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Package
19 | PackageUseDevtools: Yes
20 | PackageInstallArgs: --no-multiarch --with-keep.source
21 | PackageRoxygenize: rd,collate,namespace
22 |
--------------------------------------------------------------------------------
/inst/rstudio/addins.dcf:
--------------------------------------------------------------------------------
1 | Name: List system calls
2 | Description: Lists all system calls in given directory in R source files in a data.frame
3 | Binding: system_calls_addin
4 | Interactive: true
5 |
6 | Name: List namespace imports
7 | Description: Lists all system related imports in package
8 | Binding: check_namespace_addin
9 | Interactive: true
10 |
--------------------------------------------------------------------------------
/man/check_namespace.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/check_namespace.R
3 | \name{check_namespace}
4 | \alias{check_namespace}
5 | \title{Read a package NAMESPACE and check for dangerous imports}
6 | \usage{
7 | check_namespace(pkg_path, imports_to_flag = dangerous_imports())
8 | }
9 | \arguments{
10 | \item{pkg_path}{path to package source tree}
11 |
12 | \item{imports_to_flag}{character vector of dangerous items to find}
13 | }
14 | \description{
15 | Given a path to a package source tree, return a data.frame of Imports (both whole
16 | packages and fully qualified references).
17 | }
18 | \examples{
19 | \dontrun{
20 | check_namespace("../testevil")
21 | check_namespace(
22 | "../testevil",
23 | dangerous_imports(additional_dangerous_imports = "sys::exec_background")
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/man/dangerous_imports.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/utils.R
3 | \name{dangerous_imports}
4 | \alias{dangerous_imports}
5 | \title{Dangerous Imports in Namespace}
6 | \usage{
7 | dangerous_imports(additional_dangerous_imports = character(0))
8 | }
9 | \arguments{
10 | \item{additional_dangerous_imports}{character vector of user-defined
11 | dangerous imports}
12 | }
13 | \value{
14 | character vector of all imports considered dangerous
15 | }
16 | \description{
17 | Character vector of all imports considered dangerous to be used with
18 | \code{\link{check_namespace}}.
19 | }
20 | \examples{
21 | dangerous_imports()
22 | }
23 |
--------------------------------------------------------------------------------
/man/defender-package.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/defender-package.R
3 | \docType{package}
4 | \name{defender-package}
5 | \alias{defender}
6 | \alias{defender-package}
7 | \title{defender: Check Package for Potential Security Violations}
8 | \description{
9 | \if{html}{\figure{logo.png}{options: align='right'}}
10 |
11 | Check an R package for potential security risks and violations
12 | via static code analysis.
13 | }
14 | \seealso{
15 | Useful links:
16 | \itemize{
17 | \item \url{https://github.com/ropenscilabs/defender}
18 | \item Report bugs at \url{https://github.com/ropenscilabs/defender/issues}
19 | }
20 |
21 | }
22 | \author{
23 | \strong{Maintainer}: Ildiko Czeller \email{czeildi@gmail.com}
24 |
25 | Authors:
26 | \itemize{
27 | \item Karthik Ram \email{karthik.ram@gmail.com} ( 0000-0002-0233-1757)
28 | \item Bob Rudis \email{bob@rud.is} (0000-0001-5670-2640)
29 | \item Kara Woo
30 | }
31 |
32 | }
33 | \keyword{internal}
34 |
--------------------------------------------------------------------------------
/man/figures/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ropensci-archive/defender/d9218d5ec84227269833b8b5333e44906d40cd2f/man/figures/logo.png
--------------------------------------------------------------------------------
/man/figures/supergb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ropensci-archive/defender/d9218d5ec84227269833b8b5333e44906d40cd2f/man/figures/supergb.png
--------------------------------------------------------------------------------
/man/summarize_system_calls.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/system_calls.R
3 | \name{summarize_system_calls}
4 | \alias{summarize_system_calls}
5 | \title{summarize occurrences of system calls in project}
6 | \usage{
7 | summarize_system_calls(path = ".", calls_to_flag = system_calls())
8 | }
9 | \arguments{
10 | \item{path}{character, relative or absolute path for local code directory}
11 |
12 | \item{calls_to_flag}{character vector of functions names to flag as
13 | potentially dangerous system calls}
14 | }
15 | \value{
16 | data.frame with a row for each system call
17 | }
18 | \description{
19 | Summarize and show occurrences of system calls in the R files in a given
20 | folder. There could be more occurrences. These calls should be further checked
21 | for maliciousness.
22 | }
23 | \examples{
24 | \dontrun{
25 | # git clone git@github.com:ropenscilabs/testevil.git
26 | summarize_system_calls("testevil")
27 | summarize_system_calls("testevil", system_calls("exec_background"))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/man/system_calls.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/utils.R
3 | \name{system_calls}
4 | \alias{system_calls}
5 | \title{Functions making System Calls}
6 | \usage{
7 | system_calls(additional_system_calls = character(0))
8 | }
9 | \arguments{
10 | \item{additional_system_calls}{character vector of user-defined
11 | system calls}
12 | }
13 | \value{
14 | character vector of all system calls
15 | }
16 | \description{
17 | Character vector of functions making system calls to check for in R source
18 | files.
19 | }
20 | \examples{
21 | system_calls("exec_background")
22 | }
23 |
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | library(testthat)
2 | library(defender)
3 |
4 | test_check("defender")
5 |
--------------------------------------------------------------------------------
/tests/testthat/test-check_namespace.R:
--------------------------------------------------------------------------------
1 | context("test-check_namespace.R")
2 |
3 | setup({
4 | td <- file.path(normalizePath("."), "testdir")
5 | unlink(td, recursive = TRUE)
6 | dir.create(td)
7 | })
8 |
9 | teardown({
10 | td <- file.path(normalizePath("."), "testdir")
11 | unlink(td, recursive = TRUE)
12 | })
13 |
14 | test_that("parse fqrs from nested list to vector", {
15 | expect_equal(
16 | transform_fully_qualified_refereces(
17 | list(list("processx", "run"), list("httr", "verbose"))
18 | ),
19 | c("processx::run", "httr::verbose")
20 | )
21 | })
22 |
23 | test_that("extract packges from fully qualified references", {
24 | expect_equal(
25 | extract_pkgs_from_fully_qualified_references(
26 | list(list("processx", "run"), list("httr", "verbose"))
27 | ),
28 | c("processx", "httr")
29 | )
30 | })
31 |
32 | test_that("extract fqrs from all imports", {
33 | expect_equal(
34 | extract_fully_qualified_references(
35 | list("sys", list("processx", "run"), "httr")
36 | ),
37 | list(list("processx", "run"))
38 | )
39 | })
40 |
41 | test_that("extract whole package imports", {
42 | expect_equal(
43 | extract_whole_pkg_imports(
44 | list(list("sys"), list("processx", "run"), list("httr"))
45 | ),
46 | c("sys", "httr")
47 | )
48 | })
49 |
50 | test_that("parse namespace file", {
51 | td <- file.path(normalizePath("."), "testdir")
52 | unlink(td, recursive = TRUE)
53 | dir.create(td)
54 | file.create(file.path(td, "NAMESPACE"))
55 | expect_true(
56 | all(c("imports", "exports") %in% names(parse_ns_file(td)))
57 | )
58 | unlink(td, recursive = TRUE)
59 | })
60 |
61 | test_that("summarize imports handles empty import list", {
62 | expect_silent(summarize_imports(list(), list()))
63 | })
64 |
65 | test_that("summarize imports returns data frame", {
66 | expect_is(
67 | summarize_imports("sys", "processx::run"),
68 | "data.frame"
69 | )
70 | expect_named(
71 | summarize_imports("sys", "processx::run"),
72 | c("type", "import", "package")
73 | )
74 | })
75 |
76 | test_that("parse all imports by type", {
77 | expect_named(
78 | parse_all_imports(
79 | list(list("sys"), list("processx", "run"), list("httr"))
80 | ),
81 | c("imported_packages", "imported_functions")
82 | )
83 | })
84 |
85 | test_that("check namespace fails if path is not package", {
86 | expect_error(check_namespace(tempdir()))
87 | expect_error(check_namespace("nonexistent/path"))
88 | })
89 |
90 | test_that("check namespace returns only flagged imports", {
91 | td <- file.path(normalizePath("."), "testdir")
92 | unlink(td, recursive = TRUE)
93 | dir.create(td)
94 | file.create(file.path(td, "DESCRIPTION"))
95 | writeLines("import(magrittr)", file.path(td, "NAMESPACE"))
96 | expect_equal(
97 | nrow(check_namespace(td, imports_to_flag = "withr")),
98 | 0
99 | )
100 | expect_equal(
101 | nrow(check_namespace(td, imports_to_flag = "magrittr")),
102 | 1
103 | )
104 | unlink(td, recursive = TRUE)
105 | })
106 |
--------------------------------------------------------------------------------
/tests/testthat/test-system_calls.R:
--------------------------------------------------------------------------------
1 | context("test-system_calls.R")
2 |
3 | setup({
4 | td <- file.path(normalizePath("."), "testdir")
5 | unlink(td, recursive = TRUE)
6 | dir.create(td)
7 | })
8 |
9 | teardown({
10 | unlink(td, recursive = TRUE)
11 | })
12 |
13 | test_that("expression is parsed to data frame", {
14 | expect_s3_class(
15 | find_system_calls(parse(text = "system2()")),
16 | "data.frame"
17 | )
18 | })
19 |
20 |
21 | test_that("find system calls does not return plain functions calls", {
22 | expect_equal(
23 | nrow(find_system_calls(parse(text = "foo()\nsystem2()\nsystem()"))),
24 | 2
25 | )
26 | })
27 |
28 |
29 | test_that("informative error message if directory is not found", {
30 | expect_error(
31 | summarize_system_calls(file.path("non", "existent", "path")),
32 | "Path non/existent/path not found, you may want to clone the repository first."
33 | )
34 | })
35 |
36 |
37 | test_that("summarize system calls returns one data frame", {
38 | td <- file.path(normalizePath("."), "testdir")
39 | unlink(td, recursive = TRUE)
40 | dir.create(td)
41 | writeLines("sys::run()", file.path(td, "test.R"))
42 | expect_is(
43 | summarize_system_calls(td),
44 | "data.frame"
45 | )
46 | expect_is(
47 | summarize_system_calls(td, "sys::run"),
48 | "data.frame"
49 | )
50 | unlink(td, recursive = TRUE)
51 | })
52 |
--------------------------------------------------------------------------------
/tests/testthat/test-utils.R:
--------------------------------------------------------------------------------
1 | context("test-utils.R")
2 |
3 | setup({
4 | td <- file.path(normalizePath("."), "testdir")
5 | unlink(td, recursive = TRUE)
6 | dir.create(td)
7 | })
8 |
9 | teardown({
10 | td <- file.path(normalizePath("."), "testdir")
11 | unlink(td, recursive = TRUE)
12 | })
13 |
14 | test_that("throw error iff path does not point to a package", {
15 | td <- file.path(normalizePath("."), "testdir")
16 | unlink(td, recursive = TRUE)
17 | dir.create(td)
18 | expect_error(assert_is_package(td))
19 | file.create(file.path(td, "DESCRIPTION"))
20 | expect_silent(assert_is_package(td))
21 | unlink(td, recursive = TRUE)
22 | })
23 |
24 | test_that("find R scripts in given directory", {
25 | td <- file.path(normalizePath("."), "testdir")
26 | unlink(td, recursive = TRUE)
27 | dir.create(td)
28 | dir.create(file.path(td, "inner"))
29 | file.create(file.path(td, "first.r"))
30 | file.create(file.path(td, "second.R"))
31 | file.create(file.path(td, "third.h"))
32 | file.create(file.path(td, "inner", "fourth.R"))
33 | expect_length(
34 | get_r_script_paths(td), 3
35 | )
36 | unlink(td, recursive = TRUE)
37 | })
38 |
39 | test_that("return full path of found R scripts", {
40 | td <- file.path(normalizePath("."), "testdir")
41 | unlink(td, recursive = TRUE)
42 | dir.create(td)
43 | file.create(file.path(td, "first.r"))
44 | expect_false("first.r" %in% get_r_script_paths(td))
45 | unlink(td, recursive = TRUE)
46 | })
47 |
48 | test_that("return main packages doing system calls", {
49 | expect_setequal(
50 | c("sys", "processx"),
51 | pkgs_doing_system_calls()
52 | )
53 | })
54 |
55 | test_that("return union of built-in and user-defined dangerous imports", {
56 | expect_equal(
57 | dangerous_imports(c("testevil::evil", "processx::run")),
58 | c("sys", "processx", "processx::run", "testevil::evil")
59 | )
60 | })
61 |
--------------------------------------------------------------------------------