├── .github ├── .gitignore ├── ISSUE_TEMPLATE.md └── workflows │ ├── pkgdown.yml │ └── R-CMD-check.yml ├── LICENSE ├── revdep ├── checks.rds ├── check.R ├── README.md └── problems.md ├── _pkgdown.yml ├── tests ├── testthat.R ├── testthat │ ├── virustotal_api_key.enc │ ├── test-pkg-style.R │ ├── test-data-structures.R │ ├── test-error-handling.R │ ├── test-auth.R │ ├── test-ip-operations.R │ ├── test-rate-limiting.R │ ├── test-url-operations.R │ ├── test-domain-operations.R │ └── test-file-operations.R ├── README.md └── _covrpage.Rmd ├── .lintr ├── CRAN-SUBMISSION ├── CRAN-RELEASE ├── .gitignore ├── Citation.cff ├── man ├── init_rate_limit.Rd ├── is_rate_limit_initialized.Rd ├── print.virustotal_error.Rd ├── print.virustotal_file_report.Rd ├── print.virustotal_response.Rd ├── print.virustotal_domain_report.Rd ├── reset_rate_limit.Rd ├── summary.virustotal_response.Rd ├── validate_input.Rd ├── rate-limiting.Rd ├── get_rate_limit_status.Rd ├── virustotal-errors.Rd ├── utilities.Rd ├── virustotal-classes.Rd ├── security-utilities.Rd ├── virustotal_version.Rd ├── virustotal_info.Rd ├── is_api_key_configured.Rd ├── virustotal_GET.Rd ├── rate_limit.Rd ├── virustotal_ip_report.Rd ├── virustotal_url_scan.Rd ├── virustotal_file_report.Rd ├── virustotal_file_scan.Rd ├── virustotal_domain_report.Rd ├── virustotal_check.Rd ├── sanitize_url.Rd ├── set_key.Rd ├── format_file_size.Rd ├── is_safe_environment.Rd ├── create_safe_temp_dir.Rd ├── sanitize_ip.Rd ├── sanitize_domain.Rd ├── sanitize_hash.Rd ├── virustotal_POST.Rd ├── validate_vt_response.Rd ├── cleanup_temp_files.Rd ├── post_ip_votes.Rd ├── get_file_upload_url.Rd ├── get_file_download_url.Rd ├── virustotal_auth_error.Rd ├── post_ip_comments.Rd ├── scan_url.Rd ├── post_url_votes.Rd ├── post_domain_votes.Rd ├── post_file_votes.Rd ├── get_ip_votes.Rd ├── sanitize_file_path.Rd ├── virustotal-package.Rd ├── get_ip_comments.Rd ├── post_url_comments.Rd ├── get_url_votes.Rd ├── post_file_comments.Rd ├── virustotal_error.Rd ├── virustotal_rate_limit_error.Rd ├── get_file_votes.Rd ├── rescan_file.Rd ├── download_file.Rd ├── get_url_comments.Rd ├── virustotal_validation_error.Rd ├── get_file_comments.Rd ├── get_domain_votes.Rd ├── get_ip_info.Rd ├── ip_report.Rd ├── rescan_domain.Rd ├── get_domain_info.Rd ├── post_domain_comments.Rd ├── scan_file.Rd ├── url_report.Rd ├── rescan_ip.Rd ├── rescan_url.Rd ├── get_domain_comments.Rd ├── domain_report.Rd ├── file_report.Rd ├── get_domain_relationship.Rd ├── get_url_relationships.Rd └── get_file_relationships.Rd ├── .Rbuildignore ├── inst └── CITATION ├── R ├── zzz.R ├── get_file_upload_url.R ├── get_file_download_url.R ├── get_ip_votes.R ├── get_ip_comments.R ├── scan_url.R ├── post_ip_votes.R ├── get_file_comments.R ├── get_file_votes.R ├── rescan_file.R ├── post_ip_comments.R ├── post_domain_votes.R ├── get_domain_info.R ├── get_domain_votes.R ├── get_ip_info.R ├── post_file_comments.R ├── get_url_votes.R ├── get_url_comments.R ├── post_domain_comments.R ├── get_domain_comments.R ├── download_file.R ├── post_url_comments.R ├── ip_report.R ├── url_report.R ├── rescan_domain.R ├── post_url_votes.R ├── post_file_votes.R ├── get_domain_relationship.R ├── rescan_url.R ├── scan_file.R ├── rescan_ip.R ├── get_file_relationships.R ├── get_url_relationships.R ├── set_key.R ├── file_report.R ├── domain_report.R ├── errors.R ├── virustotal.R ├── rate_limiting.R ├── s3_classes.R └── utils.R ├── cran-comments.md ├── DESCRIPTION ├── NAMESPACE ├── README.md ├── CLAUDE.md ├── vignettes └── using_virustotal.Rmd └── NEWS.md /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2016 2 | COPYRIGHT HOLDER: Gaurav Sood -------------------------------------------------------------------------------- /revdep/checks.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themains/virustotal/HEAD/revdep/checks.rds -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: http://themains.github.io/virustotal/ 2 | template: 3 | bootstrap: 5 4 | 5 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(virustotal) 3 | 4 | test_check("virustotal") 5 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | cache_directory: ".lintr_cache" 2 | linters: linters_with_defaults( 3 | line_length_linter(80) 4 | ) 5 | -------------------------------------------------------------------------------- /CRAN-SUBMISSION: -------------------------------------------------------------------------------- 1 | Version: 0.5.0 2 | Date: 2025-12-15 19:15:16 UTC 3 | SHA: f1c93c122d4ff97795783b7ff13c58a19ca2e743 4 | -------------------------------------------------------------------------------- /revdep/check.R: -------------------------------------------------------------------------------- 1 | library("devtools") 2 | 3 | revdep_check() 4 | revdep_check_save_summary() 5 | revdep_check_print_problems() 6 | -------------------------------------------------------------------------------- /tests/testthat/virustotal_api_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themains/virustotal/HEAD/tests/testthat/virustotal_api_key.enc -------------------------------------------------------------------------------- /CRAN-RELEASE: -------------------------------------------------------------------------------- 1 | This package was submitted to CRAN on 2021-11-03. 2 | Once it is accepted, delete this file and tag the release (commit fcc8588). 3 | -------------------------------------------------------------------------------- /tests/testthat/test-pkg-style.R: -------------------------------------------------------------------------------- 1 | # https://github.com/jimhester/lintr 2 | if (requireNamespace("lintr", quietly = TRUE)) { 3 | test_that("Package Style", { 4 | lintr::expect_lint_free(cache = TRUE) 5 | }) 6 | } 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .httr-oauth 5 | dev/ 6 | tests/testthat/virustotal_api_key 7 | .lintr_cache 8 | /doc/ 9 | /Meta/ 10 | /virustotal.Rcheck 11 | *.gz 12 | /docs/ 13 | docs 14 | /..Rcheck 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /Citation.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Sood" 5 | given-names: "Gaurav" 6 | title: "virustotal: R Client for the VirusTotal Public API v3.0" 7 | version: 0.6.0 8 | date-released: 2025-12-15 9 | url: "https://github.com/themains/virustotal" -------------------------------------------------------------------------------- /man/init_rate_limit.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rate_limiting.R 3 | \name{init_rate_limit} 4 | \alias{init_rate_limit} 5 | \title{Initialize rate limiting state} 6 | \usage{ 7 | init_rate_limit() 8 | } 9 | \description{ 10 | Initialize rate limiting state 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /man/is_rate_limit_initialized.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rate_limiting.R 3 | \name{is_rate_limit_initialized} 4 | \alias{is_rate_limit_initialized} 5 | \title{Check if rate limiting is properly initialized} 6 | \usage{ 7 | is_rate_limit_initialized() 8 | } 9 | \description{ 10 | Check if rate limiting is properly initialized 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please briefly describe your problem and what output you expect. 2 | 3 | Please include a [minimal reprex](https://github.com/jennybc/reprex#what-is-a-reprex). The goal of a reprex is to make it as easy as possible for me to recreate your problem so that I can fix it. 4 | 5 | Delete these instructions once you have read them. 6 | 7 | --- 8 | 9 | Brief description of the problem 10 | 11 | ``` r 12 | # insert reprex here 13 | ``` 14 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^revdep$ 4 | ^\.httr-oauth$ 5 | ^dev$ 6 | ^docs$ 7 | ^cran-comments\.md$ 8 | ^tests/testthat/virustotal_api_key\.enc$ 9 | ^_pkgdown\.yml$ 10 | ^\.lintr 11 | ^CRAN-RELEASE$ 12 | ^\.github$ 13 | ^doc$ 14 | ^Meta$ 15 | ^CLAUDE\.md$ 16 | ^Citation\.cff$ 17 | ^CRAN-SUBMISSION$ 18 | ^\.claude$ 19 | ^README\.Rmd$ 20 | ^\.vscode$ 21 | ^\.DS_Store$ 22 | ^\.git$ 23 | ^\.gitignore$ 24 | ^codecov\.yml$ 25 | ^pkgdown$ 26 | ^tests/testthat/\.lintr_cache$ 27 | -------------------------------------------------------------------------------- /man/print.virustotal_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/errors.R 3 | \name{print.virustotal_error} 4 | \alias{print.virustotal_error} 5 | \title{Print method for VirusTotal errors} 6 | \usage{ 7 | \method{print}{virustotal_error}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{A virustotal_error object} 11 | 12 | \item{...}{Additional arguments (unused)} 13 | } 14 | \description{ 15 | Print method for VirusTotal errors 16 | } 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/print.virustotal_file_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{print.virustotal_file_report} 4 | \alias{print.virustotal_file_report} 5 | \title{Print method for file reports} 6 | \usage{ 7 | \method{print}{virustotal_file_report}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{A virustotal_file_report object} 11 | 12 | \item{...}{Additional arguments (unused)} 13 | } 14 | \description{ 15 | Print method for file reports 16 | } 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/print.virustotal_response.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{print.virustotal_response} 4 | \alias{print.virustotal_response} 5 | \title{Print method for VirusTotal responses} 6 | \usage{ 7 | \method{print}{virustotal_response}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{A virustotal_response object} 11 | 12 | \item{...}{Additional arguments (unused)} 13 | } 14 | \description{ 15 | Print method for VirusTotal responses 16 | } 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/print.virustotal_domain_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{print.virustotal_domain_report} 4 | \alias{print.virustotal_domain_report} 5 | \title{Print method for domain reports} 6 | \usage{ 7 | \method{print}{virustotal_domain_report}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{A virustotal_domain_report object} 11 | 12 | \item{...}{Additional arguments (unused)} 13 | } 14 | \description{ 15 | Print method for domain reports 16 | } 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/reset_rate_limit.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rate_limiting.R 3 | \name{reset_rate_limit} 4 | \alias{reset_rate_limit} 5 | \title{Reset rate limiting state} 6 | \usage{ 7 | reset_rate_limit() 8 | } 9 | \description{ 10 | Clears all rate limiting history. Useful for testing. 11 | } 12 | \seealso{ 13 | Other rate limiting: 14 | \code{\link{get_rate_limit_status}()}, 15 | \code{\link{rate-limiting}}, 16 | \code{\link{rate_limit}()} 17 | } 18 | \concept{rate limiting} 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/summary.virustotal_response.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{summary.virustotal_response} 4 | \alias{summary.virustotal_response} 5 | \title{Summary method for VirusTotal responses} 6 | \usage{ 7 | \method{summary}{virustotal_response}(object, ...) 8 | } 9 | \arguments{ 10 | \item{object}{A virustotal_response object} 11 | 12 | \item{...}{Additional arguments (unused)} 13 | } 14 | \description{ 15 | Summary method for VirusTotal responses 16 | } 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/validate_input.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{validate_input} 4 | \alias{validate_input} 5 | \title{Simple input validation} 6 | \usage{ 7 | validate_input(input) 8 | } 9 | \arguments{ 10 | \item{input}{Character string to validate} 11 | } 12 | \value{ 13 | Cleaned input string 14 | } 15 | \description{ 16 | Basic input validation and sanitization for VirusTotal API calls. 17 | Replaces over-engineered security functions with simpler checks. 18 | } 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/rate-limiting.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rate_limiting.R 3 | \name{rate-limiting} 4 | \alias{rate-limiting} 5 | \title{Rate Limiting for VirusTotal API} 6 | \description{ 7 | Modern rate limiting implementation that properly manages API request limits. 8 | VirusTotal public API allows 4 requests per minute. 9 | } 10 | \seealso{ 11 | Other rate limiting: 12 | \code{\link{get_rate_limit_status}()}, 13 | \code{\link{rate_limit}()}, 14 | \code{\link{reset_rate_limit}()} 15 | } 16 | \concept{rate limiting} 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/get_rate_limit_status.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rate_limiting.R 3 | \name{get_rate_limit_status} 4 | \alias{get_rate_limit_status} 5 | \title{Get current rate limit status} 6 | \usage{ 7 | get_rate_limit_status() 8 | } 9 | \value{ 10 | List with current status information 11 | } 12 | \description{ 13 | Get current rate limit status 14 | } 15 | \seealso{ 16 | Other rate limiting: 17 | \code{\link{rate-limiting}}, 18 | \code{\link{rate_limit}()}, 19 | \code{\link{reset_rate_limit}()} 20 | } 21 | \concept{rate limiting} 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /inst/CITATION: -------------------------------------------------------------------------------- 1 | citHeader("To cite package 'virustotal' in publications use:") 2 | 3 | year <- sub(".*(2[[:digit:]]{3})-.*", "\\1", meta$Date, perl = TRUE) 4 | vers <- paste("R package version", meta$Version) 5 | 6 | bibentry(bibtype = "Manual", 7 | title = "virustotal: R Client for the VirusTotal API", 8 | author = c(person("Gaurav", "Sood")), 9 | year = year, 10 | note = vers, 11 | textVersion = 12 | paste("Gaurav Sood (", 13 | year, 14 | "). virustotal: R Client for the VirusTotal API. ", 15 | vers, ".", sep="")) -------------------------------------------------------------------------------- /man/virustotal-errors.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/errors.R 3 | \name{virustotal-errors} 4 | \alias{virustotal-errors} 5 | \title{VirusTotal API Error Classes} 6 | \description{ 7 | Custom error classes for structured error handling in the virustotal package. 8 | } 9 | \seealso{ 10 | Other error handling: 11 | \code{\link{virustotal_auth_error}()}, 12 | \code{\link{virustotal_check}()}, 13 | \code{\link{virustotal_error}()}, 14 | \code{\link{virustotal_rate_limit_error}()}, 15 | \code{\link{virustotal_validation_error}()} 16 | } 17 | \concept{error handling} 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/utilities.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{utilities} 4 | \alias{utilities} 5 | \title{Utility Functions for VirusTotal Package} 6 | \description{ 7 | Helper functions and utilities for the VirusTotal package. 8 | } 9 | \seealso{ 10 | Other utilities: 11 | \code{\link{cleanup_temp_files}()}, 12 | \code{\link{create_safe_temp_dir}()}, 13 | \code{\link{format_file_size}()}, 14 | \code{\link{is_safe_environment}()}, 15 | \code{\link{validate_vt_response}()}, 16 | \code{\link{virustotal_info}()}, 17 | \code{\link{virustotal_version}()} 18 | } 19 | \concept{utilities} 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /man/virustotal-classes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{virustotal-classes} 4 | \alias{virustotal-classes} 5 | \title{S3 Classes for VirusTotal Responses} 6 | \description{ 7 | S3 classes to provide structured responses and better user experience 8 | when working with VirusTotal API results. 9 | } 10 | \seealso{ 11 | Other response classes: 12 | \code{\link{virustotal_domain_report}()}, 13 | \code{\link{virustotal_file_report}()}, 14 | \code{\link{virustotal_file_scan}()}, 15 | \code{\link{virustotal_ip_report}()}, 16 | \code{\link{virustotal_url_scan}()} 17 | } 18 | \concept{response classes} 19 | \keyword{internal} 20 | -------------------------------------------------------------------------------- /man/security-utilities.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{security-utilities} 4 | \alias{security-utilities} 5 | \title{Security Utilities for VirusTotal Package} 6 | \description{ 7 | Security functions for input sanitization and validation to prevent 8 | common security issues when working with potentially malicious inputs. 9 | } 10 | \seealso{ 11 | Other security: 12 | \code{\link{is_api_key_configured}()}, 13 | \code{\link{sanitize_domain}()}, 14 | \code{\link{sanitize_file_path}()}, 15 | \code{\link{sanitize_hash}()}, 16 | \code{\link{sanitize_ip}()}, 17 | \code{\link{sanitize_url}()} 18 | } 19 | \concept{security} 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /man/virustotal_version.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{virustotal_version} 4 | \alias{virustotal_version} 5 | \title{Get package version information} 6 | \usage{ 7 | virustotal_version() 8 | } 9 | \value{ 10 | Character string with package version 11 | } 12 | \description{ 13 | Get package version information 14 | } 15 | \seealso{ 16 | Other utilities: 17 | \code{\link{cleanup_temp_files}()}, 18 | \code{\link{create_safe_temp_dir}()}, 19 | \code{\link{format_file_size}()}, 20 | \code{\link{is_safe_environment}()}, 21 | \code{\link{utilities}}, 22 | \code{\link{validate_vt_response}()}, 23 | \code{\link{virustotal_info}()} 24 | } 25 | \concept{utilities} 26 | -------------------------------------------------------------------------------- /man/virustotal_info.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{virustotal_info} 4 | \alias{virustotal_info} 5 | \title{Print package information and configuration status} 6 | \usage{ 7 | virustotal_info() 8 | } 9 | \value{ 10 | Invisible NULL 11 | } 12 | \description{ 13 | Print package information and configuration status 14 | } 15 | \seealso{ 16 | Other utilities: 17 | \code{\link{cleanup_temp_files}()}, 18 | \code{\link{create_safe_temp_dir}()}, 19 | \code{\link{format_file_size}()}, 20 | \code{\link{is_safe_environment}()}, 21 | \code{\link{utilities}}, 22 | \code{\link{validate_vt_response}()}, 23 | \code{\link{virustotal_version}()} 24 | } 25 | \concept{utilities} 26 | -------------------------------------------------------------------------------- /man/is_api_key_configured.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{is_api_key_configured} 4 | \alias{is_api_key_configured} 5 | \title{Check if API key is properly configured} 6 | \usage{ 7 | is_api_key_configured() 8 | } 9 | \value{ 10 | Logical indicating if API key is configured 11 | } 12 | \description{ 13 | Verifies that the API key is set and appears to be valid format. 14 | } 15 | \seealso{ 16 | Other security: 17 | \code{\link{sanitize_domain}()}, 18 | \code{\link{sanitize_file_path}()}, 19 | \code{\link{sanitize_hash}()}, 20 | \code{\link{sanitize_ip}()}, 21 | \code{\link{sanitize_url}()}, 22 | \code{\link{security-utilities}} 23 | } 24 | \concept{security} 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /man/virustotal_GET.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/virustotal.R 3 | \name{virustotal_GET} 4 | \alias{virustotal_GET} 5 | \title{Base POST AND GET functions. Not exported.} 6 | \usage{ 7 | virustotal_GET(path, query = list(), key = Sys.getenv("VirustotalToken"), ...) 8 | } 9 | \arguments{ 10 | \item{path}{path to the specific API service url} 11 | 12 | \item{query}{query list} 13 | 14 | \item{key}{A character string containing Virustotal API Key. The default is retrieved from \code{Sys.getenv("VirustotalToken")}.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link[httr]{GET}}.} 17 | } 18 | \value{ 19 | list 20 | } 21 | \description{ 22 | GET for the Current V3 API 23 | } 24 | \keyword{internal} 25 | -------------------------------------------------------------------------------- /man/rate_limit.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rate_limiting.R 3 | \name{rate_limit} 4 | \alias{rate_limit} 5 | \title{Modern rate limiting implementation} 6 | \usage{ 7 | rate_limit(force_wait = FALSE) 8 | } 9 | \arguments{ 10 | \item{force_wait}{Logical. If TRUE, will wait even if under limit} 11 | } 12 | \value{ 13 | Invisible TRUE 14 | } 15 | \description{ 16 | Uses a sliding window approach to track requests and enforce limits. 17 | This replaces the old environment variable-based approach. 18 | } 19 | \seealso{ 20 | Other rate limiting: 21 | \code{\link{get_rate_limit_status}()}, 22 | \code{\link{rate-limiting}}, 23 | \code{\link{reset_rate_limit}()} 24 | } 25 | \concept{rate limiting} 26 | \keyword{internal} 27 | -------------------------------------------------------------------------------- /man/virustotal_ip_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{virustotal_ip_report} 4 | \alias{virustotal_ip_report} 5 | \title{Create a VirusTotal IP report} 6 | \usage{ 7 | virustotal_ip_report(data) 8 | } 9 | \arguments{ 10 | \item{data}{Raw API response data} 11 | } 12 | \value{ 13 | Object of class \code{virustotal_ip_report} 14 | } 15 | \description{ 16 | Create a VirusTotal IP report 17 | } 18 | \seealso{ 19 | Other response classes: 20 | \code{\link{virustotal-classes}}, 21 | \code{\link{virustotal_domain_report}()}, 22 | \code{\link{virustotal_file_report}()}, 23 | \code{\link{virustotal_file_scan}()}, 24 | \code{\link{virustotal_url_scan}()} 25 | } 26 | \concept{response classes} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/virustotal_url_scan.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{virustotal_url_scan} 4 | \alias{virustotal_url_scan} 5 | \title{Create a VirusTotal URL scan result} 6 | \usage{ 7 | virustotal_url_scan(data) 8 | } 9 | \arguments{ 10 | \item{data}{Raw API response data} 11 | } 12 | \value{ 13 | Object of class \code{virustotal_url_scan} 14 | } 15 | \description{ 16 | Create a VirusTotal URL scan result 17 | } 18 | \seealso{ 19 | Other response classes: 20 | \code{\link{virustotal-classes}}, 21 | \code{\link{virustotal_domain_report}()}, 22 | \code{\link{virustotal_file_report}()}, 23 | \code{\link{virustotal_file_scan}()}, 24 | \code{\link{virustotal_ip_report}()} 25 | } 26 | \concept{response classes} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/virustotal_file_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{virustotal_file_report} 4 | \alias{virustotal_file_report} 5 | \title{Create a VirusTotal file report} 6 | \usage{ 7 | virustotal_file_report(data) 8 | } 9 | \arguments{ 10 | \item{data}{Raw API response data} 11 | } 12 | \value{ 13 | Object of class \code{virustotal_file_report} 14 | } 15 | \description{ 16 | Create a VirusTotal file report 17 | } 18 | \seealso{ 19 | Other response classes: 20 | \code{\link{virustotal-classes}}, 21 | \code{\link{virustotal_domain_report}()}, 22 | \code{\link{virustotal_file_scan}()}, 23 | \code{\link{virustotal_ip_report}()}, 24 | \code{\link{virustotal_url_scan}()} 25 | } 26 | \concept{response classes} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/virustotal_file_scan.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{virustotal_file_scan} 4 | \alias{virustotal_file_scan} 5 | \title{Create a VirusTotal file scan result} 6 | \usage{ 7 | virustotal_file_scan(data) 8 | } 9 | \arguments{ 10 | \item{data}{Raw API response data} 11 | } 12 | \value{ 13 | Object of class \code{virustotal_file_scan} 14 | } 15 | \description{ 16 | Create a VirusTotal file scan result 17 | } 18 | \seealso{ 19 | Other response classes: 20 | \code{\link{virustotal-classes}}, 21 | \code{\link{virustotal_domain_report}()}, 22 | \code{\link{virustotal_file_report}()}, 23 | \code{\link{virustotal_ip_report}()}, 24 | \code{\link{virustotal_url_scan}()} 25 | } 26 | \concept{response classes} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/virustotal_domain_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/s3_classes.R 3 | \name{virustotal_domain_report} 4 | \alias{virustotal_domain_report} 5 | \title{Create a VirusTotal domain report} 6 | \usage{ 7 | virustotal_domain_report(data) 8 | } 9 | \arguments{ 10 | \item{data}{Raw API response data} 11 | } 12 | \value{ 13 | Object of class \code{virustotal_domain_report} 14 | } 15 | \description{ 16 | Create a VirusTotal domain report 17 | } 18 | \seealso{ 19 | Other response classes: 20 | \code{\link{virustotal-classes}}, 21 | \code{\link{virustotal_file_report}()}, 22 | \code{\link{virustotal_file_scan}()}, 23 | \code{\link{virustotal_ip_report}()}, 24 | \code{\link{virustotal_url_scan}()} 25 | } 26 | \concept{response classes} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/virustotal_check.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/virustotal.R 3 | \name{virustotal_check} 4 | \alias{virustotal_check} 5 | \title{Request Response Verification} 6 | \usage{ 7 | virustotal_check(req) 8 | } 9 | \arguments{ 10 | \item{req}{HTTP response object from httr} 11 | } 12 | \value{ 13 | Invisible NULL on success, throws structured errors on failure 14 | } 15 | \description{ 16 | Enhanced error checking with structured error classes 17 | } 18 | \seealso{ 19 | Other error handling: 20 | \code{\link{virustotal-errors}}, 21 | \code{\link{virustotal_auth_error}()}, 22 | \code{\link{virustotal_error}()}, 23 | \code{\link{virustotal_rate_limit_error}()}, 24 | \code{\link{virustotal_validation_error}()} 25 | } 26 | \concept{error handling} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/sanitize_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{sanitize_url} 4 | \alias{sanitize_url} 5 | \title{Sanitize URL input} 6 | \usage{ 7 | sanitize_url(url) 8 | } 9 | \arguments{ 10 | \item{url}{Character string representing a URL} 11 | } 12 | \value{ 13 | Sanitized URL or throws error if invalid 14 | } 15 | \description{ 16 | Validates and sanitizes URLs to prevent malicious inputs while preserving 17 | legitimate URLs for analysis. 18 | } 19 | \seealso{ 20 | Other security: 21 | \code{\link{is_api_key_configured}()}, 22 | \code{\link{sanitize_domain}()}, 23 | \code{\link{sanitize_file_path}()}, 24 | \code{\link{sanitize_hash}()}, 25 | \code{\link{sanitize_ip}()}, 26 | \code{\link{security-utilities}} 27 | } 28 | \concept{security} 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | #' Package startup and cleanup functions 2 | #' 3 | #' @name virustotal-package 4 | #' @keywords internal 5 | NULL 6 | 7 | #' @importFrom utils packageDescription 8 | .onLoad <- function(libname, pkgname) { 9 | # Initialize rate limiting state when package loads 10 | init_rate_limit() 11 | 12 | # Optionally set a default API key if environment variable exists 13 | # (but don't override if already set) 14 | if (Sys.getenv("VirustotalToken") == "" && Sys.getenv("VIRUSTOTAL_API_KEY") != "") { 15 | Sys.setenv(VirustotalToken = Sys.getenv("VIRUSTOTAL_API_KEY")) 16 | } 17 | } 18 | 19 | .onUnload <- function(libpath) { 20 | # Clean up rate limiting state 21 | if (exists(".virustotal_state", envir = asNamespace("virustotal"))) { 22 | rm(list = ls(.virustotal_state), envir = .virustotal_state) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /man/set_key.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/set_key.R 3 | \name{set_key} 4 | \alias{set_key} 5 | \title{Set VirusTotal API Key} 6 | \usage{ 7 | set_key(api_key = NULL) 8 | } 9 | \arguments{ 10 | \item{api_key}{VirusTotal API key (character string). Required.} 11 | } 12 | \value{ 13 | Invisibly returns TRUE on success 14 | } 15 | \description{ 16 | Stores your VirusTotal API key in an environment variable for use by other 17 | package functions. Get your API key from \url{https://www.virustotal.com/}. 18 | } 19 | \examples{ 20 | \dontrun{ 21 | # Set your API key 22 | set_key('your_64_character_api_key_here') 23 | 24 | # Verify it's set 25 | Sys.getenv("VirustotalToken") 26 | } 27 | } 28 | \references{ 29 | \url{https://docs.virustotal.com/reference} 30 | } 31 | \concept{authentication} 32 | -------------------------------------------------------------------------------- /man/format_file_size.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{format_file_size} 4 | \alias{format_file_size} 5 | \title{Convert file size to human readable format} 6 | \usage{ 7 | format_file_size(size_bytes) 8 | } 9 | \arguments{ 10 | \item{size_bytes}{File size in bytes} 11 | } 12 | \value{ 13 | Character string with human-readable size 14 | } 15 | \description{ 16 | Convert file size to human readable format 17 | } 18 | \seealso{ 19 | Other utilities: 20 | \code{\link{cleanup_temp_files}()}, 21 | \code{\link{create_safe_temp_dir}()}, 22 | \code{\link{is_safe_environment}()}, 23 | \code{\link{utilities}}, 24 | \code{\link{validate_vt_response}()}, 25 | \code{\link{virustotal_info}()}, 26 | \code{\link{virustotal_version}()} 27 | } 28 | \concept{utilities} 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/is_safe_environment.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{is_safe_environment} 4 | \alias{is_safe_environment} 5 | \title{Check if running in a safe environment} 6 | \usage{ 7 | is_safe_environment() 8 | } 9 | \value{ 10 | Logical indicating if environment is considered safe 11 | } 12 | \description{ 13 | Verifies that the package is running in an appropriate environment 14 | for security analysis work. 15 | } 16 | \seealso{ 17 | Other utilities: 18 | \code{\link{cleanup_temp_files}()}, 19 | \code{\link{create_safe_temp_dir}()}, 20 | \code{\link{format_file_size}()}, 21 | \code{\link{utilities}}, 22 | \code{\link{validate_vt_response}()}, 23 | \code{\link{virustotal_info}()}, 24 | \code{\link{virustotal_version}()} 25 | } 26 | \concept{utilities} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /R/get_file_upload_url.R: -------------------------------------------------------------------------------- 1 | #' Get file upload URL for large files 2 | #' 3 | #' Get a special URL for uploading files larger than 32MB to VirusTotal for analysis. 4 | #' 5 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 6 | #' 7 | #' @return list containing upload URL and other metadata 8 | #' 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{scan_file}} for regular file uploads 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' get_file_upload_url() 20 | #' } 21 | 22 | get_file_upload_url <- function(...) { 23 | 24 | res <- virustotal_GET(path = "files/upload_url", ...) 25 | 26 | res 27 | } 28 | -------------------------------------------------------------------------------- /man/create_safe_temp_dir.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{create_safe_temp_dir} 4 | \alias{create_safe_temp_dir} 5 | \title{Create a safe temporary directory for file operations} 6 | \usage{ 7 | create_safe_temp_dir() 8 | } 9 | \value{ 10 | Path to the temporary directory 11 | } 12 | \description{ 13 | Creates a temporary directory with restricted permissions for secure 14 | file operations during malware analysis. 15 | } 16 | \seealso{ 17 | Other utilities: 18 | \code{\link{cleanup_temp_files}()}, 19 | \code{\link{format_file_size}()}, 20 | \code{\link{is_safe_environment}()}, 21 | \code{\link{utilities}}, 22 | \code{\link{validate_vt_response}()}, 23 | \code{\link{virustotal_info}()}, 24 | \code{\link{virustotal_version}()} 25 | } 26 | \concept{utilities} 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/sanitize_ip.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{sanitize_ip} 4 | \alias{sanitize_ip} 5 | \title{Sanitize IP address input} 6 | \usage{ 7 | sanitize_ip(ip) 8 | } 9 | \arguments{ 10 | \item{ip}{Character string representing an IP address} 11 | } 12 | \value{ 13 | Sanitized IP address or throws error if invalid 14 | } 15 | \description{ 16 | Validates IP addresses (IPv4 and IPv6) and checks for private ranges 17 | that shouldn't be submitted to VirusTotal. 18 | } 19 | \seealso{ 20 | Other security: 21 | \code{\link{is_api_key_configured}()}, 22 | \code{\link{sanitize_domain}()}, 23 | \code{\link{sanitize_file_path}()}, 24 | \code{\link{sanitize_hash}()}, 25 | \code{\link{sanitize_url}()}, 26 | \code{\link{security-utilities}} 27 | } 28 | \concept{security} 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/sanitize_domain.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{sanitize_domain} 4 | \alias{sanitize_domain} 5 | \title{Sanitize domain input} 6 | \usage{ 7 | sanitize_domain(domain) 8 | } 9 | \arguments{ 10 | \item{domain}{Character string representing a domain name} 11 | } 12 | \value{ 13 | Sanitized domain or throws error if invalid 14 | } 15 | \description{ 16 | Validates and sanitizes domain names to prevent injection attacks 17 | while allowing legitimate domain analysis. 18 | } 19 | \seealso{ 20 | Other security: 21 | \code{\link{is_api_key_configured}()}, 22 | \code{\link{sanitize_file_path}()}, 23 | \code{\link{sanitize_hash}()}, 24 | \code{\link{sanitize_ip}()}, 25 | \code{\link{sanitize_url}()}, 26 | \code{\link{security-utilities}} 27 | } 28 | \concept{security} 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/sanitize_hash.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{sanitize_hash} 4 | \alias{sanitize_hash} 5 | \title{Sanitize hash input} 6 | \usage{ 7 | sanitize_hash(hash) 8 | } 9 | \arguments{ 10 | \item{hash}{Character string representing a file hash} 11 | } 12 | \value{ 13 | Sanitized hash or throws error if invalid 14 | } 15 | \description{ 16 | Validates hash inputs to ensure they conform to expected formats 17 | (MD5, SHA1, SHA256) and contain only valid hexadecimal characters. 18 | } 19 | \seealso{ 20 | Other security: 21 | \code{\link{is_api_key_configured}()}, 22 | \code{\link{sanitize_domain}()}, 23 | \code{\link{sanitize_file_path}()}, 24 | \code{\link{sanitize_ip}()}, 25 | \code{\link{sanitize_url}()}, 26 | \code{\link{security-utilities}} 27 | } 28 | \concept{security} 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/virustotal_POST.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/virustotal.R 3 | \name{virustotal_POST} 4 | \alias{virustotal_POST} 5 | \title{POST for the Current V3 API} 6 | \usage{ 7 | virustotal_POST( 8 | path, 9 | body = NULL, 10 | query = list(), 11 | key = Sys.getenv("VirustotalToken"), 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{path}{path to the specific API service url} 17 | 18 | \item{body}{request body (file upload or JSON data)} 19 | 20 | \item{query}{query list} 21 | 22 | \item{key}{A character string containing Virustotal API Key. The default is retrieved from \code{Sys.getenv("VirustotalToken")}.} 23 | 24 | \item{\dots}{Additional arguments passed to \code{\link[httr]{POST}}.} 25 | } 26 | \value{ 27 | list 28 | } 29 | \description{ 30 | POST for the Current V3 API 31 | } 32 | \keyword{internal} 33 | -------------------------------------------------------------------------------- /R/get_file_download_url.R: -------------------------------------------------------------------------------- 1 | #' Get download URL for a file 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 5 | #' 6 | #' @return list containing download URL and metadata 7 | #' 8 | #' @export 9 | #' 10 | #' @references \url{https://docs.virustotal.com/reference} 11 | #' 12 | #' @seealso \code{\link{set_key}} for setting the API key 13 | #' 14 | #' @examples \dontrun{ 15 | #' 16 | #' # Before calling the function, set the API key using set_key('api_key_here') 17 | #' 18 | #' get_file_download_url(hash='99017f6eebbac24f351415dd410d522d') 19 | #' } 20 | 21 | get_file_download_url <- function(hash = NULL, ...) { 22 | 23 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 24 | 25 | res <- virustotal_GET(path = paste0("files/", hash, "/download_url"), ...) 26 | 27 | res 28 | } 29 | -------------------------------------------------------------------------------- /man/validate_vt_response.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{validate_vt_response} 4 | \alias{validate_vt_response} 5 | \title{Validate VirusTotal response structure} 6 | \usage{ 7 | validate_vt_response(response) 8 | } 9 | \arguments{ 10 | \item{response}{Response object from VirusTotal API} 11 | } 12 | \value{ 13 | Logical indicating if response structure is valid 14 | } 15 | \description{ 16 | Checks if a response from VirusTotal API has the expected structure. 17 | } 18 | \seealso{ 19 | Other utilities: 20 | \code{\link{cleanup_temp_files}()}, 21 | \code{\link{create_safe_temp_dir}()}, 22 | \code{\link{format_file_size}()}, 23 | \code{\link{is_safe_environment}()}, 24 | \code{\link{utilities}}, 25 | \code{\link{virustotal_info}()}, 26 | \code{\link{virustotal_version}()} 27 | } 28 | \concept{utilities} 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/cleanup_temp_files.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{cleanup_temp_files} 4 | \alias{cleanup_temp_files} 5 | \title{Clean up temporary files and directories} 6 | \usage{ 7 | cleanup_temp_files(paths) 8 | } 9 | \arguments{ 10 | \item{paths}{Character vector of file/directory paths to clean up} 11 | } 12 | \value{ 13 | Logical indicating success 14 | } 15 | \description{ 16 | Safely removes temporary files and directories created during 17 | VirusTotal operations. 18 | } 19 | \seealso{ 20 | Other utilities: 21 | \code{\link{create_safe_temp_dir}()}, 22 | \code{\link{format_file_size}()}, 23 | \code{\link{is_safe_environment}()}, 24 | \code{\link{utilities}}, 25 | \code{\link{validate_vt_response}()}, 26 | \code{\link{virustotal_info}()}, 27 | \code{\link{virustotal_version}()} 28 | } 29 | \concept{utilities} 30 | \keyword{internal} 31 | -------------------------------------------------------------------------------- /man/post_ip_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_ip_votes.R 3 | \name{post_ip_votes} 4 | \alias{post_ip_votes} 5 | \title{Add a vote for a IP address} 6 | \usage{ 7 | post_ip_votes(ip = NULL, vote = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{IP address. String. Required.} 11 | 12 | \item{vote}{vote. String. Required.} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | named list 18 | } 19 | \description{ 20 | Add a vote for a IP address 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_ip_votes(ip = "64.233.160.0", vote = "malicious") 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key 35 | } 36 | -------------------------------------------------------------------------------- /man/get_file_upload_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_file_upload_url.R 3 | \name{get_file_upload_url} 4 | \alias{get_file_upload_url} 5 | \title{Get file upload URL for large files} 6 | \usage{ 7 | get_file_upload_url(...) 8 | } 9 | \arguments{ 10 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 11 | } 12 | \value{ 13 | list containing upload URL and other metadata 14 | } 15 | \description{ 16 | Get a special URL for uploading files larger than 32MB to VirusTotal for analysis. 17 | } 18 | \examples{ 19 | \dontrun{ 20 | 21 | # Before calling the function, set the API key using set_key('api_key_here') 22 | 23 | get_file_upload_url() 24 | } 25 | } 26 | \references{ 27 | \url{https://docs.virustotal.com/reference} 28 | } 29 | \seealso{ 30 | \code{\link{set_key}} for setting the API key, \code{\link{scan_file}} for regular file uploads 31 | } 32 | -------------------------------------------------------------------------------- /man/get_file_download_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_file_download_url.R 3 | \name{get_file_download_url} 4 | \alias{get_file_download_url} 5 | \title{Get download URL for a file} 6 | \usage{ 7 | get_file_download_url(hash = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 13 | } 14 | \value{ 15 | list containing download URL and metadata 16 | } 17 | \description{ 18 | Get download URL for a file 19 | } 20 | \examples{ 21 | \dontrun{ 22 | 23 | # Before calling the function, set the API key using set_key('api_key_here') 24 | 25 | get_file_download_url(hash='99017f6eebbac24f351415dd410d522d') 26 | } 27 | } 28 | \references{ 29 | \url{https://docs.virustotal.com/reference} 30 | } 31 | \seealso{ 32 | \code{\link{set_key}} for setting the API key 33 | } 34 | -------------------------------------------------------------------------------- /man/virustotal_auth_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/errors.R 3 | \name{virustotal_auth_error} 4 | \alias{virustotal_auth_error} 5 | \title{Create an authentication error} 6 | \usage{ 7 | virustotal_auth_error( 8 | message = "Invalid or missing API key", 9 | call = sys.call(-1) 10 | ) 11 | } 12 | \arguments{ 13 | \item{message}{Error message} 14 | 15 | \item{call}{The calling function (automatically detected)} 16 | } 17 | \value{ 18 | An error object of class \code{virustotal_auth_error} 19 | } 20 | \description{ 21 | Create an authentication error 22 | } 23 | \seealso{ 24 | Other error handling: 25 | \code{\link{virustotal-errors}}, 26 | \code{\link{virustotal_check}()}, 27 | \code{\link{virustotal_error}()}, 28 | \code{\link{virustotal_rate_limit_error}()}, 29 | \code{\link{virustotal_validation_error}()} 30 | } 31 | \concept{error handling} 32 | \keyword{internal} 33 | -------------------------------------------------------------------------------- /tests/testthat/test-data-structures.R: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | test_that("integration tests with real API", { 4 | skip_on_cran() 5 | skip_if(Sys.getenv("VirustotalToken") == "", "API key not set") 6 | 7 | # If API key file exists, use it 8 | if (file.exists("virustotal_api_key.enc")) { 9 | skip("Encrypted API key file requires special handling") 10 | } 11 | 12 | # Test basic functionality with known good inputs 13 | # These tests only run when API key is available 14 | 15 | # Test domain report (should return list) 16 | report <- domain_report("google.com") 17 | expect_type(report, "list") 18 | 19 | # Test IP report (should return list) 20 | report <- ip_report("8.8.8.8") 21 | expect_type(report, "list") 22 | 23 | # Test file report with known hash (should return list) 24 | report <- file_report("99017f6eebbac24f351415dd410d522d") 25 | expect_type(report, "list") 26 | 27 | }) 28 | 29 | -------------------------------------------------------------------------------- /man/post_ip_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_ip_comments.R 3 | \name{post_ip_comments} 4 | \alias{post_ip_comments} 5 | \title{Add a comment to an IP address} 6 | \usage{ 7 | post_ip_comments(ip = NULL, comment = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{IP address. String. Required.} 11 | 12 | \item{comment}{Comment. String. Required.} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | named list 18 | } 19 | \description{ 20 | Add a comment to an IP address 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_ip_comments(ip = "64.233.160.0", comment = "test") 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key 35 | } 36 | -------------------------------------------------------------------------------- /man/scan_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/scan_url.R 3 | \name{scan_url} 4 | \alias{scan_url} 5 | \title{Submit URL for scanning} 6 | \usage{ 7 | scan_url(url = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url}{URL to scan; string; required} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 13 | } 14 | \value{ 15 | list containing analysis details and ID 16 | } 17 | \description{ 18 | Submit a URL for analysis. Returns analysis details including an ID that can be used to 19 | retrieve the report using \code{\link{url_report}} 20 | } 21 | \examples{ 22 | \dontrun{ 23 | 24 | # Before calling the function, set the API key using set_key('api_key_here') 25 | 26 | scan_url("http://www.google.com") 27 | } 28 | } 29 | \references{ 30 | \url{https://docs.virustotal.com/reference} 31 | } 32 | \seealso{ 33 | \code{\link{set_key}} for setting the API key 34 | } 35 | -------------------------------------------------------------------------------- /man/post_url_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_url_votes.R 3 | \name{post_url_votes} 4 | \alias{post_url_votes} 5 | \title{Add a vote to a URL} 6 | \usage{ 7 | post_url_votes(url_id = NULL, verdict = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url_id}{URL or URL ID from VirusTotal} 11 | 12 | \item{verdict}{Vote verdict: "harmless" or "malicious"} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | list containing response data 18 | } 19 | \description{ 20 | Add a vote to a URL 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_url_votes(url_id='http://www.google.com', verdict='harmless') 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key 35 | } 36 | -------------------------------------------------------------------------------- /man/post_domain_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_domain_votes.R 3 | \name{post_domain_votes} 4 | \alias{post_domain_votes} 5 | \title{Add a vote for a hostname or domain} 6 | \usage{ 7 | post_domain_votes(domain = NULL, vote = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{domain name. String. Required.} 11 | 12 | \item{vote}{vote. String. Required.} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | named list 18 | } 19 | \description{ 20 | Add a vote for a hostname or domain 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_domain_votes("http://google.com", vote = "malicious") 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key 35 | } 36 | -------------------------------------------------------------------------------- /man/post_file_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_file_votes.R 3 | \name{post_file_votes} 4 | \alias{post_file_votes} 5 | \title{Add a vote to a file} 6 | \usage{ 7 | post_file_votes(hash = NULL, verdict = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 11 | 12 | \item{verdict}{Vote verdict: "harmless" or "malicious"} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | list containing response data 18 | } 19 | \description{ 20 | Add a vote to a file 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_file_votes(hash='99017f6eebbac24f351415dd410d522d', verdict='malicious') 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key 35 | } 36 | -------------------------------------------------------------------------------- /man/get_ip_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_ip_votes.R 3 | \name{get_ip_votes} 4 | \alias{get_ip_votes} 5 | \title{Retrieve votes for an IP address} 6 | \usage{ 7 | get_ip_votes(ip = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{IP address. String. Required.} 11 | 12 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | named list 20 | } 21 | \description{ 22 | Retrieve votes for an IP address 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_ip_votes("64.233.160.0") 30 | } 31 | } 32 | \references{ 33 | \url{https://docs.virustotal.com/reference} 34 | } 35 | \seealso{ 36 | \code{\link{set_key}} for setting the API key 37 | } 38 | -------------------------------------------------------------------------------- /man/sanitize_file_path.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/security.R 3 | \name{sanitize_file_path} 4 | \alias{sanitize_file_path} 5 | \title{Sanitize file path input} 6 | \usage{ 7 | sanitize_file_path(file_path, allow_relative = FALSE) 8 | } 9 | \arguments{ 10 | \item{file_path}{Character string representing a file path} 11 | 12 | \item{allow_relative}{Logical. Whether to allow relative paths (default: FALSE)} 13 | } 14 | \value{ 15 | Sanitized file path or throws error if invalid 16 | } 17 | \description{ 18 | Validates and sanitizes file paths to prevent directory traversal attacks 19 | and ensure safe file operations. 20 | } 21 | \seealso{ 22 | Other security: 23 | \code{\link{is_api_key_configured}()}, 24 | \code{\link{sanitize_domain}()}, 25 | \code{\link{sanitize_hash}()}, 26 | \code{\link{sanitize_ip}()}, 27 | \code{\link{sanitize_url}()}, 28 | \code{\link{security-utilities}} 29 | } 30 | \concept{security} 31 | \keyword{internal} 32 | -------------------------------------------------------------------------------- /man/virustotal-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/virustotal.R, R/zzz.R 3 | \docType{package} 4 | \name{virustotal-package} 5 | \alias{virustotal} 6 | \alias{virustotal-package} 7 | \title{virustotal: Access Virustotal API} 8 | \description{ 9 | Access virustotal API. See \url{https://www.virustotal.com/}. 10 | Details about results of calls to the API can be found at \url{https://docs.virustotal.com/reference}. 11 | 12 | You will need credentials to use this application. 13 | If you haven't already, get the API Key at \url{https://www.virustotal.com/}. 14 | } 15 | \seealso{ 16 | Useful links: 17 | \itemize{ 18 | \item \url{https://github.com/themains/virustotal} 19 | \item \url{https://themains.github.io/virustotal/} 20 | \item \url{http://themains.github.io/virustotal/} 21 | \item Report bugs at \url{https://github.com/themains/virustotal/issues} 22 | } 23 | 24 | } 25 | \author{ 26 | Gaurav Sood 27 | } 28 | \keyword{internal} 29 | -------------------------------------------------------------------------------- /man/get_ip_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_ip_comments.R 3 | \name{get_ip_comments} 4 | \alias{get_ip_comments} 5 | \title{Retrieve comments for an IP address} 6 | \usage{ 7 | get_ip_comments(ip = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{IP Address. String. Required.} 11 | 12 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | named list 20 | } 21 | \description{ 22 | Retrieve comments for an IP address 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_ip_comments("64.233.160.0") 30 | } 31 | } 32 | \references{ 33 | \url{https://docs.virustotal.com/reference} 34 | } 35 | \seealso{ 36 | \code{\link{set_key}} for setting the API key 37 | } 38 | -------------------------------------------------------------------------------- /man/post_url_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_url_comments.R 3 | \name{post_url_comments} 4 | \alias{post_url_comments} 5 | \title{Add a comment to a URL} 6 | \usage{ 7 | post_url_comments(url_id = NULL, comment = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url_id}{URL or URL ID from VirusTotal} 11 | 12 | \item{comment}{Comment text to add} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | list containing response data 18 | } 19 | \description{ 20 | Add a comment to a URL 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_url_comments(url_id='http://www.google.com', 28 | comment='This URL appears suspicious') 29 | } 30 | } 31 | \references{ 32 | \url{https://docs.virustotal.com/reference} 33 | } 34 | \seealso{ 35 | \code{\link{set_key}} for setting the API key 36 | } 37 | -------------------------------------------------------------------------------- /R/get_ip_votes.R: -------------------------------------------------------------------------------- 1 | #' Retrieve votes for an IP address 2 | #' 3 | #' @param ip IP address. String. Required. 4 | #' @param limit Number of entries. Integer. Optional. Default is 10. 5 | #' @param cursor String. Optional. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return named list 9 | #' 10 | #' @export 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @seealso \code{\link{set_key}} for setting the API key 15 | #' 16 | #' @examples \dontrun{ 17 | #' 18 | #' # Before calling the function, set the API key using set_key('api_key_here') 19 | #' 20 | #' get_ip_votes("64.233.160.0") 21 | #' } 22 | 23 | get_ip_votes <- function(ip = NULL, limit = NULL, cursor = NULL, ...) { 24 | 25 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | res <- virustotal_GET(path = paste0("ip_addresses/", ip, "/votes"), 28 | query = list(limit = limit, cursor = cursor), ...) 29 | 30 | res 31 | } 32 | -------------------------------------------------------------------------------- /man/get_url_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_url_votes.R 3 | \name{get_url_votes} 4 | \alias{get_url_votes} 5 | \title{Retrieve votes for a URL} 6 | \usage{ 7 | get_url_votes(url_id = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url_id}{URL or URL ID from VirusTotal} 11 | 12 | \item{limit}{Number of votes to retrieve. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String for pagination. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | list containing URL votes 20 | } 21 | \description{ 22 | Retrieve votes for a URL 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_url_votes(url_id='http://www.google.com') 30 | } 31 | } 32 | \references{ 33 | \url{https://docs.virustotal.com/reference} 34 | } 35 | \seealso{ 36 | \code{\link{set_key}} for setting the API key 37 | } 38 | -------------------------------------------------------------------------------- /man/post_file_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_file_comments.R 3 | \name{post_file_comments} 4 | \alias{post_file_comments} 5 | \title{Add a comment to a file} 6 | \usage{ 7 | post_file_comments(hash = NULL, comment = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 11 | 12 | \item{comment}{Comment text to add} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | list containing response data 18 | } 19 | \description{ 20 | Add a comment to a file 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_file_comments(hash='99017f6eebbac24f351415dd410d522d', 28 | comment='This file appears to be suspicious') 29 | } 30 | } 31 | \references{ 32 | \url{https://docs.virustotal.com/reference} 33 | } 34 | \seealso{ 35 | \code{\link{set_key}} for setting the API key 36 | } 37 | -------------------------------------------------------------------------------- /man/virustotal_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/errors.R 3 | \name{virustotal_error} 4 | \alias{virustotal_error} 5 | \title{Create a VirusTotal API error} 6 | \usage{ 7 | virustotal_error( 8 | message, 9 | status_code = NULL, 10 | response = NULL, 11 | call = sys.call(-1) 12 | ) 13 | } 14 | \arguments{ 15 | \item{message}{Error message} 16 | 17 | \item{status_code}{HTTP status code} 18 | 19 | \item{response}{Full HTTP response object} 20 | 21 | \item{call}{The calling function (automatically detected)} 22 | } 23 | \value{ 24 | An error object of class \code{virustotal_error} 25 | } 26 | \description{ 27 | Create a VirusTotal API error 28 | } 29 | \seealso{ 30 | Other error handling: 31 | \code{\link{virustotal-errors}}, 32 | \code{\link{virustotal_auth_error}()}, 33 | \code{\link{virustotal_check}()}, 34 | \code{\link{virustotal_rate_limit_error}()}, 35 | \code{\link{virustotal_validation_error}()} 36 | } 37 | \concept{error handling} 38 | \keyword{internal} 39 | -------------------------------------------------------------------------------- /man/virustotal_rate_limit_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/errors.R 3 | \name{virustotal_rate_limit_error} 4 | \alias{virustotal_rate_limit_error} 5 | \title{Create a rate limit error} 6 | \usage{ 7 | virustotal_rate_limit_error( 8 | message = "Rate limit exceeded", 9 | retry_after = 60, 10 | call = sys.call(-1) 11 | ) 12 | } 13 | \arguments{ 14 | \item{message}{Error message} 15 | 16 | \item{retry_after}{Number of seconds to wait before retry} 17 | 18 | \item{call}{The calling function (automatically detected)} 19 | } 20 | \value{ 21 | An error object of class \code{virustotal_rate_limit_error} 22 | } 23 | \description{ 24 | Create a rate limit error 25 | } 26 | \seealso{ 27 | Other error handling: 28 | \code{\link{virustotal-errors}}, 29 | \code{\link{virustotal_auth_error}()}, 30 | \code{\link{virustotal_check}()}, 31 | \code{\link{virustotal_error}()}, 32 | \code{\link{virustotal_validation_error}()} 33 | } 34 | \concept{error handling} 35 | \keyword{internal} 36 | -------------------------------------------------------------------------------- /man/get_file_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_file_votes.R 3 | \name{get_file_votes} 4 | \alias{get_file_votes} 5 | \title{Retrieve votes for a file} 6 | \usage{ 7 | get_file_votes(hash = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 11 | 12 | \item{limit}{Number of votes to retrieve. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String for pagination. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | list containing file votes 20 | } 21 | \description{ 22 | Retrieve votes for a file 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_file_votes(hash='99017f6eebbac24f351415dd410d522d') 30 | } 31 | } 32 | \references{ 33 | \url{https://docs.virustotal.com/reference} 34 | } 35 | \seealso{ 36 | \code{\link{set_key}} for setting the API key 37 | } 38 | -------------------------------------------------------------------------------- /man/rescan_file.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rescan_file.R 3 | \name{rescan_file} 4 | \alias{rescan_file} 5 | \title{Request rescan of a file} 6 | \usage{ 7 | rescan_file(hash = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256) or file ID. String. Required.} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 13 | } 14 | \value{ 15 | list containing analysis details and ID 16 | } 17 | \description{ 18 | Request a new analysis of a file already present in VirusTotal's database. 19 | Returns an analysis ID that can be used to retrieve the report using \code{\link{file_report}}. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | 24 | # Before calling the function, set the API key using set_key('api_key_here') 25 | 26 | rescan_file(hash='99017f6eebbac24f351415dd410d522d') 27 | } 28 | } 29 | \references{ 30 | \url{https://docs.virustotal.com/reference} 31 | } 32 | \seealso{ 33 | \code{\link{set_key}} for setting the API key 34 | } 35 | -------------------------------------------------------------------------------- /R/get_ip_comments.R: -------------------------------------------------------------------------------- 1 | #' Retrieve comments for an IP address 2 | #' 3 | #' @param ip IP Address. String. Required. 4 | #' @param limit Number of entries. Integer. Optional. Default is 10. 5 | #' @param cursor String. Optional. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return named list 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' get_ip_comments("64.233.160.0") 20 | #' } 21 | 22 | get_ip_comments <- function(ip = NULL, limit = NULL, cursor = NULL, ...) { 23 | 24 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 25 | 26 | res <- virustotal_GET(path = paste0("ip_addresses/", ip, "/comments"), 27 | query = list(limit = limit, cursor = cursor), ...) 28 | 29 | res 30 | } 31 | -------------------------------------------------------------------------------- /R/scan_url.R: -------------------------------------------------------------------------------- 1 | #' Submit URL for scanning 2 | #' 3 | #' Submit a URL for analysis. Returns analysis details including an ID that can be used to 4 | #' retrieve the report using \code{\link{url_report}} 5 | #' 6 | #' @param url URL to scan; string; required 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 8 | #' 9 | #' @return list containing analysis details and ID 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' scan_url("http://www.google.com") 22 | #' } 23 | 24 | scan_url <- function(url = NULL, ...) { 25 | 26 | # Validate URL using checkmate 27 | assert_character(url, len = 1, any.missing = FALSE, min.chars = 1) 28 | 29 | res <- virustotal_POST(path = "urls", 30 | body = list(url = url), ...) 31 | 32 | res 33 | } 34 | -------------------------------------------------------------------------------- /man/download_file.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/download_file.R 3 | \name{download_file} 4 | \alias{download_file} 5 | \title{Download a file from VirusTotal} 6 | \usage{ 7 | download_file(hash = NULL, output_path = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 11 | 12 | \item{output_path}{Local path to save the downloaded file. Optional.} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 15 | } 16 | \value{ 17 | Raw file content or saves file to specified path 18 | } 19 | \description{ 20 | Download a file from VirusTotal 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | download_file(hash='99017f6eebbac24f351415dd410d522d', 28 | output_path='/tmp/downloaded_file') 29 | } 30 | } 31 | \references{ 32 | \url{https://docs.virustotal.com/reference} 33 | } 34 | \seealso{ 35 | \code{\link{set_key}} for setting the API key 36 | } 37 | -------------------------------------------------------------------------------- /man/get_url_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_url_comments.R 3 | \name{get_url_comments} 4 | \alias{get_url_comments} 5 | \title{Retrieve comments for a URL} 6 | \usage{ 7 | get_url_comments(url_id = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url_id}{URL or URL ID from VirusTotal} 11 | 12 | \item{limit}{Number of comments to retrieve. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String for pagination. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | list containing URL comments 20 | } 21 | \description{ 22 | Retrieve comments for a URL 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_url_comments(url_id='http://www.google.com') 30 | } 31 | } 32 | \references{ 33 | \url{https://docs.virustotal.com/reference} 34 | } 35 | \seealso{ 36 | \code{\link{set_key}} for setting the API key 37 | } 38 | -------------------------------------------------------------------------------- /man/virustotal_validation_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/errors.R 3 | \name{virustotal_validation_error} 4 | \alias{virustotal_validation_error} 5 | \title{Create a validation error} 6 | \usage{ 7 | virustotal_validation_error( 8 | message, 9 | parameter = NULL, 10 | value = NULL, 11 | call = sys.call(-1) 12 | ) 13 | } 14 | \arguments{ 15 | \item{message}{Error message} 16 | 17 | \item{parameter}{The parameter that failed validation} 18 | 19 | \item{value}{The invalid value} 20 | 21 | \item{call}{The calling function (automatically detected)} 22 | } 23 | \value{ 24 | An error object of class \code{virustotal_validation_error} 25 | } 26 | \description{ 27 | Create a validation error 28 | } 29 | \seealso{ 30 | Other error handling: 31 | \code{\link{virustotal-errors}}, 32 | \code{\link{virustotal_auth_error}()}, 33 | \code{\link{virustotal_check}()}, 34 | \code{\link{virustotal_error}()}, 35 | \code{\link{virustotal_rate_limit_error}()} 36 | } 37 | \concept{error handling} 38 | \keyword{internal} 39 | -------------------------------------------------------------------------------- /man/get_file_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_file_comments.R 3 | \name{get_file_comments} 4 | \alias{get_file_comments} 5 | \title{Retrieve comments for a file} 6 | \usage{ 7 | get_file_comments(hash = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 11 | 12 | \item{limit}{Number of comments to retrieve. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String for pagination. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | list containing file comments 20 | } 21 | \description{ 22 | Retrieve comments for a file 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_file_comments(hash='99017f6eebbac24f351415dd410d522d') 30 | } 31 | } 32 | \references{ 33 | \url{https://docs.virustotal.com/reference} 34 | } 35 | \seealso{ 36 | \code{\link{set_key}} for setting the API key 37 | } 38 | -------------------------------------------------------------------------------- /man/get_domain_votes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_domain_votes.R 3 | \name{get_domain_votes} 4 | \alias{get_domain_votes} 5 | \title{Retrieve votes for an Internet domain} 6 | \usage{ 7 | get_domain_votes(domain = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{domain name. String. Required.} 11 | 12 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | named list 20 | } 21 | \description{ 22 | Retrieve votes for an Internet domain 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_domain_votes("http://www.google.com") 30 | get_domain_votes("http://www.goodsfwrfw.com") # Domain not found 31 | } 32 | } 33 | \references{ 34 | \url{https://docs.virustotal.com/reference} 35 | } 36 | \seealso{ 37 | \code{\link{set_key}} for setting the API key 38 | } 39 | -------------------------------------------------------------------------------- /man/get_ip_info.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_ip_info.R 3 | \name{get_ip_info} 4 | \alias{get_ip_info} 5 | \title{Retrieve information about an IP address} 6 | \usage{ 7 | get_ip_info(ip = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{IP address. String. Required.} 11 | 12 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | named list 20 | } 21 | \description{ 22 | Retrieves report on a given domain, including passive DNS, urls detected by at least one url scanner. 23 | Gives category of the domain from bitdefender. 24 | } 25 | \examples{ 26 | \dontrun{ 27 | 28 | # Before calling the function, set the API key using set_key('api_key_here') 29 | 30 | get_ip_info("64.233.160.0") 31 | } 32 | } 33 | \references{ 34 | \url{https://docs.virustotal.com/reference} 35 | } 36 | \seealso{ 37 | \code{\link{set_key}} for setting the API key 38 | } 39 | -------------------------------------------------------------------------------- /man/ip_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ip_report.R 3 | \name{ip_report} 4 | \alias{ip_report} 5 | \title{Get IP Address Report} 6 | \usage{ 7 | ip_report(ip = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{a valid IPv4 or IPv6 address; String; Required} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 13 | } 14 | \value{ 15 | list containing IP analysis results including geolocation, 16 | ASN information, DNS resolutions, detected URLs, and threat intelligence 17 | } 18 | \description{ 19 | Retrieves comprehensive analysis report for an IP address, including 20 | geolocation, ASN information, DNS resolutions, and detected URLs. 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | ip_report(ip="8.8.8.8") 28 | ip_report(ip="2001:4860:4860::8888") # IPv6 example 29 | } 30 | } 31 | \references{ 32 | \url{https://docs.virustotal.com/reference} 33 | } 34 | \seealso{ 35 | \code{\link{set_key}} for setting the API key 36 | } 37 | -------------------------------------------------------------------------------- /man/rescan_domain.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rescan_domain.R 3 | \name{rescan_domain} 4 | \alias{rescan_domain} 5 | \title{Request rescan of a domain} 6 | \usage{ 7 | rescan_domain(domain = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{Domain name to rescan. String. Required.} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 13 | } 14 | \value{ 15 | list containing analysis details and ID 16 | } 17 | \description{ 18 | Request a new analysis of a domain already present in VirusTotal's database. 19 | Returns an analysis ID that can be used to retrieve the report using \code{\link{domain_report}}. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | 24 | # Before calling the function, set the API key using set_key('api_key_here') 25 | 26 | # Request rescan of a domain 27 | rescan_domain("google.com") 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key, \code{\link{domain_report}} for getting reports 35 | } 36 | -------------------------------------------------------------------------------- /R/post_ip_votes.R: -------------------------------------------------------------------------------- 1 | #' Add a vote for a IP address 2 | #' 3 | #' 4 | #' @param ip IP address. String. Required. 5 | #' @param vote vote. String. Required. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 7 | #' 8 | #' @return named list 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_ip_votes(ip = "64.233.160.0", vote = "malicious") 20 | #' } 21 | 22 | post_ip_votes <- function(ip = NULL, vote = NULL, ...) { 23 | 24 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 25 | assert_character(vote, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | vote_r = list("data" = list("type" = "vote", "attributes" = list("verdict" = vote))) 28 | 29 | res <- virustotal_POST(path = paste0("ip_addresses/", ip, "/votes"), 30 | body = vote_r,...) 31 | 32 | res 33 | } 34 | -------------------------------------------------------------------------------- /man/get_domain_info.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_domain_info.R 3 | \name{get_domain_info} 4 | \alias{get_domain_info} 5 | \title{Retrieve information about an Internet domain} 6 | \usage{ 7 | get_domain_info(domain = NULL, limit = NULL, cursor = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{domain name. String. Required.} 11 | 12 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | named list 20 | } 21 | \description{ 22 | Retrieve information about an Internet domain 23 | } 24 | \examples{ 25 | \dontrun{ 26 | 27 | # Before calling the function, set the API key using set_key('api_key_here') 28 | 29 | get_domain_info("http://www.google.com") 30 | get_domain_info("http://www.goodsfwrfw.com") # Domain not found 31 | } 32 | } 33 | \references{ 34 | \url{https://docs.virustotal.com/reference} 35 | } 36 | \seealso{ 37 | \code{\link{set_key}} for setting the API key 38 | } 39 | -------------------------------------------------------------------------------- /man/post_domain_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/post_domain_comments.R 3 | \name{post_domain_comments} 4 | \alias{post_domain_comments} 5 | \title{Add a comment to an Internet domain} 6 | \usage{ 7 | post_domain_comments(domain = NULL, comment = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{domain name. String. Required.} 11 | 12 | \item{comment}{vote. String. Required. Any word starting with # in your comment's text will be considered a tag, and added to the comment's tag attribute.} 13 | 14 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 15 | } 16 | \value{ 17 | named list 18 | } 19 | \description{ 20 | Add a comment to an Internet domain 21 | } 22 | \examples{ 23 | \dontrun{ 24 | 25 | # Before calling the function, set the API key using set_key('api_key_here') 26 | 27 | post_domain_comments(domain = "https://google.com", comment = "Great!") 28 | } 29 | } 30 | \references{ 31 | \url{https://docs.virustotal.com/reference} 32 | } 33 | \seealso{ 34 | \code{\link{set_key}} for setting the API key 35 | } 36 | -------------------------------------------------------------------------------- /R/get_file_comments.R: -------------------------------------------------------------------------------- 1 | #' Retrieve comments for a file 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param limit Number of comments to retrieve. Integer. Optional. Default is 10. 5 | #' @param cursor String for pagination. Optional. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return list containing file comments 9 | #' 10 | #' @export 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @seealso \code{\link{set_key}} for setting the API key 15 | #' 16 | #' @examples \dontrun{ 17 | #' 18 | #' # Before calling the function, set the API key using set_key('api_key_here') 19 | #' 20 | #' get_file_comments(hash='99017f6eebbac24f351415dd410d522d') 21 | #' } 22 | 23 | get_file_comments <- function(hash = NULL, limit = NULL, cursor = NULL, ...) { 24 | 25 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | res <- virustotal_GET(path = paste0("files/", hash, "/comments"), 28 | query = list(limit = limit, cursor = cursor), ...) 29 | 30 | res 31 | } 32 | -------------------------------------------------------------------------------- /man/scan_file.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/scan_file.R 3 | \name{scan_file} 4 | \alias{scan_file} 5 | \title{Submit a file for scanning} 6 | \usage{ 7 | scan_file(file_path, ...) 8 | } 9 | \arguments{ 10 | \item{file_path}{Required; Path to the file to be scanned} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}} 13 | } 14 | \value{ 15 | A \code{virustotal_file_scan} object containing scan submission results 16 | with analysis ID and links for tracking the scan progress 17 | } 18 | \description{ 19 | Uploads a file to VirusTotal for malware analysis using the v3 API. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | # Set API key first 24 | set_key('your_api_key_here') 25 | 26 | # Scan a file 27 | result <- scan_file(file_path = 'suspicious_file.exe') 28 | print(result) 29 | } 30 | } 31 | \references{ 32 | \url{https://docs.virustotal.com/reference/files-scan} 33 | } 34 | \seealso{ 35 | \code{\link{set_key}} for setting the API key, \code{\link{file_report}} for retrieving scan results 36 | } 37 | \concept{scanning functions} 38 | -------------------------------------------------------------------------------- /R/get_file_votes.R: -------------------------------------------------------------------------------- 1 | #' Retrieve votes for a file 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param limit Number of votes to retrieve. Integer. Optional. Default is 10. 5 | #' @param cursor String for pagination. Optional. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return list containing file votes 9 | #' 10 | #' @export 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @seealso \code{\link{set_key}} for setting the API key 15 | #' 16 | #' @examples \dontrun{ 17 | #' 18 | #' # Before calling the function, set the API key using set_key('api_key_here') 19 | #' 20 | #' get_file_votes(hash='99017f6eebbac24f351415dd410d522d') 21 | #' } 22 | 23 | get_file_votes <- function(hash = NULL, limit = NULL, cursor = NULL, ...) { 24 | 25 | # Validate hash using checkmate 26 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 27 | 28 | res <- virustotal_GET(path = paste0("files/", hash, "/votes"), 29 | query = list(limit = limit, cursor = cursor), ...) 30 | 31 | res 32 | } 33 | -------------------------------------------------------------------------------- /R/rescan_file.R: -------------------------------------------------------------------------------- 1 | #' Request rescan of a file 2 | #' 3 | #' Request a new analysis of a file already present in VirusTotal's database. 4 | #' Returns an analysis ID that can be used to retrieve the report using \code{\link{file_report}}. 5 | #' 6 | #' @param hash File hash (MD5, SHA1, or SHA256) or file ID. String. Required. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 8 | #' 9 | #' @return list containing analysis details and ID 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' rescan_file(hash='99017f6eebbac24f351415dd410d522d') 22 | #' } 23 | 24 | rescan_file <- function(hash = NULL, ...) { 25 | 26 | # Validate hash using checkmate 27 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 28 | 29 | res <- virustotal_POST(path = paste0("files/", hash, "/analyse"), ...) 30 | 31 | res 32 | } 33 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Release Summary 2 | This is a major version update (0.2.2 -> 0.3.0) that migrates the package from VirusTotal API v2 to v3. 3 | 4 | ### Major Changes 5 | - **BREAKING CHANGE**: All core functions now use VirusTotal API v3 6 | - **BREAKING CHANGE**: Return types changed from data.frame to list (following v3 API structure) 7 | - Enhanced input validation and error handling 8 | - Comprehensive test suite with 47+ tests 9 | - Updated documentation and examples 10 | 11 | ## Test environments 12 | * local macOS (Apple Silicon), R 4.5.1 13 | * GitHub Actions: ubuntu-latest, windows-latest, macOS-latest with R release and devel 14 | * R CMD check --as-cran: PASS 15 | 16 | ## R CMD check results 17 | There were no ERRORs or WARNINGs. 18 | 19 | There are 3 NOTEs: 20 | 1. **CRAN incoming feasibility**: Found possibly invalid URLs that redirect but are functional 21 | 2. **Non-standard files**: CLAUDE.md (development file) and Citation.cff (GitHub citation file) 22 | 3. **HTML manual**: HTML Tidy version message (informational only) 23 | 24 | All NOTEs are acceptable and don't affect package functionality. 25 | 26 | -------------------------------------------------------------------------------- /R/post_ip_comments.R: -------------------------------------------------------------------------------- 1 | #' Add a comment to an IP address 2 | #' 3 | #' 4 | #' @param ip IP address. String. Required. 5 | #' @param comment Comment. String. Required. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 7 | #' 8 | #' @return named list 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_ip_comments(ip = "64.233.160.0", comment = "test") 20 | #' } 21 | 22 | post_ip_comments <- function(ip = NULL, comment = NULL, ...) { 23 | 24 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 25 | assert_character(comment, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | comment_r = list("data" = list("type" = "comment", "attributes" = list("text" = comment))) 28 | 29 | res <- virustotal_POST(path = paste0("ip_addresses/", ip, "/comments"), 30 | body = comment_r,...) 31 | 32 | res 33 | } 34 | -------------------------------------------------------------------------------- /man/url_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/url_report.R 3 | \name{url_report} 4 | \alias{url_report} 5 | \title{Get URL Report} 6 | \usage{ 7 | url_report(url_id = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url_id}{URL or URL ID from VirusTotal. String. Required.} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 13 | } 14 | \value{ 15 | list containing URL analysis results including scan details, 16 | detection information, and metadata 17 | } 18 | \description{ 19 | Retrieve a scan report for a given URL or URL ID from VirusTotal. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | 24 | # Before calling the function, set the API key using set_key('api_key_here') 25 | 26 | # Get report using URL 27 | url_report("http://www.google.com") 28 | 29 | # Get report using URL ID (base64 encoded URL without padding) 30 | url_report("687474703a2f2f7777772e676f6f676c652e636f6d2f") 31 | } 32 | } 33 | \references{ 34 | \url{https://docs.virustotal.com/reference} 35 | } 36 | \seealso{ 37 | \code{\link{set_key}} for setting the API key, \code{\link{scan_url}} for submitting URLs 38 | } 39 | -------------------------------------------------------------------------------- /man/rescan_ip.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rescan_ip.R 3 | \name{rescan_ip} 4 | \alias{rescan_ip} 5 | \title{Request rescan of an IP address} 6 | \usage{ 7 | rescan_ip(ip = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{ip}{IP address to rescan (IPv4 or IPv6). String. Required.} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 13 | } 14 | \value{ 15 | list containing analysis details and ID 16 | } 17 | \description{ 18 | Request a new analysis of an IP address already present in VirusTotal's database. 19 | Returns an analysis ID that can be used to retrieve the report using \code{\link{ip_report}}. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | 24 | # Before calling the function, set the API key using set_key('api_key_here') 25 | 26 | # Request rescan of an IPv4 address 27 | rescan_ip("8.8.8.8") 28 | 29 | # Request rescan of an IPv6 address 30 | rescan_ip("2001:4860:4860::8888") 31 | } 32 | } 33 | \references{ 34 | \url{https://docs.virustotal.com/reference} 35 | } 36 | \seealso{ 37 | \code{\link{set_key}} for setting the API key, \code{\link{ip_report}} for getting reports 38 | } 39 | -------------------------------------------------------------------------------- /R/post_domain_votes.R: -------------------------------------------------------------------------------- 1 | #' Add a vote for a hostname or domain 2 | #' 3 | #' 4 | #' @param domain domain name. String. Required. 5 | #' @param vote vote. String. Required. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 7 | #' 8 | #' @return named list 9 | #' 10 | #' @export 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @seealso \code{\link{set_key}} for setting the API key 15 | #' 16 | #' @examples \dontrun{ 17 | #' 18 | #' # Before calling the function, set the API key using set_key('api_key_here') 19 | #' 20 | #' post_domain_votes("http://google.com", vote = "malicious") 21 | #' } 22 | 23 | post_domain_votes <- function(domain = NULL, vote = NULL,...) { 24 | 25 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 26 | assert_character(vote, len = 1, any.missing = FALSE, min.chars = 1) 27 | 28 | domain <- gsub("^http://|^https://", "", domain) 29 | 30 | vote_r = list("data" = list("type" = "vote", "attributes" = list("verdict" = vote))) 31 | 32 | res <- virustotal_POST(path = paste0("domains/", domain, "/votes"), 33 | body = vote_r, ...) 34 | 35 | res 36 | } 37 | -------------------------------------------------------------------------------- /man/rescan_url.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rescan_url.R 3 | \name{rescan_url} 4 | \alias{rescan_url} 5 | \title{Request rescan of a URL} 6 | \usage{ 7 | rescan_url(url_id = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{url_id}{URL or URL ID (base64 encoded URL without padding). String. Required.} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_POST}}.} 13 | } 14 | \value{ 15 | list containing analysis details and ID 16 | } 17 | \description{ 18 | Request a new analysis of a URL already present in VirusTotal's database. 19 | Returns an analysis ID that can be used to retrieve the report using \code{\link{url_report}}. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | 24 | # Before calling the function, set the API key using set_key('api_key_here') 25 | 26 | # Request rescan using URL 27 | rescan_url("http://www.google.com") 28 | 29 | # Request rescan using URL ID 30 | rescan_url("687474703a2f2f7777772e676f6f676c652e636f6d2f") 31 | } 32 | } 33 | \references{ 34 | \url{https://docs.virustotal.com/reference} 35 | } 36 | \seealso{ 37 | \code{\link{set_key}} for setting the API key, \code{\link{url_report}} for getting reports 38 | } 39 | -------------------------------------------------------------------------------- /man/get_domain_comments.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_domain_comments.R 3 | \name{get_domain_comments} 4 | \alias{get_domain_comments} 5 | \title{Retrieve comments for an Internet domain} 6 | \usage{ 7 | get_domain_comments(domain = NULL, limit = limit, cursor = cursor, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{domain name. String. Required.} 11 | 12 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 13 | 14 | \item{cursor}{String. Optional.} 15 | 16 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 17 | } 18 | \value{ 19 | list containing domain comment data including comment text, authors, dates, 20 | and any associated metadata from the VirusTotal v3.0 API 21 | } 22 | \description{ 23 | Retrieve comments for an Internet domain 24 | } 25 | \examples{ 26 | \dontrun{ 27 | 28 | # Before calling the function, set the API key using set_key('api_key_here') 29 | 30 | get_domain_comments("http://www.google.com") 31 | get_domain_comments("http://www.goodsfwrfw.com") # Domain not found 32 | } 33 | } 34 | \references{ 35 | \url{https://docs.virustotal.com/reference} 36 | } 37 | \seealso{ 38 | \code{\link{set_key}} for setting the API key 39 | } 40 | -------------------------------------------------------------------------------- /R/get_domain_info.R: -------------------------------------------------------------------------------- 1 | #' Retrieve information about an Internet domain 2 | #' 3 | #' 4 | #' @param domain domain name. String. Required. 5 | #' @param limit Number of entries. Integer. Optional. Default is 10. 6 | #' @param cursor String. Optional. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 8 | #' 9 | #' @return named list 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' get_domain_info("http://www.google.com") 22 | #' get_domain_info("http://www.goodsfwrfw.com") # Domain not found 23 | #' } 24 | 25 | get_domain_info <- function(domain = NULL, limit = NULL, cursor = NULL, ...) { 26 | 27 | # Validate domain using checkmate 28 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 29 | 30 | domain <- gsub("^http://|^https://", "", domain) 31 | 32 | res <- virustotal_GET(path = paste0("domains/", domain), 33 | query = list(limit = limit, cursor = cursor), ...) 34 | 35 | res 36 | } 37 | -------------------------------------------------------------------------------- /R/get_domain_votes.R: -------------------------------------------------------------------------------- 1 | #' Retrieve votes for an Internet domain 2 | #' 3 | #' 4 | #' @param domain domain name. String. Required. 5 | #' @param limit Number of entries. Integer. Optional. Default is 10. 6 | #' @param cursor String. Optional. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 8 | #' 9 | #' @return named list 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' get_domain_votes("http://www.google.com") 22 | #' get_domain_votes("http://www.goodsfwrfw.com") # Domain not found 23 | #' } 24 | 25 | get_domain_votes <- function(domain = NULL, limit = NULL, cursor = NULL, ...) { 26 | 27 | # Validate domain using checkmate 28 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 29 | 30 | domain <- gsub("^http://", "", domain) 31 | 32 | res <- virustotal_GET(path = paste0("domains/", domain, "/votes"), 33 | query = list(limit = limit, cursor = cursor), ...) 34 | 35 | res 36 | } 37 | -------------------------------------------------------------------------------- /R/get_ip_info.R: -------------------------------------------------------------------------------- 1 | #' Retrieve information about an IP address 2 | #' 3 | #' Retrieves report on a given domain, including passive DNS, urls detected by at least one url scanner. 4 | #' Gives category of the domain from bitdefender. 5 | #' 6 | #' @param ip IP address. String. Required. 7 | #' @param limit Number of entries. Integer. Optional. Default is 10. 8 | #' @param cursor String. Optional. 9 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 10 | #' 11 | #' @return named list 12 | #' 13 | #' @export 14 | #' 15 | #' @references \url{https://docs.virustotal.com/reference} 16 | #' 17 | #' @seealso \code{\link{set_key}} for setting the API key 18 | #' 19 | #' @examples \dontrun{ 20 | #' 21 | #' # Before calling the function, set the API key using set_key('api_key_here') 22 | #' 23 | #' get_ip_info("64.233.160.0") 24 | #' } 25 | 26 | get_ip_info <- function(ip = NULL, limit = NULL, cursor = NULL, ...) { 27 | 28 | # Validate IP address using checkmate 29 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 30 | 31 | res <- virustotal_GET(path = paste0("ip_addresses/", ip), 32 | query = list(limit = limit, cursor = cursor), ...) 33 | 34 | res 35 | } 36 | -------------------------------------------------------------------------------- /R/post_file_comments.R: -------------------------------------------------------------------------------- 1 | #' Add a comment to a file 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param comment Comment text to add 5 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 6 | #' 7 | #' @return list containing response data 8 | #' 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_file_comments(hash='99017f6eebbac24f351415dd410d522d', 20 | #' comment='This file appears to be suspicious') 21 | #' } 22 | 23 | post_file_comments <- function(hash = NULL, comment = NULL, ...) { 24 | 25 | # Validate inputs using checkmate 26 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 27 | assert_character(comment, len = 1, any.missing = FALSE, min.chars = 1) 28 | 29 | res <- virustotal_POST(path = paste0("files/", hash, "/comments"), 30 | body = list(data = list(type = "comment", 31 | attributes = list(text = comment))), ...) 32 | 33 | res 34 | } 35 | -------------------------------------------------------------------------------- /man/domain_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/domain_report.R 3 | \name{domain_report} 4 | \alias{domain_report} 5 | \title{Get Domain Report} 6 | \usage{ 7 | domain_report(domain = NULL, ...) 8 | } 9 | \arguments{ 10 | \item{domain}{Domain name (character string). Required.} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}} 13 | } 14 | \value{ 15 | A \code{virustotal_domain_report} object containing domain analysis 16 | results including WHOIS data, DNS resolutions, detected URLs, categories, 17 | and threat intelligence 18 | } 19 | \description{ 20 | Retrieves comprehensive analysis report for a given domain, including 21 | WHOIS information, DNS resolutions, detected URLs, and threat intelligence 22 | data. 23 | } 24 | \examples{ 25 | \dontrun{ 26 | # Set API key first 27 | set_key('your_api_key_here') 28 | 29 | # Get domain reports 30 | report1 <- domain_report("google.com") 31 | report2 <- domain_report("https://www.example.com/path") 32 | 33 | print(report1) 34 | summary(report1) 35 | } 36 | } 37 | \references{ 38 | \url{https://docs.virustotal.com/reference/domains} 39 | } 40 | \seealso{ 41 | \code{\link{set_key}} for setting the API key 42 | } 43 | \concept{domain operations} 44 | -------------------------------------------------------------------------------- /man/file_report.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/file_report.R 3 | \name{file_report} 4 | \alias{file_report} 5 | \title{Get File Scan Report} 6 | \usage{ 7 | file_report(hash, ...) 8 | } 9 | \arguments{ 10 | \item{hash}{File hash (MD5, SHA1, or SHA256) or analysis ID} 11 | 12 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}} 13 | } 14 | \value{ 15 | A \code{virustotal_file_report} object containing file analysis results 16 | including antivirus scans, file metadata, and threat detection information 17 | } 18 | \description{ 19 | Retrieves detailed analysis results for a file from VirusTotal using the v3 API. 20 | } 21 | \examples{ 22 | \dontrun{ 23 | # Set API key first 24 | set_key('your_api_key_here') 25 | 26 | # Get file report using hash 27 | report <- file_report(hash = '99017f6eebbac24f351415dd410d522d') 28 | print(report) 29 | summary(report) 30 | 31 | # Work with the rich nested structure returned by v3 API 32 | print(report$data$attributes$last_analysis_stats) 33 | } 34 | } 35 | \references{ 36 | \url{https://docs.virustotal.com/reference/files} 37 | } 38 | \seealso{ 39 | \code{\link{set_key}} for setting the API key, \code{\link{scan_file}} for submitting files 40 | } 41 | \concept{file operations} 42 | -------------------------------------------------------------------------------- /R/get_url_votes.R: -------------------------------------------------------------------------------- 1 | #' Retrieve votes for a URL 2 | #' 3 | #' @param url_id URL or URL ID from VirusTotal 4 | #' @param limit Number of votes to retrieve. Integer. Optional. Default is 10. 5 | #' @param cursor String for pagination. Optional. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return list containing URL votes 9 | #' 10 | #' @export 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @seealso \code{\link{set_key}} for setting the API key 15 | #' 16 | #' @examples \dontrun{ 17 | #' 18 | #' # Before calling the function, set the API key using set_key('api_key_here') 19 | #' 20 | #' get_url_votes(url_id='http://www.google.com') 21 | #' } 22 | 23 | get_url_votes <- function(url_id = NULL, limit = NULL, cursor = NULL, ...) { 24 | 25 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | # If it looks like a URL, encode it to base64 (VirusTotal v3 requirement) 28 | if (grepl("^https?://", url_id)) { 29 | url_id <- base64encode(charToRaw(url_id)) 30 | url_id <- gsub("=+$", "", url_id) # Remove padding 31 | } 32 | 33 | res <- virustotal_GET(path = paste0("urls/", url_id, "/votes"), 34 | query = list(limit = limit, cursor = cursor), ...) 35 | 36 | res 37 | } 38 | -------------------------------------------------------------------------------- /R/get_url_comments.R: -------------------------------------------------------------------------------- 1 | #' Retrieve comments for a URL 2 | #' 3 | #' @param url_id URL or URL ID from VirusTotal 4 | #' @param limit Number of comments to retrieve. Integer. Optional. Default is 10. 5 | #' @param cursor String for pagination. Optional. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return list containing URL comments 9 | #' 10 | #' @export 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @seealso \code{\link{set_key}} for setting the API key 15 | #' 16 | #' @examples \dontrun{ 17 | #' 18 | #' # Before calling the function, set the API key using set_key('api_key_here') 19 | #' 20 | #' get_url_comments(url_id='http://www.google.com') 21 | #' } 22 | 23 | get_url_comments <- function(url_id = NULL, limit = NULL, cursor = NULL, ...) { 24 | 25 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | # If it looks like a URL, encode it to base64 (VirusTotal v3 requirement) 28 | if (grepl("^https?://", url_id)) { 29 | url_id <- base64encode(charToRaw(url_id)) 30 | url_id <- gsub("=+$", "", url_id) # Remove padding 31 | } 32 | 33 | res <- virustotal_GET(path = paste0("urls/", url_id, "/comments"), 34 | query = list(limit = limit, cursor = cursor), ...) 35 | 36 | res 37 | } 38 | -------------------------------------------------------------------------------- /R/post_domain_comments.R: -------------------------------------------------------------------------------- 1 | #' Add a comment to an Internet domain 2 | #' 3 | #' 4 | #' @param domain domain name. String. Required. 5 | #' @param comment vote. String. Required. Any word starting with # in your comment's text will be considered a tag, and added to the comment's tag attribute. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 7 | #' 8 | #' @return named list 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_domain_comments(domain = "https://google.com", comment = "Great!") 20 | #' } 21 | 22 | post_domain_comments <- function(domain = NULL, comment = NULL,...) { 23 | 24 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 25 | assert_character(comment, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | domain <- gsub("^http://|^https://", "", domain) 28 | 29 | comment_r = list("data" = list("type" = "comment", "attributes" = list("text" = comment))) 30 | 31 | res <- virustotal_POST(path = paste0("domains/", domain, "/comments"), 32 | body = comment_r,...) 33 | 34 | res 35 | } 36 | -------------------------------------------------------------------------------- /R/get_domain_comments.R: -------------------------------------------------------------------------------- 1 | #' Retrieve comments for an Internet domain 2 | #' 3 | #' 4 | #' @param domain domain name. String. Required. 5 | #' @param limit Number of entries. Integer. Optional. Default is 10. 6 | #' @param cursor String. Optional. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 8 | #' 9 | #' @return list containing domain comment data including comment text, authors, dates, 10 | #' and any associated metadata from the VirusTotal v3.0 API 11 | #' 12 | #' @export 13 | #' 14 | #' @references \url{https://docs.virustotal.com/reference} 15 | #' 16 | #' @seealso \code{\link{set_key}} for setting the API key 17 | #' 18 | #' @examples \dontrun{ 19 | #' 20 | #' # Before calling the function, set the API key using set_key('api_key_here') 21 | #' 22 | #' get_domain_comments("http://www.google.com") 23 | #' get_domain_comments("http://www.goodsfwrfw.com") # Domain not found 24 | #' } 25 | 26 | get_domain_comments <- function(domain = NULL, limit = limit, cursor = cursor, ...) { 27 | 28 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 29 | 30 | domain <- gsub("^http://|^https://", "", domain) 31 | 32 | res <- virustotal_GET(path = paste0("domains/", domain, "/comments"), 33 | query = list(limit = limit, cursor = cursor), ...) 34 | 35 | res 36 | } 37 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Platform 4 | 5 | |setting |value | 6 | |:--------|:----------------------------| 7 | |version |R version 3.3.2 (2016-10-31) | 8 | |system |x86_64, mingw32 | 9 | |ui |Rgui | 10 | |language |(EN) | 11 | |collate |English_United States.1252 | 12 | |tz |America/New_York | 13 | |date |2016-11-10 | 14 | 15 | ## Packages 16 | 17 | |package |* |version |date |source | 18 | |:----------|:--|:-------|:----------|:-----------------------------| 19 | |httr | |1.2.1 |2016-07-03 |CRAN (R 3.3.2) | 20 | |knitr | |1.15 |2016-11-09 |CRAN (R 3.3.2) | 21 | |rmarkdown | |1.1 |2016-10-16 |CRAN (R 3.3.2) | 22 | |testthat | |1.0.2 |2016-04-23 |CRAN (R 3.3.2) | 23 | |virustotal |* |0.2.0 |2016-11-10 |local (soodoku/virustotal@NA) | 24 | 25 | # Check results 26 | 1 packages 27 | 28 | ## rdomains (0.1.5) 29 | Maintainer: Gaurav Sood 30 | 31 | 1 error | 0 warnings | 0 notes 32 | 33 | ``` 34 | checking whether package 'rdomains' can be installed ... ERROR 35 | Installation failed. 36 | See 'D:/github/virustotal/revdep/checks/rdomains.Rcheck/00install.out' for details. 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Platform 4 | 5 | |setting |value | 6 | |:--------|:----------------------------| 7 | |version |R version 3.3.2 (2016-10-31) | 8 | |system |x86_64, mingw32 | 9 | |ui |Rgui | 10 | |language |(EN) | 11 | |collate |English_United States.1252 | 12 | |tz |America/New_York | 13 | |date |2016-11-10 | 14 | 15 | ## Packages 16 | 17 | |package |* |version |date |source | 18 | |:----------|:--|:-------|:----------|:-----------------------------| 19 | |httr | |1.2.1 |2016-07-03 |CRAN (R 3.3.2) | 20 | |knitr | |1.15 |2016-11-09 |CRAN (R 3.3.2) | 21 | |rmarkdown | |1.1 |2016-10-16 |CRAN (R 3.3.2) | 22 | |testthat | |1.0.2 |2016-04-23 |CRAN (R 3.3.2) | 23 | |virustotal |* |0.2.0 |2016-11-10 |local (soodoku/virustotal@NA) | 24 | 25 | # Check results 26 | 1 packages with problems 27 | 28 | ## rdomains (0.1.5) 29 | Maintainer: Gaurav Sood 30 | 31 | 1 error | 0 warnings | 0 notes 32 | 33 | ``` 34 | checking whether package 'rdomains' can be installed ... ERROR 35 | Installation failed. 36 | See 'D:/github/virustotal/revdep/checks/rdomains.Rcheck/00install.out' for details. 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /man/get_domain_relationship.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_domain_relationship.R 3 | \name{get_domain_relationship} 4 | \alias{get_domain_relationship} 5 | \title{Retrieve related objects to an Internet domain} 6 | \usage{ 7 | get_domain_relationship( 8 | domain = NULL, 9 | relationship = "subdomains", 10 | limit = NULL, 11 | cursor = NULL, 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{domain}{domain name. String. Required.} 17 | 18 | \item{relationship}{relationship name. String. Required. Default is \code{subdomains}. 19 | For all the options see \url{https://docs.virustotal.com/reference}} 20 | 21 | \item{limit}{Number of entries. Integer. Optional. Default is 10.} 22 | 23 | \item{cursor}{String. Optional.} 24 | 25 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 26 | } 27 | \value{ 28 | named list 29 | } 30 | \description{ 31 | Retrieve related objects to an Internet domain 32 | } 33 | \examples{ 34 | \dontrun{ 35 | 36 | # Before calling the function, set the API key using set_key('api_key_here') 37 | 38 | get_domain_relationship("https://www.google.com") 39 | get_domain_relationship("https://www.goodsfwrfw.com") # Domain not found 40 | } 41 | } 42 | \references{ 43 | \url{https://docs.virustotal.com/reference} 44 | } 45 | \seealso{ 46 | \code{\link{set_key}} for setting the API key 47 | } 48 | -------------------------------------------------------------------------------- /R/download_file.R: -------------------------------------------------------------------------------- 1 | #' Download a file from VirusTotal 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param output_path Local path to save the downloaded file. Optional. 5 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 6 | #' 7 | #' @return Raw file content or saves file to specified path 8 | #' 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' download_file(hash='99017f6eebbac24f351415dd410d522d', 20 | #' output_path='/tmp/downloaded_file') 21 | #' } 22 | 23 | download_file <- function(hash = NULL, output_path = NULL, ...) { 24 | 25 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 26 | 27 | # Note: This endpoint returns raw file content, not JSON 28 | res <- GET("https://www.virustotal.com/", 29 | path = paste0("api/v3/files/", hash, "/download"), 30 | add_headers("x-apikey" = Sys.getenv("VirustotalToken")), ...) 31 | 32 | virustotal_check(res) 33 | 34 | if (!is.null(output_path)) { 35 | writeBin(content(res, "raw"), output_path) 36 | return(paste("File downloaded to:", output_path)) 37 | } else { 38 | return(content(res, "raw")) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /man/get_url_relationships.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_url_relationships.R 3 | \name{get_url_relationships} 4 | \alias{get_url_relationships} 5 | \title{Retrieve relationships for a URL} 6 | \usage{ 7 | get_url_relationships( 8 | url_id = NULL, 9 | relationship = NULL, 10 | limit = NULL, 11 | cursor = NULL, 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{url_id}{URL or URL ID from VirusTotal} 17 | 18 | \item{relationship}{Type of relationship: "communicating_files", "downloaded_files", "graphs", "last_serving_ip_address", "network_location", "redirecting_urls", "redirects_to", "referrer_urls", "submissions"} 19 | 20 | \item{limit}{Number of relationships to retrieve. Integer. Optional. Default is 10.} 21 | 22 | \item{cursor}{String for pagination. Optional.} 23 | 24 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 25 | } 26 | \value{ 27 | list containing URL relationships 28 | } 29 | \description{ 30 | Retrieve relationships for a URL 31 | } 32 | \examples{ 33 | \dontrun{ 34 | 35 | # Before calling the function, set the API key using set_key('api_key_here') 36 | 37 | get_url_relationships(url_id='http://www.google.com', 38 | relationship='communicating_files') 39 | } 40 | } 41 | \references{ 42 | \url{https://docs.virustotal.com/reference} 43 | } 44 | \seealso{ 45 | \code{\link{set_key}} for setting the API key 46 | } 47 | -------------------------------------------------------------------------------- /R/post_url_comments.R: -------------------------------------------------------------------------------- 1 | #' Add a comment to a URL 2 | #' 3 | #' @param url_id URL or URL ID from VirusTotal 4 | #' @param comment Comment text to add 5 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 6 | #' 7 | #' @return list containing response data 8 | #' 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_url_comments(url_id='http://www.google.com', 20 | #' comment='This URL appears suspicious') 21 | #' } 22 | 23 | post_url_comments <- function(url_id = NULL, comment = NULL, ...) { 24 | 25 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 26 | assert_character(comment, len = 1, any.missing = FALSE, min.chars = 1) 27 | 28 | # If it looks like a URL, encode it to base64 (VirusTotal v3 requirement) 29 | if (grepl("^https?://", url_id)) { 30 | url_id <- base64encode(charToRaw(url_id)) 31 | url_id <- gsub("=+$", "", url_id) # Remove padding 32 | } 33 | 34 | res <- virustotal_POST(path = paste0("urls/", url_id, "/comments"), 35 | body = list(data = list(type = "comment", 36 | attributes = list(text = comment))), ...) 37 | 38 | res 39 | } 40 | -------------------------------------------------------------------------------- /R/ip_report.R: -------------------------------------------------------------------------------- 1 | #' Get IP Address Report 2 | #' 3 | #' Retrieves comprehensive analysis report for an IP address, including 4 | #' geolocation, ASN information, DNS resolutions, and detected URLs. 5 | #' 6 | #' @param ip a valid IPv4 or IPv6 address; String; Required 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 8 | #' 9 | #' @return list containing IP analysis results including geolocation, 10 | #' ASN information, DNS resolutions, detected URLs, and threat intelligence 11 | #' 12 | #' @export 13 | #' 14 | #' @references \url{https://docs.virustotal.com/reference} 15 | #' 16 | #' @seealso \code{\link{set_key}} for setting the API key 17 | #' 18 | #' @examples \dontrun{ 19 | #' 20 | #' # Before calling the function, set the API key using set_key('api_key_here') 21 | #' 22 | #' ip_report(ip="8.8.8.8") 23 | #' ip_report(ip="2001:4860:4860::8888") # IPv6 example 24 | #' } 25 | 26 | ip_report <- function(ip = NULL, ...) { 27 | # Input validation first (before API key for proper test precedence) 28 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 29 | 30 | # Check API key after basic validation 31 | if (identical(Sys.getenv("VirustotalToken"), "")) { 32 | stop(virustotal_auth_error( 33 | message = "Authentication failed. Please check your API key." 34 | )) 35 | } 36 | 37 | res <- virustotal_GET(path = paste0("ip_addresses/", ip), ...) 38 | 39 | res 40 | } 41 | -------------------------------------------------------------------------------- /R/url_report.R: -------------------------------------------------------------------------------- 1 | #' Get URL Report 2 | #' 3 | #' Retrieve a scan report for a given URL or URL ID from VirusTotal. 4 | #' 5 | #' @param url_id URL or URL ID from VirusTotal. String. Required. 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 7 | #' 8 | #' @return list containing URL analysis results including scan details, 9 | #' detection information, and metadata 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{scan_url}} for submitting URLs 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' # Get report using URL 22 | #' url_report("http://www.google.com") 23 | #' 24 | #' # Get report using URL ID (base64 encoded URL without padding) 25 | #' url_report("687474703a2f2f7777772e676f6f676c652e636f6d2f") 26 | #' } 27 | 28 | url_report <- function(url_id = NULL, ...) { 29 | 30 | # Validate URL ID using checkmate 31 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 32 | 33 | # If it looks like a URL, encode it to base64 (VirusTotal v3 requirement) 34 | if (grepl("^https?://", url_id)) { 35 | url_id <- base64encode(charToRaw(url_id)) 36 | url_id <- gsub("=+$", "", url_id) # Remove padding 37 | } 38 | 39 | res <- virustotal_GET(path = paste0("urls/", url_id), ...) 40 | 41 | res 42 | } 43 | -------------------------------------------------------------------------------- /R/rescan_domain.R: -------------------------------------------------------------------------------- 1 | #' Request rescan of a domain 2 | #' 3 | #' Request a new analysis of a domain already present in VirusTotal's database. 4 | #' Returns an analysis ID that can be used to retrieve the report using \code{\link{domain_report}}. 5 | #' 6 | #' @param domain Domain name to rescan. String. Required. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 8 | #' 9 | #' @return list containing analysis details and ID 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{domain_report}} for getting reports 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' # Request rescan of a domain 22 | #' rescan_domain("google.com") 23 | #' } 24 | 25 | rescan_domain <- function(domain = NULL, ...) { 26 | 27 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 28 | 29 | # Validate and clean domain input 30 | domain <- validate_input(domain) 31 | 32 | # Clean domain (remove protocol, www, paths) 33 | domain <- gsub("^https?://", "", domain) 34 | domain <- gsub("^www\\.", "", domain) 35 | domain <- gsub("/.*$", "", domain) 36 | 37 | res <- virustotal_POST(path = paste0("domains/", domain, "/rescan"), ...) 38 | 39 | # Return structured response 40 | structure(res, class = c("virustotal_response", "list")) 41 | } 42 | -------------------------------------------------------------------------------- /R/post_url_votes.R: -------------------------------------------------------------------------------- 1 | #' Add a vote to a URL 2 | #' 3 | #' @param url_id URL or URL ID from VirusTotal 4 | #' @param verdict Vote verdict: "harmless" or "malicious" 5 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 6 | #' 7 | #' @return list containing response data 8 | #' 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_url_votes(url_id='http://www.google.com', verdict='harmless') 20 | #' } 21 | 22 | post_url_votes <- function(url_id = NULL, verdict = NULL, ...) { 23 | 24 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 25 | 26 | if (is.null(verdict) || !verdict %in% c("harmless", "malicious")) { 27 | stop("Verdict must be either 'harmless' or 'malicious'.\n") 28 | } 29 | 30 | # If it looks like a URL, encode it to base64 (VirusTotal v3 requirement) 31 | if (grepl("^https?://", url_id)) { 32 | url_id <- base64encode(charToRaw(url_id)) 33 | url_id <- gsub("=+$", "", url_id) # Remove padding 34 | } 35 | 36 | res <- virustotal_POST(path = paste0("urls/", url_id, "/votes"), 37 | body = list(data = list(type = "vote", 38 | attributes = list(verdict = verdict))), ...) 39 | 40 | res 41 | } 42 | -------------------------------------------------------------------------------- /R/post_file_votes.R: -------------------------------------------------------------------------------- 1 | #' Add a vote to a file 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param verdict Vote verdict: "harmless" or "malicious" 5 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 6 | #' 7 | #' @return list containing response data 8 | #' 9 | #' @export 10 | #' 11 | #' @references \url{https://docs.virustotal.com/reference} 12 | #' 13 | #' @seealso \code{\link{set_key}} for setting the API key 14 | #' 15 | #' @examples \dontrun{ 16 | #' 17 | #' # Before calling the function, set the API key using set_key('api_key_here') 18 | #' 19 | #' post_file_votes(hash='99017f6eebbac24f351415dd410d522d', verdict='malicious') 20 | #' } 21 | 22 | post_file_votes <- function(hash = NULL, verdict = NULL, ...) { 23 | 24 | # Validate inputs using checkmate 25 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 26 | assert_character(verdict, len = 1, any.missing = FALSE) 27 | 28 | # Validate verdict value 29 | if (!verdict %in% c("harmless", "malicious")) { 30 | stop(virustotal_validation_error( 31 | message = "Verdict must be either 'harmless' or 'malicious'", 32 | parameter = "verdict", 33 | value = verdict 34 | )) 35 | } 36 | 37 | res <- virustotal_POST(path = paste0("files/", hash, "/votes"), 38 | body = list(data = list(type = "vote", 39 | attributes = list(verdict = verdict))), ...) 40 | 41 | res 42 | } 43 | -------------------------------------------------------------------------------- /R/get_domain_relationship.R: -------------------------------------------------------------------------------- 1 | #' Retrieve related objects to an Internet domain 2 | #' 3 | #' @param domain domain name. String. Required. 4 | #' @param limit Number of entries. Integer. Optional. Default is 10. 5 | #' @param cursor String. Optional. 6 | #' @param relationship relationship name. String. Required. Default is \code{subdomains}. 7 | #' For all the options see \url{https://docs.virustotal.com/reference} 8 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 9 | #' 10 | #' @return named list 11 | #' 12 | #' @export 13 | #' 14 | #' @references \url{https://docs.virustotal.com/reference} 15 | #' 16 | #' @seealso \code{\link{set_key}} for setting the API key 17 | #' 18 | #' @examples \dontrun{ 19 | #' 20 | #' # Before calling the function, set the API key using set_key('api_key_here') 21 | #' 22 | #' get_domain_relationship("https://www.google.com") 23 | #' get_domain_relationship("https://www.goodsfwrfw.com") # Domain not found 24 | #' } 25 | 26 | get_domain_relationship <- function(domain = NULL, relationship = "subdomains", limit = NULL, cursor = NULL, ...) { 27 | 28 | # Validate domain using checkmate 29 | assert_character(domain, len = 1, any.missing = FALSE, min.chars = 1) 30 | 31 | domain <- gsub("^http://|^https://", "", domain) 32 | 33 | res <- virustotal_GET(path = paste0("domains/", domain, "/relationships/", relationship), 34 | query = list(limit = limit, cursor = cursor), ...) 35 | 36 | res 37 | } 38 | -------------------------------------------------------------------------------- /R/rescan_url.R: -------------------------------------------------------------------------------- 1 | #' Request rescan of a URL 2 | #' 3 | #' Request a new analysis of a URL already present in VirusTotal's database. 4 | #' Returns an analysis ID that can be used to retrieve the report using \code{\link{url_report}}. 5 | #' 6 | #' @param url_id URL or URL ID (base64 encoded URL without padding). String. Required. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 8 | #' 9 | #' @return list containing analysis details and ID 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{url_report}} for getting reports 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' # Request rescan using URL 22 | #' rescan_url("http://www.google.com") 23 | #' 24 | #' # Request rescan using URL ID 25 | #' rescan_url("687474703a2f2f7777772e676f6f676c652e636f6d2f") 26 | #' } 27 | 28 | rescan_url <- function(url_id = NULL, ...) { 29 | 30 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 31 | 32 | # Validate input 33 | url_id <- validate_input(url_id) 34 | 35 | # URL encode the URL ID for safe transmission 36 | encoded_url_id <- URLencode(url_id, reserved = TRUE) 37 | 38 | res <- virustotal_POST(path = paste0("urls/", encoded_url_id, "/analyse"), ...) 39 | 40 | # Return structured response 41 | structure(res, class = c("virustotal_response", "list")) 42 | } 43 | -------------------------------------------------------------------------------- /man/get_file_relationships.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_file_relationships.R 3 | \name{get_file_relationships} 4 | \alias{get_file_relationships} 5 | \title{Retrieve relationships for a file} 6 | \usage{ 7 | get_file_relationships( 8 | hash = NULL, 9 | relationship = NULL, 10 | limit = NULL, 11 | cursor = NULL, 12 | ... 13 | ) 14 | } 15 | \arguments{ 16 | \item{hash}{File hash (MD5, SHA1, or SHA256)} 17 | 18 | \item{relationship}{Type of relationship: "behaviours", "bundled_files", "compression_parents", "contacted_domains", "contacted_ips", "contacted_urls", "dropped_files", "execution_parents", "itw_domains", "itw_ips", "itw_urls", "overlay_parents", "pcap_parents", "pe_resource_parents", "similar_files", "submissions"} 19 | 20 | \item{limit}{Number of relationships to retrieve. Integer. Optional. Default is 10.} 21 | 22 | \item{cursor}{String for pagination. Optional.} 23 | 24 | \item{\dots}{Additional arguments passed to \code{\link{virustotal_GET}}.} 25 | } 26 | \value{ 27 | list containing file relationships 28 | } 29 | \description{ 30 | Retrieve relationships for a file 31 | } 32 | \examples{ 33 | \dontrun{ 34 | 35 | # Before calling the function, set the API key using set_key('api_key_here') 36 | 37 | get_file_relationships(hash='99017f6eebbac24f351415dd410d522d', 38 | relationship='contacted_domains') 39 | } 40 | } 41 | \references{ 42 | \url{https://docs.virustotal.com/reference} 43 | } 44 | \seealso{ 45 | \code{\link{set_key}} for setting the API key 46 | } 47 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: virustotal 2 | Title: R Client for the VirusTotal API 3 | Version: 0.6.0 4 | Authors@R: person("Gaurav", "Sood", email = "gsood07@gmail.com", 5 | role = c("aut", "cre")) 6 | Author: Gaurav Sood [aut, cre] 7 | Maintainer: Gaurav Sood 8 | Description: Provides a comprehensive R interface to the VirusTotal API v3.0, 9 | a Google service that analyzes files and URLs for viruses, worms, trojans and 10 | other malware. Features include file/URL scanning, domain categorization, 11 | passive DNS information, IP reputation analysis, IoC relationships, 12 | sandbox analysis, and comment/voting systems. Implements rate limiting, 13 | error handling, and response validation for robust security analysis workflows. 14 | URL: https://github.com/themains/virustotal, https://themains.github.io/virustotal/, http://themains.github.io/virustotal/ 15 | BugReports: https://github.com/themains/virustotal/issues 16 | Depends: 17 | R (>= 4.0.0) 18 | License: MIT + file LICENSE 19 | LazyData: false 20 | VignetteBuilder: knitr 21 | Encoding: UTF-8 22 | Language: en-US 23 | Imports: 24 | httr (>= 1.4.0), 25 | dplyr (>= 1.0.0), 26 | base64enc (>= 0.1-3), 27 | jsonlite (>= 1.7.0), 28 | checkmate (>= 2.0.0), 29 | rlang (>= 1.0.0) 30 | Suggests: 31 | knitr (>= 1.30), 32 | rmarkdown (>= 2.0), 33 | testthat (>= 3.0.0), 34 | lintr (>= 3.0.0), 35 | httptest (>= 4.0.0), 36 | covr, 37 | pkgdown, 38 | spelling 39 | RoxygenNote: 7.3.3 40 | Roxygen: list(markdown = TRUE) 41 | Config/testthat/edition: 3 42 | Config/Needs/website: pkgdown 43 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yml: -------------------------------------------------------------------------------- 1 | name: pkgdown 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | pkgdown: 14 | runs-on: ubuntu-latest 15 | 16 | # Only restrict concurrency for non-PR jobs 17 | concurrency: 18 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 19 | 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} 23 | 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | 28 | permissions: 29 | contents: read 30 | pages: write 31 | id-token: write 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - uses: r-lib/actions/setup-pandoc@v2 37 | 38 | - uses: r-lib/actions/setup-r@v2 39 | with: 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::pkgdown, local::. 45 | needs: website 46 | 47 | - name: Build site 48 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 49 | shell: Rscript {0} 50 | 51 | - name: Setup Pages 52 | if: github.event_name != 'pull_request' 53 | uses: actions/configure-pages@v4 54 | 55 | - name: Upload artifact 56 | if: github.event_name != 'pull_request' 57 | uses: actions/upload-pages-artifact@v3 58 | with: 59 | path: docs 60 | 61 | - name: Deploy to GitHub Pages 62 | if: github.event_name != 'pull_request' 63 | id: deployment 64 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /R/scan_file.R: -------------------------------------------------------------------------------- 1 | #' Submit a file for scanning 2 | #' 3 | #' Uploads a file to VirusTotal for malware analysis using the v3 API. 4 | #' 5 | #' @param file_path Required; Path to the file to be scanned 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}} 7 | #' 8 | #' @return A \code{virustotal_file_scan} object containing scan submission results 9 | #' with analysis ID and links for tracking the scan progress 10 | #' 11 | #' @export 12 | #' @family scanning functions 13 | #' 14 | #' @references \url{https://docs.virustotal.com/reference/files-scan} 15 | #' 16 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{file_report}} for retrieving scan results 17 | #' 18 | #' @examples \dontrun{ 19 | #' # Set API key first 20 | #' set_key('your_api_key_here') 21 | #' 22 | #' # Scan a file 23 | #' result <- scan_file(file_path = 'suspicious_file.exe') 24 | #' print(result) 25 | #' } 26 | 27 | scan_file <- function(file_path, ...) { 28 | # Input validation using checkmate 29 | assert_character(file_path, len = 1, any.missing = FALSE) 30 | assert_file_exists(file_path, access = "r") 31 | 32 | # Check file size (VirusTotal has a 650MB limit for public API) 33 | file_size <- file.info(file_path)$size 34 | if (file_size > 650 * 1024 * 1024) { 35 | stop(virustotal_validation_error( 36 | message = "File size exceeds 650MB limit for public API", 37 | parameter = "file_path", 38 | value = paste(round(file_size / 1024 / 1024, 2), "MB") 39 | )) 40 | } 41 | 42 | tryCatch({ 43 | res <- virustotal_POST( 44 | path = "files", 45 | body = list(file = upload_file(file_path)), 46 | ... 47 | ) 48 | 49 | # Return structured response 50 | virustotal_file_scan(res) 51 | }, error = function(e) { 52 | if (!inherits(e, "virustotal_error")) { 53 | stop(virustotal_error( 54 | message = paste("Failed to upload file:", e$message), 55 | response = NULL 56 | )) 57 | } 58 | stop(e) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yml: -------------------------------------------------------------------------------- 1 | name: R-CMD-check 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | pull_request: 7 | branches: [ master, main ] 8 | schedule: 9 | # Run weekly to catch issues with dependencies 10 | - cron: '0 8 * * 1' 11 | 12 | jobs: 13 | R-CMD-check: 14 | runs-on: ${{ matrix.config.os }} 15 | 16 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | config: 22 | - {os: ubuntu-latest, r: 'oldrel-1'} 23 | - {os: ubuntu-latest, r: 'release'} 24 | - {os: ubuntu-latest, r: 'devel'} 25 | - {os: windows-latest, r: 'release'} 26 | - {os: macOS-latest, r: 'release'} 27 | 28 | env: 29 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 30 | VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} 31 | R_KEEP_PKG_SOURCE: yes 32 | _R_CHECK_CRAN_INCOMING_: false 33 | _R_CHECK_FORCE_SUGGESTS_: false 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v4 38 | 39 | - name: Setup Pandoc 40 | uses: r-lib/actions/setup-pandoc@v2 41 | 42 | - name: Setup R 43 | uses: r-lib/actions/setup-r@v2 44 | with: 45 | r-version: ${{ matrix.config.r }} 46 | http-user-agent: ${{ matrix.config.http-user-agent }} 47 | use-public-rspm: true 48 | 49 | - name: Setup R dependencies 50 | uses: r-lib/actions/setup-r-dependencies@v2 51 | with: 52 | extra-packages: any::rcmdcheck, any::lintr 53 | needs: check 54 | 55 | - name: Run R CMD check 56 | uses: r-lib/actions/check-r-package@v2 57 | with: 58 | upload-snapshots: true 59 | error-on: '"error"' 60 | 61 | - name: Run lintr 62 | if: matrix.config.os == 'ubuntu-latest' && matrix.config.r == 'release' 63 | run: | 64 | Rscript -e "lintr::lint_package()" -------------------------------------------------------------------------------- /R/rescan_ip.R: -------------------------------------------------------------------------------- 1 | #' Request rescan of an IP address 2 | #' 3 | #' Request a new analysis of an IP address already present in VirusTotal's database. 4 | #' Returns an analysis ID that can be used to retrieve the report using \code{\link{ip_report}}. 5 | #' 6 | #' @param ip IP address to rescan (IPv4 or IPv6). String. Required. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_POST}}. 8 | #' 9 | #' @return list containing analysis details and ID 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{ip_report}} for getting reports 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' # Request rescan of an IPv4 address 22 | #' rescan_ip("8.8.8.8") 23 | #' 24 | #' # Request rescan of an IPv6 address 25 | #' rescan_ip("2001:4860:4860::8888") 26 | #' } 27 | 28 | rescan_ip <- function(ip = NULL, ...) { 29 | 30 | assert_character(ip, len = 1, any.missing = FALSE, min.chars = 1) 31 | 32 | # Validate IP address format 33 | ip <- validate_input(ip) 34 | 35 | # Basic IP validation (IPv4 and IPv6) 36 | if (!is_valid_ip(ip)) { 37 | stop("Invalid IP address format. Must be a valid IPv4 or IPv6 address.\n") 38 | } 39 | 40 | res <- virustotal_POST(path = paste0("ip_addresses/", ip, "/rescan"), ...) 41 | 42 | # Return structured response 43 | structure(res, class = c("virustotal_response", "list")) 44 | } 45 | 46 | # Helper function to validate IP addresses 47 | is_valid_ip <- function(ip) { 48 | # Simple regex for IPv4 49 | ipv4_pattern <- "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" 50 | 51 | # Simple regex for IPv6 (basic validation) 52 | ipv6_pattern <- "^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^::$|^::1$|^([0-9a-fA-F]{1,4}:){1,6}:$|^::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$" 53 | 54 | grepl(ipv4_pattern, ip) || grepl(ipv6_pattern, ip) 55 | } 56 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(domain_report) 4 | export(download_file) 5 | export(file_report) 6 | export(get_domain_comments) 7 | export(get_domain_info) 8 | export(get_domain_relationship) 9 | export(get_domain_votes) 10 | export(get_file_comments) 11 | export(get_file_download_url) 12 | export(get_file_relationships) 13 | export(get_file_upload_url) 14 | export(get_file_votes) 15 | export(get_ip_comments) 16 | export(get_ip_info) 17 | export(get_ip_votes) 18 | export(get_url_comments) 19 | export(get_url_relationships) 20 | export(get_url_votes) 21 | export(ip_report) 22 | export(is_api_key_configured) 23 | export(post_domain_comments) 24 | export(post_domain_votes) 25 | export(post_file_comments) 26 | export(post_file_votes) 27 | export(post_ip_comments) 28 | export(post_ip_votes) 29 | export(post_url_comments) 30 | export(post_url_votes) 31 | export(rescan_domain) 32 | export(rescan_file) 33 | export(rescan_ip) 34 | export(rescan_url) 35 | export(scan_file) 36 | export(scan_url) 37 | export(set_key) 38 | export(url_report) 39 | export(virustotal_auth_error) 40 | export(virustotal_domain_report) 41 | export(virustotal_error) 42 | export(virustotal_file_report) 43 | export(virustotal_file_scan) 44 | export(virustotal_info) 45 | export(virustotal_ip_report) 46 | export(virustotal_rate_limit_error) 47 | export(virustotal_url_scan) 48 | export(virustotal_validation_error) 49 | export(virustotal_version) 50 | importFrom(base64enc,base64encode) 51 | importFrom(checkmate,assert_character) 52 | importFrom(checkmate,assert_file_exists) 53 | importFrom(checkmate,assert_numeric) 54 | importFrom(dplyr,bind_rows) 55 | importFrom(httr,GET) 56 | importFrom(httr,POST) 57 | importFrom(httr,add_headers) 58 | importFrom(httr,content) 59 | importFrom(httr,headers) 60 | importFrom(httr,parse_url) 61 | importFrom(httr,upload_file) 62 | importFrom(jsonlite,fromJSON) 63 | importFrom(jsonlite,toJSON) 64 | importFrom(rlang,.data) 65 | importFrom(tools,toTitleCase) 66 | importFrom(utils,URLencode) 67 | importFrom(utils,packageDescription) 68 | importFrom(utils,read.table) 69 | -------------------------------------------------------------------------------- /R/get_file_relationships.R: -------------------------------------------------------------------------------- 1 | #' Retrieve relationships for a file 2 | #' 3 | #' @param hash File hash (MD5, SHA1, or SHA256) 4 | #' @param relationship Type of relationship: "behaviours", "bundled_files", "compression_parents", "contacted_domains", "contacted_ips", "contacted_urls", "dropped_files", "execution_parents", "itw_domains", "itw_ips", "itw_urls", "overlay_parents", "pcap_parents", "pe_resource_parents", "similar_files", "submissions" 5 | #' @param limit Number of relationships to retrieve. Integer. Optional. Default is 10. 6 | #' @param cursor String for pagination. Optional. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 8 | #' 9 | #' @return list containing file relationships 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' get_file_relationships(hash='99017f6eebbac24f351415dd410d522d', 22 | #' relationship='contacted_domains') 23 | #' } 24 | 25 | get_file_relationships <- function(hash = NULL, relationship = NULL, 26 | limit = NULL, cursor = NULL, ...) { 27 | 28 | assert_character(hash, len = 1, any.missing = FALSE, min.chars = 1) 29 | assert_character(relationship, len = 1, any.missing = FALSE, min.chars = 1) 30 | 31 | valid_relationships <- c("behaviours", "bundled_files", "compression_parents", 32 | "contacted_domains", "contacted_ips", "contacted_urls", 33 | "dropped_files", "execution_parents", "itw_domains", 34 | "itw_ips", "itw_urls", "overlay_parents", "pcap_parents", 35 | "pe_resource_parents", "similar_files", "submissions") 36 | 37 | if (!relationship %in% valid_relationships) { 38 | stop("Invalid relationship type") 39 | } 40 | 41 | res <- virustotal_GET(path = paste0("files/", hash, "/relationships/", relationship), 42 | query = list(limit = limit, cursor = cursor), ...) 43 | 44 | res 45 | } 46 | -------------------------------------------------------------------------------- /R/get_url_relationships.R: -------------------------------------------------------------------------------- 1 | #' Retrieve relationships for a URL 2 | #' 3 | #' @param url_id URL or URL ID from VirusTotal 4 | #' @param relationship Type of relationship: "communicating_files", "downloaded_files", "graphs", "last_serving_ip_address", "network_location", "redirecting_urls", "redirects_to", "referrer_urls", "submissions" 5 | #' @param limit Number of relationships to retrieve. Integer. Optional. Default is 10. 6 | #' @param cursor String for pagination. Optional. 7 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}}. 8 | #' 9 | #' @return list containing URL relationships 10 | #' 11 | #' @export 12 | #' 13 | #' @references \url{https://docs.virustotal.com/reference} 14 | #' 15 | #' @seealso \code{\link{set_key}} for setting the API key 16 | #' 17 | #' @examples \dontrun{ 18 | #' 19 | #' # Before calling the function, set the API key using set_key('api_key_here') 20 | #' 21 | #' get_url_relationships(url_id='http://www.google.com', 22 | #' relationship='communicating_files') 23 | #' } 24 | 25 | get_url_relationships <- function(url_id = NULL, relationship = NULL, 26 | limit = NULL, cursor = NULL, ...) { 27 | 28 | assert_character(url_id, len = 1, any.missing = FALSE, min.chars = 1) 29 | assert_character(relationship, len = 1, any.missing = FALSE, min.chars = 1) 30 | 31 | valid_relationships <- c("communicating_files", "downloaded_files", "graphs", 32 | "last_serving_ip_address", "network_location", 33 | "redirecting_urls", "redirects_to", "referrer_urls", 34 | "submissions") 35 | 36 | if (!relationship %in% valid_relationships) { 37 | stop("Invalid relationship type. Must be one of: ", 38 | paste(valid_relationships, collapse = ", "), "\n") 39 | } 40 | 41 | # If it looks like a URL, encode it to base64 (VirusTotal v3 requirement) 42 | if (grepl("^https?://", url_id)) { 43 | url_id <- base64encode(charToRaw(url_id)) 44 | url_id <- gsub("=+$", "", url_id) # Remove padding 45 | } 46 | 47 | res <- virustotal_GET(path = paste0("urls/", url_id, "/relationships/", relationship), 48 | query = list(limit = limit, cursor = cursor), ...) 49 | 50 | res 51 | } 52 | -------------------------------------------------------------------------------- /tests/testthat/test-error-handling.R: -------------------------------------------------------------------------------- 1 | test_that("virustotal error classes work correctly", { 2 | # Test basic error 3 | err <- virustotal_error("Test message", status_code = 400) 4 | expect_s3_class(err, "virustotal_error") 5 | expect_equal(err$message, "Test message") 6 | expect_equal(err$status_code, 400) 7 | 8 | # Test rate limit error 9 | rate_err <- virustotal_rate_limit_error("Rate limit", retry_after = 60) 10 | expect_s3_class(rate_err, "virustotal_rate_limit_error") 11 | expect_s3_class(rate_err, "virustotal_error") 12 | expect_equal(rate_err$retry_after, 60) 13 | 14 | # Test auth error 15 | auth_err <- virustotal_auth_error("Auth failed") 16 | expect_s3_class(auth_err, "virustotal_auth_error") 17 | expect_s3_class(auth_err, "virustotal_error") 18 | 19 | # Test validation error 20 | val_err <- virustotal_validation_error("Invalid param", parameter = "test", value = "bad") 21 | expect_s3_class(val_err, "virustotal_validation_error") 22 | expect_s3_class(val_err, "virustotal_error") 23 | expect_equal(val_err$parameter, "test") 24 | expect_equal(val_err$value, "bad") 25 | }) 26 | 27 | test_that("error printing works", { 28 | err <- virustotal_error("Test error", status_code = 404) 29 | output <- capture.output(print(err)) 30 | expect_true(grepl("VirusTotal API Error: Test error", output[1])) 31 | expect_true(grepl("HTTP Status Code: 404", output[2])) 32 | }) 33 | 34 | test_that("virustotal_check handles HTTP status codes", { 35 | # Test success - should not error 36 | success_resp <- list(status_code = 200) 37 | expect_silent(virustotal_check(success_resp)) 38 | 39 | # Test auth error 40 | auth_resp <- list(status_code = 401) 41 | expect_error(virustotal_check(auth_resp), class = "virustotal_auth_error") 42 | 43 | # Test not found 44 | not_found_resp <- list(status_code = 404) 45 | expect_error(virustotal_check(not_found_resp), class = "virustotal_error") 46 | 47 | # Test server error 48 | server_error_resp <- list(status_code = 500) 49 | expect_error(virustotal_check(server_error_resp), class = "virustotal_error") 50 | 51 | # Test rate limit - the function should handle cases where headers might not be available 52 | rate_limit_resp <- list(status_code = 204) 53 | expect_error(virustotal_check(rate_limit_resp), class = "virustotal_rate_limit_error") 54 | }) 55 | 56 | -------------------------------------------------------------------------------- /tests/testthat/test-auth.R: -------------------------------------------------------------------------------- 1 | test_that("set_key validates input correctly", { 2 | # Missing argument 3 | expect_error(set_key(), class = "virustotal_validation_error") 4 | 5 | # Wrong types 6 | expect_error(set_key(NULL), class = "virustotal_validation_error") 7 | expect_error(set_key(123), class = "virustotal_validation_error") 8 | expect_error(set_key(character(0)), class = "virustotal_validation_error") 9 | 10 | # Too short 11 | expect_error(set_key("short"), class = "virustotal_validation_error") 12 | 13 | # Common placeholder values 14 | expect_error(set_key("your_api_key_here"), class = "virustotal_validation_error") 15 | expect_error(set_key("api_key_here"), class = "virustotal_validation_error") 16 | }) 17 | 18 | test_that("set_key handles whitespace correctly", { 19 | old_key <- Sys.getenv("VirustotalToken") 20 | 21 | # Test trimming whitespace 22 | expect_warning( 23 | set_key(" valid_32_character_api_key_1234567 "), 24 | "Removed leading/trailing whitespace" 25 | ) 26 | expect_equal(Sys.getenv("VirustotalToken"), "valid_32_character_api_key_1234567") 27 | 28 | # Restore original key 29 | if (old_key != "") { 30 | Sys.setenv(VirustotalToken = old_key) 31 | } else { 32 | Sys.unsetenv("VirustotalToken") 33 | } 34 | }) 35 | 36 | test_that("set_key sets environment variable correctly", { 37 | old_key <- Sys.getenv("VirustotalToken") 38 | 39 | expect_message( 40 | result <- set_key("valid_32_character_api_key_1234567890"), 41 | "VirusTotal API key successfully set" 42 | ) 43 | expect_true(result) 44 | expect_equal(Sys.getenv("VirustotalToken"), "valid_32_character_api_key_1234567890") 45 | 46 | # Restore original key 47 | if (old_key != "") { 48 | Sys.setenv(VirustotalToken = old_key) 49 | } else { 50 | Sys.unsetenv("VirustotalToken") 51 | } 52 | }) 53 | 54 | test_that("API functions require API key", { 55 | old_key <- Sys.getenv("VirustotalToken") 56 | Sys.unsetenv("VirustotalToken") 57 | 58 | expect_error(file_report("dummy_hash"), class = "virustotal_auth_error") 59 | expect_error(ip_report("8.8.8.8"), class = "virustotal_auth_error") 60 | expect_error(domain_report("example.com"), class = "virustotal_auth_error") 61 | 62 | # Restore original key 63 | if (old_key != "") { 64 | Sys.setenv(VirustotalToken = old_key) 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /R/set_key.R: -------------------------------------------------------------------------------- 1 | #' Set VirusTotal API Key 2 | #' 3 | #' Stores your VirusTotal API key in an environment variable for use by other 4 | #' package functions. Get your API key from \url{https://www.virustotal.com/}. 5 | #' 6 | #' @param api_key VirusTotal API key (character string). Required. 7 | #' 8 | #' @return Invisibly returns TRUE on success 9 | #' @export 10 | #' @family authentication 11 | #' 12 | #' @references \url{https://docs.virustotal.com/reference} 13 | #' 14 | #' @examples \dontrun{ 15 | #' # Set your API key 16 | #' set_key('your_64_character_api_key_here') 17 | #' 18 | #' # Verify it's set 19 | #' Sys.getenv("VirustotalToken") 20 | #' } 21 | 22 | set_key <- function(api_key = NULL) { 23 | # Handle NULL argument 24 | if (is.null(api_key)) { 25 | stop(virustotal_validation_error( 26 | message = "API key must be provided", 27 | parameter = "api_key", 28 | value = "NULL" 29 | )) 30 | } 31 | 32 | # Input validation with proper error handling 33 | tryCatch({ 34 | assert_character(api_key, len = 1, any.missing = FALSE, min.chars = 1) 35 | }, error = function(e) { 36 | stop(virustotal_validation_error( 37 | message = "API key must be a non-empty character string", 38 | parameter = "api_key", 39 | value = if (is.null(api_key)) "NULL" else class(api_key)[1] 40 | )) 41 | }) 42 | 43 | # Validate API key format (VirusTotal keys are typically 64 characters) 44 | if (nchar(api_key) < 32) { 45 | stop(virustotal_validation_error( 46 | message = "API key appears to be too short. VirusTotal keys are typically 64 characters.", 47 | parameter = "api_key", 48 | value = paste("Length:", nchar(api_key)) 49 | )) 50 | } 51 | 52 | # Check for common mistakes 53 | if (grepl("^[[:space:]]+|[[:space:]]+$", api_key)) { 54 | api_key <- trimws(api_key) 55 | warning("Removed leading/trailing whitespace from API key.") 56 | } 57 | 58 | if (api_key == "your_api_key_here" || api_key == "api_key_here") { 59 | stop(virustotal_validation_error( 60 | message = "Please replace placeholder with your actual API key", 61 | parameter = "api_key", 62 | value = api_key 63 | )) 64 | } 65 | 66 | # Set the environment variable 67 | Sys.setenv(VirustotalToken = api_key) 68 | 69 | message("VirusTotal API key successfully set.") 70 | invisible(TRUE) 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## virustotal: R Client for the VirusTotal Public API v3.0 2 | 3 | [![R-CMD-check](https://github.com/themains/virustotal/actions/workflows/R-CMD-check.yml/badge.svg)](https://github.com/themains/virustotal/actions/workflows/R-CMD-check.yml) 4 | [![pkgdown](https://github.com/themains/virustotal/actions/workflows/pkgdown.yml/badge.svg)](https://github.com/themains/virustotal/actions/workflows/pkgdown.yml) 5 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/virustotal)](https://cran.r-project.org/package=virustotal) 6 | ![](https://cranlogs.r-pkg.org/badges/grand-total/virustotal) 7 | 8 | 9 | Use [VirusTotal](https://www.virustotal.com), a Google service that analyzes files and URLs for viruses, worms, trojans etc., provides category of the content hosted by a domain from a variety of prominent services, provides passive DNS information, among other things. 10 | 11 | This package provides comprehensive support for the VirusTotal API v3.0, which offers richer data including IoC relationships, sandbox dynamic analysis, static file information, YARA rules, and comprehensive threat intelligence. 12 | 13 | **API Rate Limits:** 14 | - **Public API**: 500 requests/day, 4 requests/minute 15 | - **Premium API**: No daily or rate limitations 16 | 17 | **Supported Operations:** 18 | - **Files**: Upload, scan, get reports, download, comments, votes, relationships 19 | - **URLs**: Submit for analysis, get reports, comments, votes, relationships 20 | - **Domains**: Get reports, comments, votes, relationships, WHOIS data 21 | - **IP Addresses**: Get reports, comments, votes, relationships, passive DNS 22 | 23 | See [https://www.virustotal.com](https://www.virustotal.com) for more information. 24 | 25 | ### Installation 26 | 27 | To get the current released version from CRAN: 28 | ```r 29 | install.packages("virustotal") 30 | ``` 31 | 32 | To get the current development version from GitHub: 33 | 34 | ```r 35 | install.packages("devtools") 36 | devtools::install_github("themains/virustotal", build_vignettes = TRUE) 37 | ``` 38 | 39 | ### Usage 40 | 41 | To learn about how to use the package, read the [vignette](vignettes/using_virustotal.Rmd). Or launch the vignette within R: 42 | 43 | ```r 44 | # Using virustotal 45 | vignette("using_virustotal", package = "virustotal") 46 | ``` 47 | 48 | ### License 49 | Scripts are released under the [MIT License](https://opensource.org/licenses/MIT). 50 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is an R package (`virustotal`) that provides an R client for the VirusTotal Public API v3.0. VirusTotal is a Google service that analyzes files and URLs for malware, provides domain categorization, and offers passive DNS information. 8 | 9 | ## Key Development Commands 10 | 11 | ### Testing 12 | ```bash 13 | # Run all tests 14 | R -e "devtools::test()" 15 | 16 | # Run tests using testthat directly 17 | R -e "testthat::test_dir('tests/testthat')" 18 | 19 | # Run package checks (includes tests) 20 | R CMD check . 21 | ``` 22 | 23 | ### Linting 24 | ```bash 25 | # Run lintr (mentioned in DESCRIPTION suggests) 26 | R -e "lintr::lint_package()" 27 | ``` 28 | 29 | ### Building and Installation 30 | ```bash 31 | # Build package 32 | R CMD build . 33 | 34 | # Install from source 35 | R CMD INSTALL . 36 | 37 | # Development installation with devtools 38 | R -e "devtools::install(build_vignettes = TRUE)" 39 | 40 | # Build documentation 41 | R -e "devtools::document()" 42 | 43 | # Build pkgdown site 44 | R -e "pkgdown::build_site()" 45 | ``` 46 | 47 | ## Architecture 48 | 49 | ### Core Components 50 | 51 | 1. **Authentication**: `R/set_key.R` - Manages API key storage in environment variable `VirustotalToken` 52 | 53 | 2. **HTTP Layer**: `R/virustotal.R` - Contains base GET/POST functions: 54 | - `virustotal_GET()`, `virustotal_POST()` for v3 API 55 | - `rate_limit()` enforces 4 requests/minute limit 56 | - `virustotal_check()` handles response validation 57 | 58 | 3. **API Endpoints**: Individual R files for each API endpoint: 59 | - File operations: `scan_file.R`, `file_report.R`, `rescan_file.R` 60 | - URL operations: `scan_url.R`, `url_report.R` 61 | - IP operations: `ip_report.R`, `get_ip_info.R`, `get_ip_votes.R`, etc. 62 | - Domain operations: `domain_report.R`, `get_domain_info.R`, etc. 63 | - Comments/votes: `post_comments.R`, `add_comments.R`, etc. 64 | 65 | ### Rate Limiting 66 | The package implements automatic rate limiting via environment variable `VT_RATE_LIMIT` that tracks request count and timing to enforce VirusTotal's 4 requests/minute limit. 67 | 68 | ### API Versions 69 | - All functions use v3 API (`virustotal_*` functions) 70 | - v3 API uses `x-apikey` header for authentication 71 | 72 | ## Testing Notes 73 | 74 | Tests require a VirusTotal API key stored in `tests/testthat/virustotal_api_key.enc`. Tests are skipped on CRAN and only run when API key is available. 75 | 76 | ## Package Structure 77 | 78 | Standard R package structure with roxygen2 documentation. Uses pkgdown for website generation. Exports 20+ functions for interacting with VirusTotal API endpoints. -------------------------------------------------------------------------------- /tests/testthat/test-ip-operations.R: -------------------------------------------------------------------------------- 1 | # IP Operations Tests 2 | 3 | test_that("ip_report validates input correctly", { 4 | expect_error(ip_report(), "Assertion on 'ip' failed") 5 | expect_error(ip_report(NULL), "Assertion on 'ip' failed") 6 | expect_error(ip_report(123), "Assertion on 'ip' failed") 7 | expect_error(ip_report(""), "All elements must have at least 1 characters") 8 | }) 9 | 10 | # IP Comments Tests 11 | test_that("get_ip_comments validates input correctly", { 12 | expect_error(get_ip_comments(), "Assertion on 'ip' failed") 13 | expect_error(get_ip_comments(NULL), "Assertion on 'ip' failed") 14 | expect_error(get_ip_comments(123), "Assertion on 'ip' failed") 15 | expect_error(get_ip_comments(""), "All elements must have at least 1 characters") 16 | }) 17 | 18 | test_that("post_ip_comments validates input correctly", { 19 | expect_error(post_ip_comments(), "Assertion on 'ip' failed") 20 | expect_error(post_ip_comments("1.2.3.4"), "Assertion on 'comment' failed") 21 | expect_error(post_ip_comments("1.2.3.4", ""), "All elements must have at least 1 characters") 22 | }) 23 | 24 | # IP Votes Tests 25 | test_that("get_ip_votes validates input correctly", { 26 | expect_error(get_ip_votes(), "Assertion on 'ip' failed") 27 | expect_error(get_ip_votes(NULL), "Assertion on 'ip' failed") 28 | expect_error(get_ip_votes(""), "All elements must have at least 1 characters") 29 | }) 30 | 31 | test_that("post_ip_votes validates input correctly", { 32 | expect_error(post_ip_votes(), "Assertion on 'ip' failed") 33 | expect_error(post_ip_votes("1.2.3.4"), "Assertion on 'vote' failed") 34 | expect_error(post_ip_votes("1.2.3.4", ""), "All elements must have at least 1 characters") 35 | }) 36 | 37 | # IP Info Tests 38 | test_that("get_ip_info validates input correctly", { 39 | expect_error(get_ip_info(), "Assertion on 'ip' failed") 40 | expect_error(get_ip_info(NULL), "Assertion on 'ip' failed") 41 | expect_error(get_ip_info(""), "All elements must have at least 1 characters") 42 | }) 43 | 44 | # Rescan Tests 45 | test_that("rescan_ip validates input correctly", { 46 | expect_error(rescan_ip(), "Assertion on 'ip' failed") 47 | expect_error(rescan_ip(NULL), "Assertion on 'ip' failed") 48 | expect_error(rescan_ip(""), "All elements must have at least 1 characters") 49 | }) 50 | 51 | test_that("ip operations work with mocked responses", { 52 | skip_if_not_installed("httptest") 53 | skip_if(Sys.getenv("VirustotalToken") == "", "API key not set") 54 | 55 | expect_true(exists("ip_report")) 56 | expect_true(exists("get_ip_comments")) 57 | expect_true(exists("post_ip_comments")) 58 | expect_true(exists("get_ip_votes")) 59 | expect_true(exists("post_ip_votes")) 60 | expect_true(exists("get_ip_info")) 61 | expect_true(exists("rescan_ip")) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/testthat/test-rate-limiting.R: -------------------------------------------------------------------------------- 1 | test_that("rate limiting initialization works", { 2 | # Reset state and explicitly check initialization 3 | reset_rate_limit() 4 | 5 | # Verify environment is properly set up 6 | expect_true(.virustotal_state$initialized) 7 | expect_equal(.virustotal_state$max_requests, 4) 8 | expect_equal(.virustotal_state$window_size, 60) 9 | expect_length(.virustotal_state$requests, 0) 10 | 11 | # Test status function 12 | status <- get_rate_limit_status() 13 | expect_equal(status$requests_used, 0) 14 | expect_equal(status$max_requests, 4) 15 | expect_equal(status$requests_remaining, 4) 16 | }) 17 | 18 | test_that("rate limiting tracks requests", { 19 | # Reset state 20 | reset_rate_limit() 21 | 22 | # Make some requests 23 | rate_limit() 24 | status1 <- get_rate_limit_status() 25 | expect_equal(status1$requests_used, 1) 26 | expect_equal(status1$requests_remaining, 3) 27 | 28 | rate_limit() 29 | rate_limit() 30 | status2 <- get_rate_limit_status() 31 | expect_equal(status2$requests_used, 3) 32 | expect_equal(status2$requests_remaining, 1) 33 | }) 34 | 35 | test_that("rate limiting enforces limits", { 36 | # Reset state 37 | reset_rate_limit() 38 | 39 | # Fill up the rate limit 40 | rate_limit() 41 | rate_limit() 42 | rate_limit() 43 | rate_limit() 44 | 45 | status <- get_rate_limit_status() 46 | expect_equal(status$requests_used, 4) 47 | expect_equal(status$requests_remaining, 0) 48 | 49 | # Next request should trigger waiting (we can't easily test the actual waiting) 50 | # Just verify the function doesn't error - expect message about rate limiting 51 | expect_message(rate_limit(), "Rate limit reached") 52 | }) 53 | 54 | test_that("rate limiting window slides correctly", { 55 | # Reset state 56 | reset_rate_limit() 57 | 58 | # Manually add old requests that should be expired 59 | current_time <- as.numeric(Sys.time()) 60 | .virustotal_state$requests <- c(current_time - 70, current_time - 65) # Older than 60 seconds 61 | 62 | # Check that old requests are cleaned 63 | status <- get_rate_limit_status() 64 | expect_equal(status$requests_used, 0) # Old requests should be cleaned 65 | }) 66 | 67 | test_that("rate limiting handles NULL/empty states gracefully", { 68 | # Test what happens if environment is in a bad state 69 | .virustotal_state$requests <- NULL 70 | .virustotal_state$max_requests <- NULL 71 | .virustotal_state$window_size <- NULL 72 | .virustotal_state$initialized <- NULL 73 | 74 | # Should not error and should reinitialize 75 | expect_no_error({ 76 | status <- get_rate_limit_status() 77 | expect_equal(status$max_requests, 4) 78 | expect_equal(status$requests_used, 0) 79 | }) 80 | 81 | # Rate limit should also work 82 | expect_no_error(rate_limit()) 83 | }) 84 | -------------------------------------------------------------------------------- /R/file_report.R: -------------------------------------------------------------------------------- 1 | #' Get File Scan Report 2 | #' 3 | #' Retrieves detailed analysis results for a file from VirusTotal using the v3 API. 4 | #' 5 | #' @param hash File hash (MD5, SHA1, or SHA256) or analysis ID 6 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}} 7 | #' 8 | #' @return A \code{virustotal_file_report} object containing file analysis results 9 | #' including antivirus scans, file metadata, and threat detection information 10 | #' 11 | #' @export 12 | #' @family file operations 13 | #' 14 | #' @references \url{https://docs.virustotal.com/reference/files} 15 | #' 16 | #' @seealso \code{\link{set_key}} for setting the API key, \code{\link{scan_file}} for submitting files 17 | #' 18 | #' @examples \dontrun{ 19 | #' # Set API key first 20 | #' set_key('your_api_key_here') 21 | #' 22 | #' # Get file report using hash 23 | #' report <- file_report(hash = '99017f6eebbac24f351415dd410d522d') 24 | #' print(report) 25 | #' summary(report) 26 | #' 27 | #' # Work with the rich nested structure returned by v3 API 28 | #' print(report$data$attributes$last_analysis_stats) 29 | #' } 30 | 31 | file_report <- function(hash, ...) { 32 | # Handle missing argument first 33 | if (missing(hash)) { 34 | stop(virustotal_validation_error( 35 | message = "Hash must be provided", 36 | parameter = "hash", 37 | value = "missing" 38 | )) 39 | } 40 | 41 | # Handle NULL and type validation before API key (for proper test precedence) 42 | if (is.null(hash)) { 43 | stop(virustotal_validation_error( 44 | message = "Hash cannot be NULL", 45 | parameter = "hash", 46 | value = "NULL" 47 | )) 48 | } 49 | 50 | # Input validation 51 | tryCatch({ 52 | assert_character(hash, len = 1, any.missing = FALSE, 53 | min.chars = 1) 54 | }, error = function(e) { 55 | stop(virustotal_validation_error( 56 | message = "Hash must be a non-empty character string", 57 | parameter = "hash", 58 | value = if (is.null(hash)) "NULL" else class(hash)[1] 59 | )) 60 | }) 61 | 62 | # Check API key after basic validation 63 | if (identical(Sys.getenv("VirustotalToken"), "")) { 64 | stop(virustotal_auth_error( 65 | message = "Authentication failed. Please check your API key." 66 | )) 67 | } 68 | 69 | # Validate hash format (basic check) 70 | # MD5, SHA1, SHA256, or analysis ID lengths 71 | if (!grepl("^[a-fA-F0-9]+$", hash) || 72 | (!nchar(hash) %in% c(32, 40, 64, 40))) { 73 | stop(virustotal_validation_error( 74 | message = paste("Hash must be a valid MD5 (32), SHA1 (40),", 75 | "SHA256 (64), or analysis ID"), 76 | parameter = "hash", 77 | value = hash 78 | )) 79 | } 80 | 81 | tryCatch({ 82 | res <- virustotal_GET(path = paste0("files/", hash), ...) 83 | 84 | # Return structured response 85 | virustotal_file_report(res) 86 | }, error = function(e) { 87 | if (!inherits(e, "virustotal_error")) { 88 | stop(virustotal_error( 89 | message = paste("Failed to retrieve file report:", e$message), 90 | response = NULL 91 | )) 92 | } 93 | stop(e) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /R/domain_report.R: -------------------------------------------------------------------------------- 1 | #' Get Domain Report 2 | #' 3 | #' Retrieves comprehensive analysis report for a given domain, including 4 | #' WHOIS information, DNS resolutions, detected URLs, and threat intelligence 5 | #' data. 6 | #' 7 | #' @param domain Domain name (character string). Required. 8 | #' @param \dots Additional arguments passed to \code{\link{virustotal_GET}} 9 | #' 10 | #' @return A \code{virustotal_domain_report} object containing domain analysis 11 | #' results including WHOIS data, DNS resolutions, detected URLs, categories, 12 | #' and threat intelligence 13 | #' 14 | #' @export 15 | #' @family domain operations 16 | #' 17 | #' @references \url{https://docs.virustotal.com/reference/domains} 18 | #' 19 | #' @seealso \code{\link{set_key}} for setting the API key 20 | #' 21 | #' @examples \dontrun{ 22 | #' # Set API key first 23 | #' set_key('your_api_key_here') 24 | #' 25 | #' # Get domain reports 26 | #' report1 <- domain_report("google.com") 27 | #' report2 <- domain_report("https://www.example.com/path") 28 | #' 29 | #' print(report1) 30 | #' summary(report1) 31 | #' } 32 | 33 | domain_report <- function(domain = NULL, ...) { 34 | # Handle NULL argument 35 | if (is.null(domain)) { 36 | stop(virustotal_validation_error( 37 | message = "Domain must be provided", 38 | parameter = "domain", 39 | value = "NULL" 40 | )) 41 | } 42 | 43 | # Input validation with proper error handling (before API key for tests) 44 | tryCatch({ 45 | assert_character(domain, len = 1, any.missing = FALSE, 46 | min.chars = 1) 47 | }, error = function(e) { 48 | stop(virustotal_validation_error( 49 | message = "Domain must be a non-empty character string", 50 | parameter = "domain", 51 | value = if (is.null(domain)) "NULL" else class(domain)[1] 52 | )) 53 | }) 54 | 55 | # Clean up domain (remove protocol, www, and paths for security) before validation 56 | domain_clean <- domain 57 | domain_clean <- gsub("^https?://", "", domain_clean) 58 | domain_clean <- gsub("^www\\.", "", domain_clean) 59 | domain_clean <- gsub("/.*$", "", domain_clean) # Remove any path 60 | domain_clean <- gsub("\\.$", "", domain_clean) # Remove trailing dot 61 | 62 | # Basic domain validation (before API key check for test precedence) 63 | if (!grepl("^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$", domain_clean) || 64 | grepl("\\.\\.", domain_clean)) { 65 | stop(virustotal_validation_error( 66 | message = "Invalid domain format", 67 | parameter = "domain", 68 | value = domain 69 | )) 70 | } 71 | 72 | # Check API key after all validation 73 | if (identical(Sys.getenv("VirustotalToken"), "")) { 74 | stop(virustotal_auth_error( 75 | message = "Authentication failed. Please check your API key." 76 | )) 77 | } 78 | 79 | tryCatch({ 80 | res <- virustotal_GET(path = paste0("domains/", domain_clean), ...) 81 | 82 | # Return structured response 83 | virustotal_domain_report(res) 84 | }, error = function(e) { 85 | if (!inherits(e, "virustotal_error")) { 86 | stop(virustotal_error( 87 | message = paste("Failed to retrieve domain report:", e$message), 88 | response = NULL 89 | )) 90 | } 91 | stop(e) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Tests and Coverage 2 | ================ 3 | 06 November, 2018 18:49:57 4 | 5 | This output is created by 6 | [covrpage](https://github.com/yonicd/covrpage). 7 | 8 | ## Coverage 9 | 10 | Coverage summary is created using the 11 | [covr](https://github.com/r-lib/covr) package. 12 | 13 | | Object | Coverage (%) | 14 | | :----------------------------------------- | :----------: | 15 | | virustotal | 0 | 16 | | [R/add\_comments.R](../R/add_comments.R) | 0 | 17 | | [R/domain\_report.R](../R/domain_report.R) | 0 | 18 | | [R/file\_report.R](../R/file_report.R) | 0 | 19 | | [R/ip\_report.R](../R/ip_report.R) | 0 | 20 | | [R/rescan\_file.R](../R/rescan_file.R) | 0 | 21 | | [R/scan\_file.R](../R/scan_file.R) | 0 | 22 | | [R/scan\_url.R](../R/scan_url.R) | 0 | 23 | | [R/set\_key.R](../R/set_key.R) | 0 | 24 | | [R/url\_report.R](../R/url_report.R) | 0 | 25 | | [R/virustotal.R](../R/virustotal.R) | 0 | 26 | 27 |
28 | 29 | ## Unit Tests 30 | 31 | Unit Test summary is created using the 32 | [testthat](https://github.com/r-lib/testthat) 33 | package. 34 | 35 | | file | n | time | error | failed | skipped | warning | icon | 36 | | :-------------------------------------------------------- | -: | ---: | ----: | -----: | ------: | ------: | :--- | 37 | | [test-data-structures.R](testthat/test-data-structures.R) | 1 | 0 | 0 | 0 | 1 | 0 | \+ | 38 | | [test-pkg-style.R](testthat/test-pkg-style.R) | 1 | 0 | 0 | 0 | 1 | 0 | \+ | 39 | 40 |
41 | 42 | Show Detailed Test Results 43 | 44 | 45 | | file | context | test | status | n | time | icon | 46 | | :----------------------------------------------------------- | :--------------------- | :----------------------------------------------- | :------ | -: | ---: | :--- | 47 | | [test-data-structures.R](testthat/test-data-structures.R#L6) | Verify data structures | can decrypt secrets and data structures verified | SKIPPED | 1 | 0 | \+ | 48 | | [test-pkg-style.R](testthat/test-pkg-style.R#L5) | lints | Package Style | SKIPPED | 1 | 0 | \+ | 49 | 50 | | Failed | Warning | Skipped | 51 | | :----- | :------ | :------ | 52 | | \! | \- | \+ | 53 | 54 |
55 | 56 |
57 | 58 | Session Info 59 | 60 | | Field | Value | 61 | | :------- | :------------------------------- | 62 | | Version | R version 3.5.1 (2018-07-02) | 63 | | Platform | x86\_64-w64-mingw32/x64 (64-bit) | 64 | | Running | Windows 10 x64 (build 17134) | 65 | | Language | English\_United States | 66 | | Timezone | America/Los\_Angeles | 67 | 68 | | Package | Version | 69 | | :------- | :------ | 70 | | testthat | 2.0.1 | 71 | | covr | 3.2.1 | 72 | | covrpage | 0.0.62 | 73 | 74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /tests/testthat/test-url-operations.R: -------------------------------------------------------------------------------- 1 | # URL Operations Tests 2 | 3 | test_that("scan_url validates input correctly", { 4 | expect_error(scan_url(), "Assertion on 'url' failed") 5 | expect_error(scan_url(NULL), "Assertion on 'url' failed") 6 | expect_error(scan_url(123), "Assertion on 'url' failed") 7 | expect_error(scan_url(""), "All elements must have at least 1 characters") 8 | }) 9 | 10 | test_that("url_report validates input correctly", { 11 | expect_error(url_report(), "Assertion on 'url_id' failed") 12 | expect_error(url_report(NULL), "Assertion on 'url_id' failed") 13 | expect_error(url_report(123), "Assertion on 'url_id' failed") 14 | expect_error(url_report(""), "All elements must have at least 1 characters") 15 | }) 16 | 17 | # URL Comments Tests 18 | test_that("get_url_comments validates input correctly", { 19 | expect_error(get_url_comments(), "Assertion on 'url_id' failed") 20 | expect_error(get_url_comments(NULL), "Assertion on 'url_id' failed") 21 | expect_error(get_url_comments(123), "Assertion on 'url_id' failed") 22 | expect_error(get_url_comments(""), "All elements must have at least 1 characters") 23 | }) 24 | 25 | test_that("post_url_comments validates input correctly", { 26 | expect_error(post_url_comments(), "Assertion on 'url_id' failed") 27 | expect_error(post_url_comments("validurl"), "Assertion on 'comment' failed") 28 | expect_error(post_url_comments("validurl", ""), "All elements must have at least 1 characters") 29 | }) 30 | 31 | # URL Votes Tests 32 | test_that("get_url_votes validates input correctly", { 33 | expect_error(get_url_votes(), "Assertion on 'url_id' failed") 34 | expect_error(get_url_votes(NULL), "Assertion on 'url_id' failed") 35 | expect_error(get_url_votes(""), "All elements must have at least 1 characters") 36 | }) 37 | 38 | test_that("post_url_votes validates input correctly", { 39 | expect_error(post_url_votes(), "Assertion on 'url_id' failed") 40 | expect_error(post_url_votes("validurl"), "Verdict must be either 'harmless' or 'malicious'") 41 | expect_error(post_url_votes("validurl", "invalid"), "Verdict must be either 'harmless' or 'malicious'") 42 | }) 43 | 44 | # URL Relationships Tests 45 | test_that("get_url_relationships validates input correctly", { 46 | expect_error(get_url_relationships(), "Assertion on 'url_id' failed") 47 | expect_error(get_url_relationships("validurl"), "Assertion on 'relationship' failed") 48 | expect_error(get_url_relationships("validurl", ""), "All elements must have at least 1 characters") 49 | }) 50 | 51 | # Rescan Tests 52 | test_that("rescan_url validates input correctly", { 53 | expect_error(rescan_url(), "Assertion on 'url_id' failed") 54 | expect_error(rescan_url(NULL), "Assertion on 'url_id' failed") 55 | expect_error(rescan_url(""), "All elements must have at least 1 characters") 56 | }) 57 | 58 | test_that("URL encoding works correctly", { 59 | skip_if_not_installed("base64enc") 60 | 61 | # Test URL encoding logic (without API call) 62 | test_url <- "http://www.google.com" 63 | encoded <- base64enc::base64encode(charToRaw(test_url)) 64 | encoded <- gsub("=+$", "", encoded) 65 | 66 | expect_true(nchar(encoded) > 0) 67 | expect_false(grepl("=", encoded)) 68 | }) 69 | 70 | test_that("URL operations work with mocked responses", { 71 | skip_if_not_installed("httptest") 72 | skip_if(Sys.getenv("VirustotalToken") == "", "API key not set") 73 | 74 | expect_true(exists("scan_url")) 75 | expect_true(exists("url_report")) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/_covrpage.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tests and Coverage" 3 | date: "`r format(Sys.time(), '%d %B, %Y %H:%M:%S')`" 4 | --- 5 | 6 | This output is created by [covrpage](https://github.com/yonicd/covrpage). 7 | 8 | ```{r load,include=FALSE} 9 | library(covr , warn.conflicts = FALSE , quietly = TRUE) 10 | library(knitr , warn.conflicts = FALSE , quietly = TRUE) 11 | library(magrittr , warn.conflicts = FALSE , quietly = TRUE) 12 | 13 | ``` 14 | 15 | ```{r tests, include=FALSE} 16 | #test_x <- testthat::test_dir('testthat') 17 | this_pkg <- basename(normalizePath('..')) 18 | test_x <- testthat::test_check(this_pkg,reporter = testthat::default_reporter(),stop_on_failure = FALSE,stop_on_warning = FALSE) 19 | 20 | skip <- length(test_x)>0 21 | failed <- FALSE 22 | ``` 23 | 24 | ```{r maps, include=FALSE} 25 | test_m <- covrpage::map_testthat('testthat') 26 | ``` 27 | 28 | 29 | ```{r, include=FALSE} 30 | test_x_short <- test_x%>% 31 | covrpage::testthat_summary(type='short') 32 | 33 | test_x_long <- test_x%>% 34 | covrpage::testthat_summary(type='long') 35 | 36 | ``` 37 | 38 | ```{r, include=FALSE,eval=skip} 39 | test_skip <- test_x_long[test_x_long$status!='PASS',c('file','test')] 40 | 41 | test_skip$file <- gsub('#(.*?)$','',basename(test_skip$file)) 42 | 43 | test_skip <- merge(test_skip,test_m) 44 | 45 | failed <- any(test_x_long$status%in%c('ERROR','FAILED')) 46 | 47 | ``` 48 | 49 | ```{r, include=FALSE,eval=!skip} 50 | 51 | test_skip <- test_m 52 | 53 | ``` 54 | 55 | ## Coverage 56 | 57 | Coverage summary is created using the [covr](https://github.com/r-lib/covr) package. 58 | 59 | ```{r,include=FALSE} 60 | cvr <- covrpage::coverage_skip(test_path = '../tests/testthat', test_skip = test_skip) 61 | ``` 62 | 63 | ```{r,echo=FALSE,eval=failed} 64 | cat(sprintf('%s Not All Tests Passed\n Coverage statistics are approximations of the non-failing tests.\n Use with caution\n\n For further investigation check in testthat summary tables.',covrpage:::emos[[platform()]][['WARNING']])) 65 | ``` 66 | 67 | ```{r,echo=FALSE} 68 | cvr%>% 69 | covrpage::covr_summary(failed = failed)%>% 70 | knitr::kable(digits = 2, 71 | col.names = c('Object','Coverage (%)'),align = c('l','c')) 72 | 73 | ``` 74 | 75 |
76 | 77 | ## Unit Tests 78 | 79 | Unit Test summary is created using the [testthat](https://github.com/r-lib/testthat) package. 80 | 81 | ```{r,echo=FALSE,warning=FALSE,message=FALSE,eval=!skip} 82 | cat('All tests were skipped') 83 | ``` 84 | 85 | ```{r,echo=FALSE,warning=FALSE,message=FALSE,eval=skip} 86 | 87 | test_x_short%>% 88 | knitr::kable() 89 | 90 | ``` 91 | 92 |
93 | Show Detailed Test Results 94 | 95 | ```{r,echo=FALSE,warning=FALSE,message=FALSE,eval=!skip} 96 | cat('All tests were skipped') 97 | ``` 98 | 99 | ```{r,echo=FALSE,warning=FALSE,message=FALSE,eval=skip} 100 | 101 | test_x_long%>% 102 | knitr::kable() 103 | 104 | ``` 105 | 106 | ```{r,echo=FALSE} 107 | if(length(names(test_x_long))>0){ 108 | if('icon'%in%names(test_x_long)){ 109 | emos <- covrpage:::emos[[covrpage:::platform()]] 110 | knitr::kable(t(c('Failed' = emos[['FAILED']],'Warning' = emos[['WARNING']], 'Skipped' = emos[['SKIPPED']]))) 111 | } 112 | } 113 | ``` 114 | 115 |
116 | 117 |
118 | Session Info 119 | 120 | ```{r,echo=FALSE,warning=FALSE,message=FALSE} 121 | 122 | x <- covrpage:::sinfo() 123 | 124 | x$info%>% 125 | knitr::kable() 126 | 127 | x$pkgs%>% 128 | knitr::kable() 129 | 130 | ``` 131 | 132 |
133 | 134 | `r sprintf('', test_to_badge(test_x_short))` 135 | -------------------------------------------------------------------------------- /vignettes/using_virustotal.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Using virustotal" 3 | author: "Gaurav Sood" 4 | date: "`r Sys.Date()`" 5 | vignette: > 6 | %\VignetteIndexEntry{Using virustotal} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ## Using virustotal 12 | 13 | The virustotal package provides access to the VirusTotal API v3, allowing you to scan files and URLs for malware, get domain and IP intelligence, and retrieve comprehensive threat analysis reports. 14 | 15 | ### Installation 16 | 17 | To get the current development version from GitHub: 18 | 19 | ```{r, eval=F, install} 20 | # Install from CRAN 21 | install.packages("virustotal") 22 | 23 | # Or install development version 24 | # library(devtools) 25 | # install_github("themains/virustotal") 26 | ``` 27 | 28 | #### Load the library: 29 | 30 | ```{r, eval=F, load} 31 | library(virustotal) 32 | ``` 33 | 34 | #### Authentication 35 | 36 | 1. Get your free API key from [VirusTotal](https://www.virustotal.com/) 37 | 2. Set the API key in your R session: 38 | 39 | ```{r, eval=F, api_key} 40 | set_key("your_api_key_here") 41 | ``` 42 | 43 | ### Core Functions 44 | 45 | #### File Analysis 46 | 47 | **Scan a file for malware:** 48 | 49 | ```{r, eval=F, scan_file} 50 | # Submit a file for analysis 51 | result <- scan_file("path/to/suspicious_file.exe") 52 | analysis_id <- result$data$id 53 | ``` 54 | 55 | **Get file analysis report:** 56 | 57 | ```{r, eval=F, file_report} 58 | # Get analysis results using file hash 59 | report <- file_report("99017f6eebbac24f351415dd410d522d") 60 | 61 | # Access scan results 62 | scan_results <- report$data$attributes$last_analysis_results 63 | total_engines <- length(scan_results) 64 | detections <- sum(sapply(scan_results, function(x) x$category == "malicious")) 65 | ``` 66 | 67 | **Request file rescan:** 68 | 69 | ```{r, eval=F, rescan_file} 70 | # Request new analysis of existing file 71 | rescan_result <- rescan_file("99017f6eebbac24f351415dd410d522d") 72 | new_analysis_id <- rescan_result$data$id 73 | ``` 74 | 75 | #### URL Analysis 76 | 77 | **Scan a URL:** 78 | 79 | ```{r, eval=F, scan_url} 80 | # Submit URL for analysis 81 | url_result <- scan_url("http://suspicious-site.com") 82 | analysis_id <- url_result$data$id 83 | ``` 84 | 85 | **Get URL analysis report:** 86 | 87 | ```{r, eval=F, url_report} 88 | # Get analysis results using URL 89 | report <- url_report("http://www.google.com") 90 | 91 | # Access scan results 92 | scan_results <- report$data$attributes$last_analysis_results 93 | threat_score <- report$data$attributes$stats 94 | ``` 95 | 96 | #### Domain Intelligence 97 | 98 | **Get domain information:** 99 | 100 | ```{r, eval=F, domain} 101 | # Get comprehensive domain analysis 102 | domain_info <- domain_report("google.com") 103 | 104 | # Access various data points 105 | categories <- domain_info$data$attributes$categories 106 | whois_data <- domain_info$data$attributes$whois 107 | dns_records <- domain_info$data$attributes$dns_records 108 | ``` 109 | 110 | #### IP Address Intelligence 111 | 112 | **Get IP address information:** 113 | 114 | ```{r, eval=F, ip} 115 | # Get IP analysis including geolocation and ASN 116 | ip_info <- ip_report("8.8.8.8") 117 | 118 | # Access geo and network information 119 | country <- ip_info$data$attributes$country 120 | asn <- ip_info$data$attributes$asn 121 | network <- ip_info$data$attributes$network 122 | ``` 123 | 124 | ### Rate Limiting 125 | 126 | The package automatically handles VirusTotal's rate limits (4 requests per minute for free accounts). You don't need to implement your own rate limiting. 127 | 128 | ### Error Handling 129 | 130 | All functions include comprehensive input validation and will provide clear error messages for common issues like missing API keys or invalid parameters. 131 | 132 | -------------------------------------------------------------------------------- /tests/testthat/test-domain-operations.R: -------------------------------------------------------------------------------- 1 | # Domain Operations Tests 2 | 3 | test_that("domain_report validates input correctly", { 4 | expect_error(domain_report(), class = "virustotal_validation_error") 5 | expect_error(domain_report(NULL), class = "virustotal_validation_error") 6 | expect_error(domain_report(123), class = "virustotal_validation_error") 7 | expect_error(domain_report(""), class = "virustotal_validation_error") 8 | expect_error(domain_report("invalid..domain"), class = "virustotal_validation_error") 9 | }) 10 | 11 | test_that("domain cleaning works correctly", { 12 | # Test domain normalization logic (without API call) 13 | expect_equal(gsub("^https?://", "", "http://example.com"), "example.com") 14 | expect_equal(gsub("^www\\.", "", "www.example.com"), "example.com") 15 | expect_equal(gsub("/.*$", "", "example.com/path"), "example.com") 16 | }) 17 | 18 | # Domain Comments Tests 19 | test_that("get_domain_comments validates input correctly", { 20 | expect_error(get_domain_comments(), "Assertion on 'domain' failed") 21 | expect_error(get_domain_comments(NULL), "Assertion on 'domain' failed") 22 | expect_error(get_domain_comments(123), "Assertion on 'domain' failed") 23 | expect_error(get_domain_comments(""), "All elements must have at least 1 characters") 24 | }) 25 | 26 | test_that("post_domain_comments validates input correctly", { 27 | expect_error(post_domain_comments(), "Assertion on 'domain' failed") 28 | expect_error(post_domain_comments("example.com"), "Assertion on 'comment' failed") 29 | expect_error(post_domain_comments("example.com", ""), "All elements must have at least 1 characters") 30 | }) 31 | 32 | # Domain Votes Tests 33 | test_that("get_domain_votes validates input correctly", { 34 | expect_error(get_domain_votes(), "Assertion on 'domain' failed") 35 | expect_error(get_domain_votes(NULL), "Assertion on 'domain' failed") 36 | expect_error(get_domain_votes(""), "All elements must have at least 1 characters") 37 | }) 38 | 39 | test_that("post_domain_votes validates input correctly", { 40 | expect_error(post_domain_votes(), "Assertion on 'domain' failed") 41 | expect_error(post_domain_votes("example.com"), "Assertion on 'vote' failed") 42 | expect_error(post_domain_votes("example.com", ""), "All elements must have at least 1 characters") 43 | }) 44 | 45 | # Domain Info Tests 46 | test_that("get_domain_info validates input correctly", { 47 | expect_error(get_domain_info(), "Assertion on 'domain' failed") 48 | expect_error(get_domain_info(NULL), "Assertion on 'domain' failed") 49 | expect_error(get_domain_info(""), "All elements must have at least 1 characters") 50 | }) 51 | 52 | # Domain Relationships Tests 53 | test_that("get_domain_relationship validates input correctly", { 54 | expect_error(get_domain_relationship(), "Assertion on 'domain' failed") 55 | expect_error(get_domain_relationship(NULL), "Assertion on 'domain' failed") 56 | expect_error(get_domain_relationship(""), "All elements must have at least 1 characters") 57 | }) 58 | 59 | # Rescan Tests 60 | test_that("rescan_domain validates input correctly", { 61 | expect_error(rescan_domain(), "Assertion on 'domain' failed") 62 | expect_error(rescan_domain(NULL), "Assertion on 'domain' failed") 63 | expect_error(rescan_domain(""), "All elements must have at least 1 characters") 64 | }) 65 | 66 | test_that("domain operations work with mocked responses", { 67 | skip_if_not_installed("httptest") 68 | skip_if(Sys.getenv("VirustotalToken") == "", "API key not set") 69 | 70 | expect_true(exists("domain_report")) 71 | expect_true(exists("get_domain_comments")) 72 | expect_true(exists("post_domain_comments")) 73 | expect_true(exists("get_domain_votes")) 74 | expect_true(exists("post_domain_votes")) 75 | expect_true(exists("get_domain_info")) 76 | expect_true(exists("get_domain_relationship")) 77 | expect_true(exists("rescan_domain")) 78 | }) 79 | -------------------------------------------------------------------------------- /R/errors.R: -------------------------------------------------------------------------------- 1 | #' VirusTotal API Error Classes 2 | #' 3 | #' @description 4 | #' Custom error classes for structured error handling in the virustotal package. 5 | #' 6 | #' @name virustotal-errors 7 | #' @keywords internal 8 | #' @family error handling 9 | NULL 10 | 11 | #' Create a VirusTotal API error 12 | #' 13 | #' @param message Error message 14 | #' @param status_code HTTP status code 15 | #' @param response Full HTTP response object 16 | #' @param call The calling function (automatically detected) 17 | #' 18 | #' @return An error object of class \code{virustotal_error} 19 | #' @keywords internal 20 | #' @export 21 | #' @family error handling 22 | virustotal_error <- function(message, status_code = NULL, response = NULL, call = sys.call(-1)) { 23 | structure( 24 | list( 25 | message = message, 26 | status_code = status_code, 27 | response = response, 28 | call = call 29 | ), 30 | class = c("virustotal_error", "error", "condition") 31 | ) 32 | } 33 | 34 | #' Create a rate limit error 35 | #' 36 | #' @param message Error message 37 | #' @param retry_after Number of seconds to wait before retry 38 | #' @param call The calling function (automatically detected) 39 | #' 40 | #' @return An error object of class \code{virustotal_rate_limit_error} 41 | #' @keywords internal 42 | #' @export 43 | #' @family error handling 44 | virustotal_rate_limit_error <- function(message = "Rate limit exceeded", 45 | retry_after = 60, 46 | call = sys.call(-1)) { 47 | structure( 48 | list( 49 | message = message, 50 | retry_after = retry_after, 51 | call = call 52 | ), 53 | class = c("virustotal_rate_limit_error", "virustotal_error", "error", "condition") 54 | ) 55 | } 56 | 57 | #' Create an authentication error 58 | #' 59 | #' @param message Error message 60 | #' @param call The calling function (automatically detected) 61 | #' 62 | #' @return An error object of class \code{virustotal_auth_error} 63 | #' @keywords internal 64 | #' @export 65 | #' @family error handling 66 | virustotal_auth_error <- function(message = "Invalid or missing API key", 67 | call = sys.call(-1)) { 68 | structure( 69 | list( 70 | message = message, 71 | call = call 72 | ), 73 | class = c("virustotal_auth_error", "virustotal_error", "error", "condition") 74 | ) 75 | } 76 | 77 | #' Create a validation error 78 | #' 79 | #' @param message Error message 80 | #' @param parameter The parameter that failed validation 81 | #' @param value The invalid value 82 | #' @param call The calling function (automatically detected) 83 | #' 84 | #' @return An error object of class \code{virustotal_validation_error} 85 | #' @keywords internal 86 | #' @export 87 | #' @family error handling 88 | virustotal_validation_error <- function(message, parameter = NULL, value = NULL, 89 | call = sys.call(-1)) { 90 | structure( 91 | list( 92 | message = message, 93 | parameter = parameter, 94 | value = value, 95 | call = call 96 | ), 97 | class = c("virustotal_validation_error", "virustotal_error", "error", "condition") 98 | ) 99 | } 100 | 101 | #' Print method for VirusTotal errors 102 | #' 103 | #' @param x A virustotal_error object 104 | #' @param ... Additional arguments (unused) 105 | #' @keywords internal 106 | print.virustotal_error <- function(x, ...) { 107 | cat("VirusTotal API Error: ", x$message, "\n", sep = "") 108 | if (!is.null(x$status_code)) { 109 | cat("HTTP Status Code: ", x$status_code, "\n", sep = "") 110 | } 111 | if (inherits(x, "virustotal_rate_limit_error") && !is.null(x$retry_after)) { 112 | cat("Retry after: ", x$retry_after, " seconds\n", sep = "") 113 | } 114 | if (!is.null(x$parameter)) { 115 | cat("Parameter: ", x$parameter, "\n", sep = "") 116 | } 117 | invisible(x) 118 | } 119 | -------------------------------------------------------------------------------- /R/virustotal.R: -------------------------------------------------------------------------------- 1 | #' @title virustotal: Access Virustotal API 2 | #' 3 | #' @description Access virustotal API. See \url{https://www.virustotal.com/}. 4 | #' Details about results of calls to the API can be found at \url{https://docs.virustotal.com/reference}. 5 | #' 6 | #' You will need credentials to use this application. 7 | #' If you haven't already, get the API Key at \url{https://www.virustotal.com/}. 8 | #' 9 | #' 10 | #' @importFrom httr GET content POST upload_file add_headers headers parse_url 11 | #' @importFrom dplyr bind_rows 12 | #' @importFrom utils read.table packageDescription URLencode 13 | #' @importFrom jsonlite fromJSON toJSON 14 | #' @importFrom checkmate assert_character assert_file_exists assert_numeric 15 | #' @importFrom rlang .data 16 | #' @importFrom base64enc base64encode 17 | #' @importFrom tools toTitleCase 18 | #' @author Gaurav Sood 19 | "_PACKAGE" 20 | 21 | #' 22 | #' Base POST AND GET functions. Not exported. 23 | 24 | #' 25 | #' GET for the Current V3 API 26 | #' 27 | #' @param path path to the specific API service url 28 | #' @param query query list 29 | #' @param key A character string containing Virustotal API Key. The default is retrieved from \code{Sys.getenv("VirustotalToken")}. 30 | #' @param \dots Additional arguments passed to \code{\link[httr]{GET}}. 31 | #' @return list 32 | #' @keywords internal 33 | 34 | virustotal_GET <- function(path, query = list(), 35 | key = Sys.getenv("VirustotalToken"), ...) { 36 | 37 | if (identical(key, "")) { 38 | stop("Please set application key via set_key(key='key')).\n") 39 | } 40 | 41 | rate_limit() 42 | 43 | res <- GET("https://www.virustotal.com/", 44 | path = paste0("api/v3/", path), 45 | query = query, 46 | add_headers('x-apikey' = key), ...) 47 | 48 | virustotal_check(res) 49 | res <- content(res, as = "parsed", type = "application/json") 50 | 51 | res 52 | } 53 | 54 | 55 | #' 56 | #' POST for the Current V3 API 57 | #' 58 | #' @param path path to the specific API service url 59 | #' @param body request body (file upload or JSON data) 60 | #' @param query query list 61 | #' @param key A character string containing Virustotal API Key. The default is retrieved from \code{Sys.getenv("VirustotalToken")}. 62 | #' @param \dots Additional arguments passed to \code{\link[httr]{POST}}. 63 | #' @return list 64 | #' @keywords internal 65 | 66 | virustotal_POST <- function(path, body = NULL, query = list(), 67 | key = Sys.getenv("VirustotalToken"), ...) { 68 | 69 | if (identical(key, "")) { 70 | stop("Please set application key via set_key(key='key')).\n") 71 | } 72 | 73 | rate_limit() 74 | 75 | res <- POST("https://www.virustotal.com/", 76 | path = paste0("api/v3/", path), 77 | body = body, 78 | encode = "json", 79 | query = query, 80 | add_headers('x-apikey' = key), ...) 81 | 82 | virustotal_check(res) 83 | res <- content(res, as = "parsed", type = "application/json") 84 | 85 | res 86 | } 87 | 88 | 89 | #' Request Response Verification 90 | #' 91 | #' Enhanced error checking with structured error classes 92 | #' 93 | #' @param req HTTP response object from httr 94 | #' @return Invisible NULL on success, throws structured errors on failure 95 | #' @family error handling 96 | #' @keywords internal 97 | 98 | virustotal_check <- function(req) { 99 | # Rate limit errors (check before general success cases) 100 | if (req$status_code == 204 || req$status_code == 429) { 101 | # Try to get retry-after header, with fallback to 60 if anything fails 102 | retry_after <- tryCatch({ 103 | # Check if this is a real httr response or a mock object 104 | if (inherits(req, "response")) { 105 | as.numeric(headers(req)[["retry-after"]]) %||% 60 106 | } else { 107 | # For test mock objects, use a default 108 | 60 109 | } 110 | }, error = function(e) { 111 | 60 # Default fallback for any error 112 | }) 113 | 114 | stop(virustotal_rate_limit_error( 115 | message = "Rate limit exceeded. Only 4 requests per minute allowed.", 116 | retry_after = retry_after 117 | )) 118 | } 119 | 120 | # Success cases (after rate limit check) 121 | if (req$status_code < 400) return(invisible()) 122 | 123 | # Authentication errors 124 | if (req$status_code == 401 || req$status_code == 403) { 125 | stop(virustotal_auth_error( 126 | message = "Authentication failed. Please check your API key." 127 | )) 128 | } 129 | 130 | # Not found errors 131 | if (req$status_code == 404) { 132 | stop(virustotal_error( 133 | message = "Resource not found.", 134 | status_code = req$status_code, 135 | response = req 136 | )) 137 | } 138 | 139 | # Server errors 140 | if (req$status_code >= 500) { 141 | stop(virustotal_error( 142 | message = paste("VirusTotal server error:", req$status_code), 143 | status_code = req$status_code, 144 | response = req 145 | )) 146 | } 147 | 148 | # Generic client errors 149 | stop(virustotal_error( 150 | message = paste("HTTP request failed with status", req$status_code), 151 | status_code = req$status_code, 152 | response = req 153 | )) 154 | } 155 | 156 | # Helper operator for default values 157 | `%||%` <- function(x, y) if (is.null(x)) y else x 158 | 159 | # The rate limiting function is now implemented in rate_limiting.R 160 | -------------------------------------------------------------------------------- /R/rate_limiting.R: -------------------------------------------------------------------------------- 1 | #' Rate Limiting for VirusTotal API 2 | #' 3 | #' @description 4 | #' Modern rate limiting implementation that properly manages API request limits. 5 | #' VirusTotal public API allows 4 requests per minute. 6 | #' 7 | #' @name rate-limiting 8 | #' @keywords internal 9 | #' @family rate limiting 10 | NULL 11 | 12 | # Package-level environment for rate limiting state 13 | .virustotal_state <- new.env(parent = emptyenv()) 14 | 15 | #' Initialize rate limiting state 16 | #' 17 | #' @keywords internal 18 | init_rate_limit <- function() { 19 | .virustotal_state$requests <- numeric(0) 20 | .virustotal_state$window_size <- 60 # 60 seconds 21 | .virustotal_state$max_requests <- 4 22 | .virustotal_state$initialized <- TRUE 23 | } 24 | 25 | #' Check if rate limiting is properly initialized 26 | #' 27 | #' @keywords internal 28 | is_rate_limit_initialized <- function() { 29 | !is.null(.virustotal_state$initialized) && 30 | !is.null(.virustotal_state$requests) && 31 | !is.null(.virustotal_state$window_size) && 32 | !is.null(.virustotal_state$max_requests) 33 | } 34 | 35 | #' Modern rate limiting implementation 36 | #' 37 | #' Uses a sliding window approach to track requests and enforce limits. 38 | #' This replaces the old environment variable-based approach. 39 | #' 40 | #' @param force_wait Logical. If TRUE, will wait even if under limit 41 | #' @return Invisible TRUE 42 | #' @keywords internal 43 | #' @family rate limiting 44 | rate_limit <- function(force_wait = FALSE) { 45 | # Initialize if needed - check all components 46 | if (!is_rate_limit_initialized()) { 47 | init_rate_limit() 48 | } 49 | 50 | current_time <- as.numeric(Sys.time()) 51 | window_size <- .virustotal_state$window_size 52 | if (is.null(window_size)) window_size <- 60 # fallback 53 | 54 | window_start <- current_time - window_size 55 | 56 | # Clean old requests outside the window - handle empty case 57 | requests <- .virustotal_state$requests 58 | if (is.null(requests)) requests <- numeric(0) 59 | 60 | if (length(requests) > 0) { 61 | active_requests <- requests[requests > window_start] 62 | } else { 63 | active_requests <- numeric(0) 64 | } 65 | .virustotal_state$requests <- active_requests 66 | 67 | # Check if we're at the limit - add NULL safety 68 | max_requests <- .virustotal_state$max_requests 69 | if (is.null(max_requests)) max_requests <- 4 # fallback 70 | 71 | if (length(.virustotal_state$requests) >= max_requests || force_wait) { 72 | if (length(.virustotal_state$requests) > 0) { 73 | # Calculate wait time until oldest request expires - use safe variables 74 | oldest_request <- min(active_requests) 75 | wait_time <- max(0, window_size - (current_time - oldest_request)) 76 | 77 | if (wait_time > 0) { 78 | message(sprintf("Rate limit reached. Waiting %.1f seconds...", wait_time)) 79 | Sys.sleep(wait_time + 0.1) # Add small buffer 80 | 81 | # Update current time and clean requests again 82 | current_time <- as.numeric(Sys.time()) 83 | window_start <- current_time - window_size 84 | 85 | # Re-clean with defensive programming 86 | requests_after_wait <- .virustotal_state$requests 87 | if (!is.null(requests_after_wait) && length(requests_after_wait) > 0) { 88 | active_requests_after_wait <- requests_after_wait[requests_after_wait > window_start] 89 | } else { 90 | active_requests_after_wait <- numeric(0) 91 | } 92 | .virustotal_state$requests <- active_requests_after_wait 93 | } 94 | } 95 | } 96 | 97 | # Record this request 98 | .virustotal_state$requests <- c(.virustotal_state$requests, current_time) 99 | 100 | invisible(TRUE) 101 | } 102 | 103 | #' Get current rate limit status 104 | #' 105 | #' @return List with current status information 106 | #' @keywords internal 107 | #' @family rate limiting 108 | get_rate_limit_status <- function() { 109 | # Ensure complete initialization 110 | if (!is_rate_limit_initialized()) { 111 | init_rate_limit() 112 | } 113 | 114 | current_time <- as.numeric(Sys.time()) 115 | 116 | # Get values with fallbacks 117 | window_size <- .virustotal_state$window_size 118 | if (is.null(window_size)) window_size <- 60 119 | 120 | max_requests <- .virustotal_state$max_requests 121 | if (is.null(max_requests)) max_requests <- 4 122 | 123 | requests <- .virustotal_state$requests 124 | if (is.null(requests)) requests <- numeric(0) 125 | 126 | window_start <- current_time - window_size 127 | 128 | # Clean old requests - handle empty case 129 | if (length(requests) > 0) { 130 | active_requests <- requests[requests > window_start] 131 | } else { 132 | active_requests <- numeric(0) 133 | } 134 | 135 | list( 136 | requests_used = length(active_requests), 137 | max_requests = max_requests, 138 | window_size = window_size, 139 | requests_remaining = max_requests - length(active_requests), 140 | window_reset_time = if (length(active_requests) > 0) { 141 | min(active_requests) + window_size 142 | } else { 143 | current_time 144 | } 145 | ) 146 | } 147 | 148 | #' Reset rate limiting state 149 | #' 150 | #' Clears all rate limiting history. Useful for testing. 151 | #' 152 | #' @keywords internal 153 | #' @family rate limiting 154 | reset_rate_limit <- function() { 155 | init_rate_limit() 156 | message("Rate limiting state reset.") 157 | invisible(TRUE) 158 | } 159 | -------------------------------------------------------------------------------- /tests/testthat/test-file-operations.R: -------------------------------------------------------------------------------- 1 | # File Operations Tests 2 | 3 | # Mock response data for file operations 4 | file_scan_response <- list( 5 | data = list( 6 | type = "analysis", 7 | id = "mock_analysis_id_123" 8 | ) 9 | ) 10 | 11 | file_report_response <- list( 12 | data = list( 13 | type = "file", 14 | id = "mock_file_id", 15 | attributes = list( 16 | last_analysis_results = list( 17 | "Antivirus1" = list(category = "undetected"), 18 | "Antivirus2" = list(category = "malicious") 19 | ), 20 | total_votes = list( 21 | harmless = 50, 22 | malicious = 2 23 | ) 24 | ) 25 | ) 26 | ) 27 | 28 | test_that("scan_file validates input correctly", { 29 | expect_error(scan_file(), "argument \"file_path\" is missing") 30 | expect_error(scan_file(NULL), "Must be of type 'character'") 31 | expect_error(scan_file(123), "Must be of type 'character'") 32 | expect_error(scan_file(character(0)), "Must have length 1") 33 | expect_error(scan_file("nonexistent_file.txt"), "File does not exist") 34 | 35 | # Test file size validation with temporary large file (if we create one) 36 | # This would need a very large temp file to test the 650MB limit 37 | }) 38 | 39 | test_that("file_report validates input correctly", { 40 | expect_error(file_report(), class = "virustotal_validation_error") 41 | expect_error(file_report(NULL), class = "virustotal_validation_error") 42 | expect_error(file_report(123), class = "virustotal_validation_error") 43 | expect_error(file_report(""), class = "virustotal_validation_error") 44 | 45 | # Test with invalid hash format (but valid API key unset for auth error) 46 | old_key <- Sys.getenv("VirustotalToken") 47 | Sys.unsetenv("VirustotalToken") 48 | expect_error(file_report("dummy_hash"), class = "virustotal_auth_error") 49 | 50 | # Restore API key 51 | if (old_key != "") { 52 | Sys.setenv(VirustotalToken = old_key) 53 | } 54 | }) 55 | 56 | test_that("rescan_file validates input correctly", { 57 | expect_error(rescan_file(), "Assertion on 'hash' failed") 58 | expect_error(rescan_file(NULL), "Assertion on 'hash' failed") 59 | expect_error(rescan_file(123), "Assertion on 'hash' failed") 60 | expect_error(rescan_file(""), "All elements must have at least 1 characters") 61 | }) 62 | 63 | # Test new v3 file functions 64 | test_that("get_file_upload_url validates correctly", { 65 | expect_true(exists("get_file_upload_url")) 66 | }) 67 | 68 | test_that("get_file_comments validates input correctly", { 69 | expect_error(get_file_comments(), "Assertion on 'hash' failed") 70 | expect_error(get_file_comments(NULL), "Assertion on 'hash' failed") 71 | expect_error(get_file_comments(""), "All elements must have at least 1 characters") 72 | }) 73 | 74 | test_that("post_file_comments validates input correctly", { 75 | expect_error(post_file_comments(), "Assertion on 'hash' failed") 76 | expect_error(post_file_comments("hash123"), "Assertion on 'comment' failed") 77 | expect_error(post_file_comments("hash123", ""), "All elements must have at least 1 characters") 78 | }) 79 | 80 | test_that("get_file_votes validates input correctly", { 81 | expect_error(get_file_votes(), "Assertion on 'hash' failed") 82 | expect_error(get_file_votes(NULL), "Assertion on 'hash' failed") 83 | expect_error(get_file_votes(""), "All elements must have at least 1 characters") 84 | }) 85 | 86 | test_that("post_file_votes validates input correctly", { 87 | expect_error(post_file_votes(), "Assertion on 'hash' failed") 88 | expect_error(post_file_votes("hash123"), "Assertion on 'verdict' failed") 89 | expect_error(post_file_votes("hash123", "invalid"), "Verdict must be either 'harmless' or 'malicious'") 90 | }) 91 | 92 | test_that("get_file_relationships validates input correctly", { 93 | expect_error(get_file_relationships(), "Assertion on 'hash' failed") 94 | expect_error(get_file_relationships("hash123"), "Assertion on 'relationship' failed") 95 | expect_error(get_file_relationships("hash123", "invalid"), "Invalid relationship type") 96 | }) 97 | 98 | test_that("download_file validates input correctly", { 99 | expect_error(download_file(), "Assertion on 'hash' failed") 100 | expect_error(download_file(NULL), "Assertion on 'hash' failed") 101 | expect_error(download_file(""), "All elements must have at least 1 characters") 102 | }) 103 | 104 | test_that("get_file_download_url validates input correctly", { 105 | expect_error(get_file_download_url(), "Assertion on 'hash' failed") 106 | expect_error(get_file_download_url(NULL), "Assertion on 'hash' failed") 107 | expect_error(get_file_download_url(""), "All elements must have at least 1 characters") 108 | }) 109 | 110 | # Mock tests require httptest package and API key setup 111 | test_that("file operations work with mocked responses", { 112 | skip_if_not_installed("httptest") 113 | skip_if(Sys.getenv("VirustotalToken") == "", "API key not set") 114 | 115 | # These would use httptest::with_mock_api() in practice 116 | # For now, just verify the functions exist and are callable 117 | expect_true(exists("scan_file")) 118 | expect_true(exists("file_report")) 119 | expect_true(exists("rescan_file")) 120 | expect_true(exists("get_file_upload_url")) 121 | expect_true(exists("get_file_comments")) 122 | expect_true(exists("post_file_comments")) 123 | expect_true(exists("get_file_votes")) 124 | expect_true(exists("post_file_votes")) 125 | expect_true(exists("get_file_relationships")) 126 | expect_true(exists("download_file")) 127 | expect_true(exists("get_file_download_url")) 128 | }) 129 | -------------------------------------------------------------------------------- /R/s3_classes.R: -------------------------------------------------------------------------------- 1 | #' S3 Classes for VirusTotal Responses 2 | #' 3 | #' @description 4 | #' S3 classes to provide structured responses and better user experience 5 | #' when working with VirusTotal API results. 6 | #' 7 | #' @name virustotal-classes 8 | #' @keywords internal 9 | #' @family response classes 10 | NULL 11 | 12 | #' Create a VirusTotal file scan result 13 | #' 14 | #' @param data Raw API response data 15 | #' @return Object of class \code{virustotal_file_scan} 16 | #' @keywords internal 17 | #' @export 18 | #' @family response classes 19 | virustotal_file_scan <- function(data) { 20 | structure( 21 | data, 22 | class = c("virustotal_file_scan", "virustotal_response", "list") 23 | ) 24 | } 25 | 26 | #' Create a VirusTotal file report 27 | #' 28 | #' @param data Raw API response data 29 | #' @return Object of class \code{virustotal_file_report} 30 | #' @keywords internal 31 | #' @export 32 | #' @family response classes 33 | virustotal_file_report <- function(data) { 34 | structure( 35 | data, 36 | class = c("virustotal_file_report", "virustotal_response", "list") 37 | ) 38 | } 39 | 40 | #' Create a VirusTotal URL scan result 41 | #' 42 | #' @param data Raw API response data 43 | #' @return Object of class \code{virustotal_url_scan} 44 | #' @keywords internal 45 | #' @export 46 | #' @family response classes 47 | virustotal_url_scan <- function(data) { 48 | structure( 49 | data, 50 | class = c("virustotal_url_scan", "virustotal_response", "list") 51 | ) 52 | } 53 | 54 | #' Create a VirusTotal domain report 55 | #' 56 | #' @param data Raw API response data 57 | #' @return Object of class \code{virustotal_domain_report} 58 | #' @keywords internal 59 | #' @export 60 | #' @family response classes 61 | virustotal_domain_report <- function(data) { 62 | structure( 63 | data, 64 | class = c("virustotal_domain_report", "virustotal_response", "list") 65 | ) 66 | } 67 | 68 | #' Create a VirusTotal IP report 69 | #' 70 | #' @param data Raw API response data 71 | #' @return Object of class \code{virustotal_ip_report} 72 | #' @keywords internal 73 | #' @export 74 | #' @family response classes 75 | virustotal_ip_report <- function(data) { 76 | structure( 77 | data, 78 | class = c("virustotal_ip_report", "virustotal_response", "list") 79 | ) 80 | } 81 | 82 | #' Print method for VirusTotal responses 83 | #' 84 | #' @param x A virustotal_response object 85 | #' @param ... Additional arguments (unused) 86 | #' @keywords internal 87 | print.virustotal_response <- function(x, ...) { 88 | cat("VirusTotal API Response\n") 89 | cat("======================\n\n") 90 | 91 | # Get the specific class 92 | specific_class <- class(x)[1] 93 | type <- gsub("virustotal_", "", specific_class) 94 | type <- gsub("_", " ", type) 95 | type <- toTitleCase(type) 96 | 97 | cat("Type:", type, "\n") 98 | 99 | if (!is.null(x$data$id)) { 100 | cat("ID:", x$data$id, "\n") 101 | } 102 | 103 | if (!is.null(x$data$type)) { 104 | cat("Resource Type:", x$data$type, "\n") 105 | } 106 | 107 | cat("\n") 108 | invisible(x) 109 | } 110 | 111 | #' Print method for file reports 112 | #' 113 | #' @param x A virustotal_file_report object 114 | #' @param ... Additional arguments (unused) 115 | #' @keywords internal 116 | print.virustotal_file_report <- function(x, ...) { 117 | NextMethod() 118 | 119 | if (!is.null(x$data$attributes)) { 120 | attrs <- x$data$attributes 121 | 122 | # Detection summary 123 | if (!is.null(attrs$last_analysis_stats)) { 124 | stats <- attrs$last_analysis_stats 125 | cat("Detection Summary:\n") 126 | cat(sprintf(" Malicious: %d\n", stats$malicious %||% 0)) 127 | cat(sprintf(" Suspicious: %d\n", stats$suspicious %||% 0)) 128 | cat(sprintf(" Undetected: %d\n", stats$undetected %||% 0)) 129 | cat(sprintf(" Harmless: %d\n", stats$harmless %||% 0)) 130 | cat("\n") 131 | } 132 | 133 | # File info 134 | if (!is.null(attrs$size)) { 135 | cat(sprintf("File Size: %s bytes\n", format(attrs$size, big.mark = ","))) 136 | } 137 | 138 | if (!is.null(attrs$sha256)) { 139 | cat(sprintf("SHA256: %s\n", attrs$sha256)) 140 | } 141 | 142 | cat("\n") 143 | } 144 | 145 | invisible(x) 146 | } 147 | 148 | #' Print method for domain reports 149 | #' 150 | #' @param x A virustotal_domain_report object 151 | #' @param ... Additional arguments (unused) 152 | #' @keywords internal 153 | print.virustotal_domain_report <- function(x, ...) { 154 | NextMethod() 155 | 156 | if (!is.null(x$data$attributes)) { 157 | attrs <- x$data$attributes 158 | 159 | # Domain reputation 160 | if (!is.null(attrs$last_analysis_stats)) { 161 | stats <- attrs$last_analysis_stats 162 | cat("Domain Reputation:\n") 163 | cat(sprintf(" Malicious: %d\n", stats$malicious %||% 0)) 164 | cat(sprintf(" Suspicious: %d\n", stats$suspicious %||% 0)) 165 | cat(sprintf(" Undetected: %d\n", stats$undetected %||% 0)) 166 | cat(sprintf(" Harmless: %d\n", stats$harmless %||% 0)) 167 | cat("\n") 168 | } 169 | 170 | # Categories 171 | if (!is.null(attrs$categories)) { 172 | cats <- names(attrs$categories) 173 | if (length(cats) > 0) { 174 | cat("Categories:", paste(cats, collapse = ", "), "\n") 175 | } 176 | } 177 | 178 | cat("\n") 179 | } 180 | 181 | invisible(x) 182 | } 183 | 184 | #' Summary method for VirusTotal responses 185 | #' 186 | #' @param object A virustotal_response object 187 | #' @param ... Additional arguments (unused) 188 | #' @keywords internal 189 | summary.virustotal_response <- function(object, ...) { 190 | print(object) 191 | 192 | if (inherits(object, "virustotal_file_report") && !is.null(object$data$attributes$last_analysis_results)) { 193 | results <- object$data$attributes$last_analysis_results 194 | 195 | # Show top detections 196 | detections <- sapply(results, function(x) x$category %||% "undetected") 197 | malicious <- names(detections[detections == "malicious"]) 198 | 199 | if (length(malicious) > 0) { 200 | cat("Engines detecting as malicious:\n") 201 | cat(paste(" -", malicious[1:min(10, length(malicious))]), sep = "\n") 202 | if (length(malicious) > 10) { 203 | cat(sprintf(" ... and %d more\n", length(malicious) - 10)) 204 | } 205 | cat("\n") 206 | } 207 | } 208 | 209 | invisible(object) 210 | } 211 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # virustotal 0.6.0 2 | 3 | ## Breaking Changes 4 | 5 | * **Removed VirusTotal API v2.0 Support**: Completely removed all v2.0 API functions (`virustotal2_GET()`, `virustotal2_POST()`) and deprecated functions (`add_comments()`). The package now exclusively uses VirusTotal API v3.0, aligning with VirusTotal's recommendation to migrate to v3.0 for enhanced functionality and data richness. 6 | 7 | ## Package Simplification 8 | 9 | * **Streamlined Architecture**: Simplified codebase by removing dual API support, reducing complexity and maintenance overhead. 10 | * **Enhanced Documentation**: Updated all documentation, README, and package description to reflect v3.0-only support with clearer feature descriptions including IoC relationships and sandbox analysis. 11 | 12 | ## Migration Guide 13 | 14 | Users upgrading from versions that used v2.0 functions should ensure their code uses the equivalent v3.0 functions. All core functionality remains available through the modernized v3.0 API endpoints. 15 | 16 | --- 17 | 18 | # virustotal 0.5.0 19 | 20 | ## Major Updates 21 | 22 | * **Modernized Package Architecture**: Complete modernization of the virustotal package with enhanced security, error handling, and user experience. 23 | 24 | ### New Features 25 | 26 | * **Structured Error Handling**: New S3 error classes (`virustotal_error`, `virustotal_auth_error`, `virustotal_validation_error`, `virustotal_rate_limit_error`) provide detailed error information and better debugging. 27 | 28 | * **S3 Response Classes**: All API responses now return structured S3 objects (`virustotal_file_report`, `virustotal_domain_report`, etc.) with custom `print()` and `summary()` methods for better user experience. 29 | 30 | * **Modern Rate Limiting**: Replaced environment variable-based rate limiting with a sliding window implementation that properly manages the 4 requests/minute VirusTotal API limit. 31 | 32 | * **Comprehensive Input Validation**: Added robust input validation using the `checkmate` package with security-focused sanitization functions. 33 | 34 | * **Enhanced Security Utilities**: New security functions for safe file operations and input sanitization to prevent common security issues. 35 | 36 | ### Infrastructure Improvements 37 | 38 | * **Updated CI/CD**: Migrated from Travis CI/AppVeyor to GitHub Actions with comprehensive testing matrix (R oldrel-1, release, devel). 39 | 40 | * **Modern Dependencies**: Updated minimum R version to 4.0.0, migrated from `plyr` to `dplyr`, added modern packages (`checkmate`, `jsonlite`, `rlang`). 41 | 42 | * **Enhanced Documentation**: Improved documentation with roxygen2 markdown support and comprehensive examples. 43 | 44 | * **Test Coverage**: Expanded test suite with proper mocking support and comprehensive error handling validation. 45 | 46 | ### API Enhancements 47 | 48 | * **Improved Domain Processing**: Enhanced domain cleaning logic that properly handles URLs with protocols, www prefixes, and paths. 49 | 50 | * **Better Error Messages**: More informative error messages with parameter context and suggested fixes. 51 | 52 | * **Response Formatting**: Rich response formatting with detection summaries, file metadata, and threat intelligence display. 53 | 54 | ### Breaking Changes 55 | 56 | * Minimum R version increased from 3.3.0 to 4.0.0 57 | * Some internal functions have been refactored (not user-facing) 58 | * Error objects now use structured S3 classes instead of simple character strings 59 | 60 | ### Bug Fixes 61 | 62 | * Fixed rate limiting edge cases and timing issues 63 | * Improved handling of malformed API responses 64 | * Enhanced validation precedence for better test compatibility 65 | * Fixed Unicode character encoding in utility functions 66 | 67 | ### Development Tools 68 | 69 | * Added `virustotal_info()` function for package configuration diagnostics 70 | * Enhanced rate limit status reporting with `get_rate_limit_status()` 71 | * Improved temporary file management with security-focused utilities 72 | 73 | --- 74 | 75 | # virustotal 0.3.0 76 | 77 | ## Major Changes 78 | * **BREAKING**: Migrated all core functions to VirusTotal API v3 79 | * **BREAKING**: Function return types changed from data.frame to list (following v3 API structure) 80 | * Updated all functions: `file_report()`, `scan_file()`, `rescan_file()`, `url_report()`, `scan_url()`, `domain_report()`, `ip_report()` 81 | * Removed deprecated `virustotal2_*` function calls from user-facing functions 82 | 83 | ## New Features 84 | * Enhanced input validation for all functions 85 | * Automatic URL encoding for v3 API compatibility 86 | * Improved error messages with actionable guidance 87 | * Support for IPv6 addresses in `ip_report()` 88 | * Domain name normalization (removes protocols, www, paths) 89 | 90 | ## Testing & Quality 91 | * Comprehensive test suite with 47+ tests 92 | * Added input validation tests for all core functions 93 | * Proper error handling tests 94 | * GitHub Actions CI/CD pipeline replacing AppVeyor 95 | * Multi-platform testing (Ubuntu, Windows, macOS) 96 | * Automated test coverage reporting 97 | 98 | ## Documentation 99 | * Updated all function documentation for v3 API 100 | * Comprehensive vignette rewrite with modern examples 101 | * Updated references to point to current VirusTotal documentation 102 | * Added usage examples for all major functions 103 | 104 | ## Dependencies 105 | * Added `base64enc` for URL encoding support 106 | * Updated imports and suggests for modern R ecosystem 107 | 108 | # virustotal 0.2.2 109 | 110 | * support for domain and ip v3 111 | * deprecate v2 domain and ip functions 112 | 113 | # virustotal 0.2.1 114 | 115 | * extensive linting, passes expect_no_lint 116 | * url_report now returns service name 117 | 118 | # virustotal 0.2.0 119 | 120 | * Removed link to bitdefender because CRAN was having issues 121 | * Better documentation with examples including comment for set_key, better formatting 122 | * Better error handling and more consistent returned data structures for url_report, file_report, rescan_file 123 | * url_report now accepts scan_id as a param 124 | * Warning messages end with new line 125 | * Added more tests, specifically checking returns to what happens when params/hash are incorrect 126 | * Enforces rate limiting --- 4 queries per minute. 127 | * Graceful error handling if error limit exceeded. 128 | * changed virustotal to VirusTotal as CRAN doesn't muck around. 129 | 130 | # virustotal 0.1.0 131 | 132 | * Initial release -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Utility Functions for VirusTotal Package 2 | #' 3 | #' @description 4 | #' Helper functions and utilities for the VirusTotal package. 5 | #' 6 | #' @name utilities 7 | #' @keywords internal 8 | #' @family utilities 9 | NULL 10 | 11 | #' Simple input validation 12 | #' 13 | #' Basic input validation and sanitization for VirusTotal API calls. 14 | #' Replaces over-engineered security functions with simpler checks. 15 | #' 16 | #' @param input Character string to validate 17 | #' @return Cleaned input string 18 | #' @keywords internal 19 | validate_input <- function(input) { 20 | # Use checkmate for consistent validation 21 | tryCatch({ 22 | assert_character(input, len = 1, any.missing = FALSE, min.chars = 1) 23 | }, error = function(e) { 24 | stop(virustotal_validation_error( 25 | message = "Input must be a single non-empty character string", 26 | parameter = "input", 27 | value = input 28 | )) 29 | }) 30 | 31 | # Basic cleanup - remove leading/trailing whitespace 32 | input <- trimws(input) 33 | 34 | return(input) 35 | } 36 | 37 | #' Check if running in a safe environment 38 | #' 39 | #' Verifies that the package is running in an appropriate environment 40 | #' for security analysis work. 41 | #' 42 | #' @return Logical indicating if environment is considered safe 43 | #' @keywords internal 44 | #' @family utilities 45 | is_safe_environment <- function() { 46 | # Check if we're in an interactive session 47 | if (!interactive()) { 48 | warning("Running in non-interactive mode. Be cautious with file operations.") 49 | } 50 | 51 | # Check for common CI environments where we should be extra careful 52 | ci_vars <- c("CI", "GITHUB_ACTIONS", "TRAVIS", "APPVEYOR", "GITLAB_CI") 53 | if (any(Sys.getenv(ci_vars) != "")) { 54 | message("Detected CI environment. Some functions may be disabled for security.") 55 | return(FALSE) 56 | } 57 | 58 | return(TRUE) 59 | } 60 | 61 | #' Convert file size to human readable format 62 | #' 63 | #' @param size_bytes File size in bytes 64 | #' @return Character string with human-readable size 65 | #' @keywords internal 66 | #' @family utilities 67 | format_file_size <- function(size_bytes) { 68 | assert_numeric(size_bytes, len = 1, lower = 0) 69 | 70 | units <- c("B", "KB", "MB", "GB", "TB") 71 | unit_index <- 1 72 | size <- size_bytes 73 | 74 | while (size >= 1024 && unit_index < length(units)) { 75 | size <- size / 1024 76 | unit_index <- unit_index + 1 77 | } 78 | 79 | sprintf("%.2f %s", size, units[unit_index]) 80 | } 81 | 82 | #' Validate VirusTotal response structure 83 | #' 84 | #' Checks if a response from VirusTotal API has the expected structure. 85 | #' 86 | #' @param response Response object from VirusTotal API 87 | #' @return Logical indicating if response structure is valid 88 | #' @keywords internal 89 | #' @family utilities 90 | validate_vt_response <- function(response) { 91 | if (!is.list(response)) { 92 | return(FALSE) 93 | } 94 | 95 | # Check for common VirusTotal response fields 96 | if (is.null(response$data)) { 97 | return(FALSE) 98 | } 99 | 100 | # Basic structure validation 101 | if (!is.list(response$data)) { 102 | return(FALSE) 103 | } 104 | 105 | return(TRUE) 106 | } 107 | 108 | #' Create a safe temporary directory for file operations 109 | #' 110 | #' Creates a temporary directory with restricted permissions for secure 111 | #' file operations during malware analysis. 112 | #' 113 | #' @return Path to the temporary directory 114 | #' @keywords internal 115 | #' @family utilities 116 | create_safe_temp_dir <- function() { 117 | temp_dir <- tempfile(pattern = "virustotal_") 118 | dir.create(temp_dir, mode = "0700") # Owner read/write/execute only 119 | 120 | # Set stricter permissions if on Unix-like system 121 | if (.Platform$OS.type == "unix") { 122 | Sys.chmod(temp_dir, mode = "0700") 123 | } 124 | 125 | message("Created secure temporary directory: ", temp_dir) 126 | return(temp_dir) 127 | } 128 | 129 | #' Clean up temporary files and directories 130 | #' 131 | #' Safely removes temporary files and directories created during 132 | #' VirusTotal operations. 133 | #' 134 | #' @param paths Character vector of file/directory paths to clean up 135 | #' @return Logical indicating success 136 | #' @keywords internal 137 | #' @family utilities 138 | cleanup_temp_files <- function(paths) { 139 | assert_character(paths, any.missing = FALSE) 140 | 141 | success <- TRUE 142 | for (path in paths) { 143 | if (file.exists(path)) { 144 | tryCatch({ 145 | if (file.info(path)$isdir) { 146 | unlink(path, recursive = TRUE, force = TRUE) 147 | } else { 148 | file.remove(path) 149 | } 150 | message("Cleaned up: ", path) 151 | }, error = function(e) { 152 | warning("Failed to clean up ", path, ": ", e$message) 153 | success <<- FALSE 154 | }) 155 | } 156 | } 157 | 158 | return(success) 159 | } 160 | 161 | #' Get package version information 162 | #' 163 | #' @return Character string with package version 164 | #' @export 165 | #' @family utilities 166 | virustotal_version <- function() { 167 | desc <- packageDescription("virustotal") 168 | paste0("virustotal ", desc$Version) 169 | } 170 | 171 | #' Print package information and configuration status 172 | #' 173 | #' @return Invisible NULL 174 | #' @export 175 | #' @family utilities 176 | virustotal_info <- function() { 177 | cat("VirusTotal R Package Information\n") 178 | cat("================================\n\n") 179 | 180 | # Version info 181 | cat("Version:", packageDescription("virustotal")$Version, "\n") 182 | cat("R Version:", R.version.string, "\n\n") 183 | 184 | # API key status 185 | cat("API Key Status: ") 186 | if (is_api_key_configured()) { 187 | cat("\u2713 Configured\n") 188 | } else { 189 | cat("\u2717 Not configured (use set_key())\n") 190 | } 191 | 192 | # Rate limiting status 193 | rate_status <- get_rate_limit_status() 194 | cat("Rate Limit Status:\n") 195 | cat(sprintf(" Requests used: %d/%d\n", 196 | rate_status$requests_used, rate_status$max_requests)) 197 | cat(sprintf(" Requests remaining: %d\n", rate_status$requests_remaining)) 198 | 199 | # Environment info 200 | cat("\nEnvironment: ") 201 | if (is_safe_environment()) { 202 | cat("\u2713 Safe\n") 203 | } else { 204 | cat("\u26A0 CI/Non-interactive\n") 205 | } 206 | 207 | cat("\nFor help, see: ?virustotal or https://docs.virustotal.com/reference\n") 208 | 209 | invisible(NULL) 210 | } 211 | --------------------------------------------------------------------------------