pub mod anti_tamper;
pub mod scan_auth;
pub use scan_auth::{DeniedModule, ModuleAuthorizeResponse, ScanAuthorization};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::OnceLock;
use tracing::{debug, error, info, warn};
const LICENSE_SERVER: &str = "https://lonkero.bountyy.fi/api/v1";
static KILLSWITCH_ACTIVE: AtomicBool = AtomicBool::new(false);
static KILLSWITCH_CHECKED: AtomicBool = AtomicBool::new(false);
static GLOBAL_LICENSE: OnceLock<LicenseStatus> = OnceLock::new();
static LAST_VALIDATION: AtomicU64 = AtomicU64::new(0);
static VALIDATION_TOKEN: AtomicU64 = AtomicU64::new(0);
static SCAN_COUNTER: AtomicU64 = AtomicU64::new(0);
const INTEGRITY_MARKER: u64 = 0x4C4F4E4B45524F;
#[inline(never)] pub fn verify_binary_integrity() -> bool {
if INTEGRITY_MARKER != 0x4C4F4E4B45524F {
error!("INTEGRITY VIOLATION: Marker tampered");
return false;
}
let token = VALIDATION_TOKEN.load(Ordering::SeqCst);
let checked = KILLSWITCH_CHECKED.load(Ordering::SeqCst);
let _marker_xor = get_integrity_marker();
if checked && token != 0 {
if token == INTEGRITY_MARKER {
error!("INTEGRITY VIOLATION: Token/marker collision");
return false;
}
}
let fn_ptr = verify_binary_integrity as *const ();
let fn_addr = fn_ptr as usize;
if fn_addr == 0 || fn_addr == usize::MAX {
error!("INTEGRITY VIOLATION: Function pointer invalid");
return false;
}
true
}
#[inline(never)]
pub fn verify_enforcement_integrity() -> bool {
let verify_scan_ptr = verify_scan_authorized as *const ();
let verify_rt_ptr = verify_rt_state as *const ();
let is_killswitch_ptr = is_killswitch_active as *const ();
let addrs = [
verify_scan_ptr as usize,
verify_rt_ptr as usize,
is_killswitch_ptr as usize,
];
for &addr in &addrs {
if addr == 0 || addr == usize::MAX {
error!("INTEGRITY VIOLATION: Enforcement function pointer invalid");
return false;
}
}
if addrs[0] == addrs[1] || addrs[1] == addrs[2] || addrs[0] == addrs[2] {
error!("INTEGRITY VIOLATION: Function pointers redirected");
return false;
}
true
}
fn generate_token(status: &LicenseStatus) -> u64 {
let mut hasher = Sha256::new();
hasher.update(format!("{:?}", status.license_type).as_bytes());
hasher.update(status.licensee.as_deref().unwrap_or("").as_bytes());
hasher.update(&status.max_targets.unwrap_or(0).to_le_bytes());
hasher.update(&INTEGRITY_MARKER.to_le_bytes());
let hash = hasher.finalize();
u64::from_le_bytes(hash[0..8].try_into().unwrap())
}
#[inline]
pub fn verify_rt_state() -> bool {
let token = VALIDATION_TOKEN.load(Ordering::SeqCst);
let checked = KILLSWITCH_CHECKED.load(Ordering::SeqCst);
(token != 0 || !checked) && !KILLSWITCH_ACTIVE.load(Ordering::SeqCst)
}
#[inline]
pub fn get_integrity_marker() -> u64 {
INTEGRITY_MARKER ^ VALIDATION_TOKEN.load(Ordering::SeqCst)
}
#[inline]
pub fn increment_scan_counter() -> u64 {
SCAN_COUNTER.fetch_add(1, Ordering::SeqCst)
}
#[inline]
pub fn get_scan_counter() -> u64 {
SCAN_COUNTER.load(Ordering::SeqCst)
}
#[inline(never)]
pub fn verify_scan_authorized() -> bool {
if KILLSWITCH_CHECKED.load(Ordering::SeqCst) {
let token = VALIDATION_TOKEN.load(Ordering::SeqCst);
if token == 0 {
return false;
}
}
if let Some(license) = get_global_license() {
if !license.valid || license.killswitch_active {
return false;
}
}
true
}
pub fn get_license_signature() -> String {
if let Some(license) = get_global_license() {
let mut hasher = Sha256::new();
hasher.update(format!("{:?}", license.license_type).as_bytes());
hasher.update(
license
.licensee
.as_deref()
.unwrap_or("unlicensed")
.as_bytes(),
);
hasher.update(&chrono::Utc::now().timestamp().to_le_bytes());
let hash = hasher.finalize();
format!("LKR-{}", hex::encode(&hash[0..8]))
} else {
"LKR-UNVALIDATED".to_string()
}
}
#[inline]
pub fn is_killswitch_active() -> bool {
KILLSWITCH_ACTIVE.load(Ordering::SeqCst)
}
const PREMIUM_FEATURES: &[&str] = &[
"cloud_scanning",
"api_fuzzing",
"container_scanning",
"ssti_advanced",
"team_sharing",
"custom_integrations",
"priority_support",
"rate_limiting_bypass",
"mfa_bypass_advanced",
"mass_assignment_advanced",
"client_route_auth_bypass",
"template_injection",
"session_analyzer",
"baseline_detector",
"information_disclosure",
"browser_extension",
];
#[inline(never)]
pub fn is_feature_available(feature: &str) -> bool {
let skip_aggressive_checks = std::env::var("LONKERO_DEV").is_ok()
|| std::env::var("CI").is_ok()
|| true;
if !skip_aggressive_checks {
if anti_tamper::was_tampered() {
return false;
}
let check_count = SCAN_COUNTER.fetch_add(1, Ordering::SeqCst);
if check_count % 100 == 0 {
if !anti_tamper::full_integrity_check() {
return false;
}
}
if !anti_tamper::verify_no_hook(is_feature_available as *const ()) {
return false;
}
}
if !PREMIUM_FEATURES.contains(&feature) {
return true;
}
if !skip_aggressive_checks {
if !anti_tamper::is_validated() {
let token = VALIDATION_TOKEN.load(Ordering::SeqCst);
if token == 0 && KILLSWITCH_CHECKED.load(Ordering::SeqCst) {
return false;
}
}
}
if let Some(license) = get_global_license() {
if let Some(license_type) = license.license_type {
match license_type {
LicenseType::Enterprise | LicenseType::Team => {
return true;
}
LicenseType::Professional => {
let enterprise_only =
&["team_sharing", "custom_integrations", "dedicated_support"];
if enterprise_only.contains(&feature) {
return false;
}
return true;
}
LicenseType::Personal => {
if feature == "browser_extension" {
let is_paid = license.features.iter().any(|f| {
f == "cms_security"
|| f == "all_features"
|| f == "browser_extension"
});
if is_paid {
return true;
}
return false;
}
let granted = license.features.iter().any(|f| f == feature);
if granted {
return true;
}
return false;
}
}
}
}
let token = VALIDATION_TOKEN.load(Ordering::SeqCst);
if token == 0 && KILLSWITCH_CHECKED.load(Ordering::SeqCst) {
return false;
}
false
}
pub fn allows_commercial_use() -> bool {
if let Some(license) = get_global_license() {
match license.license_type {
Some(LicenseType::Enterprise)
| Some(LicenseType::Team)
| Some(LicenseType::Professional) => true,
_ => false,
}
} else {
false
}
}
pub fn has_feature(feature: &str) -> bool {
if let Some(license) = get_global_license() {
if license.killswitch_active {
return false;
}
if license
.features
.iter()
.any(|f| f == feature || f == "all_features")
{
return true;
}
if let Some(license_type) = license.license_type {
match license_type {
LicenseType::Enterprise => {
return true;
}
LicenseType::Team => {
return matches!(
feature,
"cloud_scanning"
| "advanced_scanning"
| "cms_security"
| "browser_extension"
);
}
LicenseType::Professional => {
return matches!(
feature,
"advanced_scanning" | "cms_security" | "browser_extension"
);
}
LicenseType::Personal => {
return matches!(feature, "cms_security" | "browser_extension");
}
}
}
}
let token = VALIDATION_TOKEN.load(Ordering::SeqCst);
if token == 0 && KILLSWITCH_CHECKED.load(Ordering::SeqCst) {
return false;
}
false
}
pub fn get_max_targets() -> usize {
if let Some(license) = get_global_license() {
license.max_targets.unwrap_or(100) as usize
} else {
100 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LicenseType {
#[serde(alias = "Personal")]
Personal,
#[serde(alias = "Professional")]
Professional,
#[serde(alias = "Team")]
Team,
#[serde(alias = "Enterprise")]
Enterprise,
}
impl std::fmt::Display for LicenseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LicenseType::Personal => write!(f, "Personal"),
LicenseType::Professional => write!(f, "Professional"),
LicenseType::Team => write!(f, "Team"),
LicenseType::Enterprise => write!(f, "Enterprise"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseStatus {
#[serde(default)]
pub valid: bool,
pub license_type: Option<LicenseType>,
pub licensee: Option<String>,
pub organization: Option<String>,
pub expires_at: Option<String>,
#[serde(default)]
pub features: Vec<String>,
pub max_targets: Option<u32>,
#[serde(default)]
pub killswitch_active: bool,
pub killswitch_reason: Option<String>,
pub message: Option<String>,
}
impl Default for LicenseStatus {
fn default() -> Self {
Self {
valid: true,
license_type: Some(LicenseType::Personal),
licensee: None,
organization: None,
expires_at: None,
features: vec![
"basic_scanners".to_string(),
"basic_outputs".to_string(),
],
max_targets: Some(10), killswitch_active: false,
killswitch_reason: None,
message: Some("OFFLINE MODE: Limited features. Server unreachable. For full access: https://bountyy.fi".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
struct KillswitchResponse {
pub active: bool,
pub reason: Option<String>,
pub message: Option<String>,
#[serde(default)]
pub revoked_keys: Vec<String>,
}
pub struct LicenseManager {
license_key: Option<String>,
http_client: reqwest::Client,
hardware_id: Option<String>,
}
impl LicenseManager {
pub fn new() -> Result<Self> {
let user_agent = format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Lonkero/{}",
env!("CARGO_PKG_VERSION")
);
Ok(Self {
license_key: None,
http_client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(10))
.user_agent(&user_agent)
.default_headers({
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
"application/json, text/plain, */*".parse().unwrap(),
);
headers.insert(
reqwest::header::ACCEPT_LANGUAGE,
"en-US,en;q=0.9".parse().unwrap(),
);
headers.insert(
reqwest::header::ACCEPT_ENCODING,
"gzip, deflate, br".parse().unwrap(),
);
headers.insert(
"Sec-Fetch-Dest",
"empty".parse().unwrap(),
);
headers.insert(
"Sec-Fetch-Mode",
"cors".parse().unwrap(),
);
headers.insert(
"Sec-Fetch-Site",
"cross-site".parse().unwrap(),
);
headers
})
.build()?,
hardware_id: Self::get_hardware_id(),
})
}
fn get_hardware_id() -> Option<String> {
let mut components = Vec::new();
#[cfg(target_os = "linux")]
{
if let Ok(id) = fs::read_to_string("/etc/machine-id") {
components.push(format!("mid:{}", id.trim()));
}
if let Ok(id) = fs::read_to_string("/var/lib/dbus/machine-id") {
components.push(format!("dbus:{}", id.trim()));
}
}
#[cfg(target_os = "macos")]
{
if let Ok(output) = std::process::Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
let output_str = String::from_utf8_lossy(&output.stdout);
if let Some(uuid_line) = output_str.lines().find(|l| l.contains("IOPlatformUUID")) {
components.push(format!("uuid:{}", uuid_line.trim()));
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(output) = std::process::Command::new("cmd")
.args(["/C", "reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid"])
.output()
{
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("MachineGuid") {
if let Some(guid) = line.split_whitespace().last() {
components.push(format!("mid:{}", guid.trim()));
}
}
}
}
}
if let Ok(output) = std::process::Command::new("cmd")
.args(["/C", "reg query \"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\" /v ProductId"])
.output()
{
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("ProductId") {
if let Some(pid) = line.split_whitespace().last() {
components.push(format!("pid:{}", pid.trim()));
}
}
}
}
}
if let Ok(output) = std::process::Command::new("cmd")
.args(["/C", "wmic cpu get processorid"])
.output()
{
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines().skip(1) {
let cpu_id = line.trim();
if !cpu_id.is_empty() {
components.push(format!("cpu:{}", cpu_id));
break;
}
}
}
}
if let Ok(output) = std::process::Command::new("cmd")
.args(["/C", "wmic bios get serialnumber"])
.output()
{
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines().skip(1) {
let serial = line.trim();
if !serial.is_empty() && serial != "To be filled by O.E.M." {
components.push(format!("bios:{}", serial));
break;
}
}
}
}
if let Ok(output) = std::process::Command::new("cmd")
.args(["/C", "getmac /fo csv /nh"])
.output()
{
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = output_str.lines().next() {
if let Some(mac) = first_line.split(',').next() {
let mac = mac.trim().trim_matches('"');
if !mac.is_empty() && mac != "N/A" {
components.push(format!("mac:{}", mac));
}
}
}
}
}
if let Ok(output) = std::process::Command::new("cmd")
.args(["/C", "wmic baseboard get serialnumber"])
.output()
{
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines().skip(1) {
let serial = line.trim();
if !serial.is_empty() && serial != "To be filled by O.E.M." {
components.push(format!("board:{}", serial));
break;
}
}
}
}
}
#[cfg(target_os = "linux")]
{
if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") {
for line in cpuinfo.lines() {
if line.starts_with("Serial") || line.starts_with("processor") {
components.push(format!("cpu:{}", line.trim()));
break;
}
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
if let Ok(output) = std::process::Command::new("sh")
.args(["-c", "cat /sys/class/net/*/address 2>/dev/null | head -n1"])
.output()
{
if output.status.success() {
let mac = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !mac.is_empty() && mac != "00:00:00:00:00:00" {
components.push(format!("mac:{}", mac));
}
}
}
}
if let Ok(hostname) = hostname::get() {
if let Ok(hostname_str) = hostname.into_string() {
components.push(format!("host:{}", hostname_str));
}
}
if components.len() >= 2 {
let mut hasher = Sha256::new();
for component in components {
hasher.update(component.as_bytes());
hasher.update(b"|"); }
hasher.update(b"LONKERO_HW_V2"); let hash = hasher.finalize();
Some(hex::encode(&hash[0..16]))
} else {
warn!("Hardware fingerprinting failed: insufficient identifiers");
None
}
}
pub fn load_license(&mut self) -> Result<Option<String>> {
if let Ok(key) = std::env::var("LONKERO_LICENSE_KEY") {
self.license_key = Some(key.clone());
return Ok(Some(key));
}
if let Ok(entry) = keyring::Entry::new("lonkero", "license_key") {
if let Ok(key) = entry.get_password() {
if !key.is_empty() {
debug!("License key loaded from OS keychain");
self.license_key = Some(key.clone());
return Ok(Some(key));
}
}
}
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("lonkero");
let license_file = config_dir.join("license.key");
if license_file.exists() {
if let Ok(content) = fs::read_to_string(&license_file) {
let key = content.trim().to_string();
if !key.is_empty() {
warn!("Found license in INSECURE plaintext file. Migrating to OS keychain...");
if let Err(e) = self.save_license(&key) {
warn!("Failed to migrate license to keychain: {}", e);
} else {
if let Err(e) = fs::remove_file(&license_file) {
warn!("Failed to delete plaintext license file: {}", e);
} else {
info!("License migrated to secure OS keychain");
}
}
self.license_key = Some(key.clone());
return Ok(Some(key));
}
}
}
Ok(None)
}
pub fn set_license_key(&mut self, key: String) {
if anti_tamper::check_honeypot_key(&key) {
error!("Invalid license key detected");
return;
}
self.license_key = Some(key);
}
pub fn save_license(&self, key: &str) -> Result<()> {
let entry = keyring::Entry::new("lonkero", "license_key")
.map_err(|e| anyhow!("Failed to access OS keychain: {}", e))?;
entry
.set_password(key)
.map_err(|e| anyhow!("Failed to save license to keychain: {}", e))?;
info!("License key saved to OS keychain (encrypted)");
Ok(())
}
pub async fn validate(&self) -> Result<LicenseStatus> {
debug!("Starting license validation with retry...");
match self.check_server_with_retry().await {
Ok(status) => {
KILLSWITCH_CHECKED.store(true, Ordering::SeqCst);
KILLSWITCH_ACTIVE.store(status.killswitch_active, Ordering::SeqCst);
let token = generate_token(&status);
VALIDATION_TOKEN.store(token, Ordering::SeqCst);
anti_tamper::initialize_protection();
if status.valid && !status.killswitch_active {
let license_hash = {
let mut hasher = Sha256::new();
hasher.update(format!("{:?}", status.license_type).as_bytes());
hasher.update(status.licensee.as_deref().unwrap_or("").as_bytes());
hasher.update(&token.to_le_bytes());
let hash = hasher.finalize();
u64::from_le_bytes(hash[0..8].try_into().unwrap())
};
anti_tamper::set_validated(license_hash);
}
if status.killswitch_active {
error!(
"KILLSWITCH ACTIVE: {}",
status.killswitch_reason.as_deref().unwrap_or("Unknown")
);
VALIDATION_TOKEN.store(0, Ordering::SeqCst);
anti_tamper::trigger_tamper_response("killswitch_active");
}
Ok(status)
}
Err(e) => {
error!("License server unreachable: {}. Running in OFFLINE MODE with limited features.", e);
warn!(
"Premium features DISABLED. Restore network connection for full functionality."
);
KILLSWITCH_CHECKED.store(true, Ordering::SeqCst);
KILLSWITCH_ACTIVE.store(false, Ordering::SeqCst);
let offline_status = LicenseStatus::default();
let token = generate_token(&offline_status);
VALIDATION_TOKEN.store(token, Ordering::SeqCst);
anti_tamper::initialize_protection();
anti_tamper::set_validated(0);
Ok(offline_status)
}
}
}
async fn check_server_with_retry(&self) -> Result<LicenseStatus> {
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 1000;
let mut last_error = None;
for attempt in 0..MAX_RETRIES {
if attempt > 0 {
let backoff_ms = INITIAL_BACKOFF_MS * 2_u64.pow(attempt - 1);
debug!("Retry attempt {} after {}ms", attempt + 1, backoff_ms);
tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
}
match self.check_server().await {
Ok(status) => return Ok(status),
Err(e) => {
debug!("License server attempt {} failed: {}", attempt + 1, e);
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or_else(|| anyhow!("All retry attempts failed")))
}
async fn check_server(&self) -> Result<LicenseStatus> {
let url = format!("{}/validate", LICENSE_SERVER);
debug!("Validating license with server: {}", url);
debug!("License key present: {}", self.license_key.is_some());
let mut request = self
.http_client
.post(&url)
.header("X-Product", "lonkero")
.header("X-Version", env!("CARGO_PKG_VERSION"));
if let Some(ref hw_id) = self.hardware_id {
request = request.header("X-Hardware-ID", hw_id);
}
let body = serde_json::json!({
"license_key": self.license_key,
"hardware_id": self.hardware_id,
"product": "lonkero",
"version": env!("CARGO_PKG_VERSION")
});
let response = request.json(&body).send().await?;
let status_code = response.status();
debug!("License server response status: {}", status_code);
if status_code.is_success() {
let text = response.text().await?;
debug!("License server response: {}", text);
match serde_json::from_str::<LicenseStatus>(&text) {
Ok(status) => {
info!(
"License validated: type={:?}, licensee={:?}",
status.license_type, status.licensee
);
Ok(status)
}
Err(e) => {
warn!(
"Failed to parse license response: {}. Response was: {}",
e, text
);
Err(anyhow!("Failed to parse license response: {}", e))
}
}
} else if status_code.as_u16() == 403 {
let text = response.text().await.unwrap_or_default();
let is_cloudflare_block = text.contains("error code: 1020")
|| text.contains("cloudflare")
|| text.contains("Cloudflare")
|| text.contains("cf-ray")
|| text.contains("Ray ID:");
if is_cloudflare_block {
warn!("Cloudflare blocking license server request. Running in limited mode.");
warn!("Response: {}", &text[..text.char_indices().nth(200).map_or(text.len(), |(i, _)| i)]);
Err(anyhow!("Cloudflare blocked license request - network issue"))
} else {
warn!("License blocked (403): {}", text);
let status: LicenseStatus =
serde_json::from_str(&text).unwrap_or_else(|_| LicenseStatus {
valid: false,
killswitch_active: true,
killswitch_reason: Some("Access denied".to_string()),
..Default::default()
});
Ok(status)
}
} else {
let text = response.text().await.unwrap_or_default();
warn!("License server error {}: {}", status_code, text);
Err(anyhow!("Server returned: {} - {}", status_code, text))
}
}
pub fn allows_commercial_use(&self) -> bool {
if let Some(ref _key) = self.license_key {
return true;
}
false
}
}
pub async fn verify_license_for_scan(
license_key: Option<&str>,
_target_count: usize,
is_commercial: bool,
) -> Result<LicenseStatus> {
let mut manager = LicenseManager::new()?;
manager.load_license()?;
if let Some(key) = license_key {
manager.set_license_key(key.to_string());
}
let status = manager.validate().await?;
let _ = GLOBAL_LICENSE.set(status.clone());
let now = chrono::Utc::now().timestamp() as u64;
LAST_VALIDATION.store(now, Ordering::SeqCst);
if status.killswitch_active {
return Err(anyhow!(
"Scanner disabled: {}",
status
.killswitch_reason
.clone()
.unwrap_or_else(|| "Contact info@bountyy.fi".to_string())
));
}
if is_commercial && !manager.allows_commercial_use() {
warn!("========================================================");
warn!("NOTE: Commercial use requires a license from Bountyy Oy");
warn!(" Visit: https://bountyy.fi");
warn!("========================================================");
}
Ok(status)
}
pub fn get_global_license() -> Option<&'static LicenseStatus> {
GLOBAL_LICENSE.get()
}
pub fn is_validation_stale() -> bool {
const VALIDATION_TIMEOUT_SECS: u64 = 86400; let last = LAST_VALIDATION.load(Ordering::SeqCst);
if last == 0 {
return true; }
let now = chrono::Utc::now().timestamp() as u64;
now.saturating_sub(last) > VALIDATION_TIMEOUT_SECS
}
pub fn hours_since_validation() -> u64 {
let last = LAST_VALIDATION.load(Ordering::SeqCst);
if last == 0 {
return u64::MAX; }
let now = chrono::Utc::now().timestamp() as u64;
now.saturating_sub(last) / 3600
}
pub fn print_license_info(status: &LicenseStatus) {
if let Some(lt) = status.license_type {
match lt {
LicenseType::Personal => info!("License: Free Non-Commercial Edition"),
_ => info!("License: {} Edition", lt),
}
}
if let Some(ref licensee) = status.licensee {
info!("Licensed to: {}", licensee);
}
if let Some(ref org) = status.organization {
info!("Organization: {}", org);
}
if let Some(ref msg) = status.message {
debug!("{}", msg);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_license_has_limited_access() {
let status = LicenseStatus::default();
assert!(status.valid);
assert!(!status.killswitch_active);
assert!(status.features.contains(&"basic_scanners".to_string()));
assert!(status.features.contains(&"basic_outputs".to_string()));
assert!(!status.features.contains(&"all_scanners".to_string()));
assert_eq!(status.max_targets, Some(10));
}
#[test]
fn test_license_type_display() {
assert_eq!(format!("{}", LicenseType::Personal), "Personal");
assert_eq!(format!("{}", LicenseType::Enterprise), "Enterprise");
}
}