#![allow(missing_docs)]
use crate::SensitiveString;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, LazyLock};
use thiserror::Error;
pub const DEFAULT_WINDOW_SIZE_BYTES: usize = 1024 * 1024;
pub const DEFAULT_WINDOW_OVERLAP_BYTES: usize = 128 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceCoverageGapKind {
Inaccessible,
Truncated,
}
impl std::fmt::Display for SourceCoverageGapKind {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Inaccessible => "inaccessible",
Self::Truncated => "truncated",
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chunk {
pub data: SensitiveString,
pub metadata: ChunkMetadata,
}
impl From<String> for Chunk {
fn from(data: String) -> Self {
Self {
data: data.into(),
metadata: ChunkMetadata::default(),
}
}
}
impl From<&str> for Chunk {
fn from(data: &str) -> Self {
Self::from(data.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChunkMetadata {
#[serde(with = "crate::finding::serde_arc_str")]
pub source_type: Arc<str>,
#[serde(with = "crate::finding::serde_arc_str_opt")]
pub path: Option<Arc<str>>,
#[serde(with = "crate::finding::serde_arc_str_opt")]
pub commit: Option<Arc<str>>,
#[serde(with = "crate::finding::serde_arc_str_opt")]
pub author: Option<Arc<str>>,
#[serde(with = "crate::finding::serde_arc_str_opt")]
pub date: Option<Arc<str>>,
pub base_offset: usize,
#[serde(default)]
pub base_line: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mtime_ns: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ctime_ns: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decoded_span: Option<(usize, usize)>,
}
macro_rules! define_intern_table {
(
$(#[$intern_meta:meta])*
pub fn $fn_intern:ident,
$(#[$list_meta:meta])*
pub fn $fn_list:ident,
$(
$(#[$item_meta:meta])*
($name:ident, $str_val:literal)
),* $(,)?
) => {
$(
$(#[$item_meta])*
#[doc = concat!("Canonical `source_type` for `", $str_val, "` chunks.")]
pub static $name: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from($str_val));
)*
$(#[$list_meta])*
pub fn $fn_list() -> &'static [&'static str] {
&[
$( $str_val ),*
]
}
$(#[$intern_meta])*
pub fn $fn_intern(val: &str) -> Arc<str> {
match val {
$(
$str_val => Arc::clone(&$name),
)*
other => Arc::from(other),
}
}
};
}
define_intern_table! {
pub fn intern_source_type,
pub fn common_source_types,
(SOURCE_TYPE_AZURE_BLOB, "azure_blob"),
(SOURCE_TYPE_BINARY, "binary"),
(SOURCE_TYPE_BINARY_GHIDRA_DECOMPILED, "binary:ghidra:decompiled"),
(SOURCE_TYPE_BINARY_GHIDRA_STRINGS, "binary:ghidra:strings"),
(SOURCE_TYPE_BINARY_STRINGS, "binary:strings"),
(SOURCE_TYPE_DOCKER, "docker"),
(SOURCE_TYPE_FILESYSTEM, "filesystem"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE, "filesystem/archive"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_BINARY, "filesystem/archive-binary"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_BINARY_TEX_ORPHANED, "filesystem/archive-binary/tex-orphaned"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_BINARY_TEX_REFERENCED, "filesystem/archive-binary/tex-referenced"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_BINARY_TEX_ROOT, "filesystem/archive-binary/tex-root"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_ANDROID, "filesystem/archive/android"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_ANDROID_RESOURCE, "filesystem/archive/android-resource"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_ANDROID_XML, "filesystem/archive/android-xml"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_TEX_COMMENT_ORPHANED, "filesystem/archive/tex-comment/orphaned"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_TEX_COMMENT_REFERENCED, "filesystem/archive/tex-comment/referenced"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_TEX_COMMENT_ROOT, "filesystem/archive/tex-comment/root"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_TEX_ORPHANED, "filesystem/archive/tex-orphaned"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_TEX_REFERENCED, "filesystem/archive/tex-referenced"),
(SOURCE_TYPE_FILESYSTEM_ARCHIVE_TEX_ROOT, "filesystem/archive/tex-root"),
(SOURCE_TYPE_FILESYSTEM_COMPRESSED, "filesystem/compressed"),
(SOURCE_TYPE_FILESYSTEM_COMPRESSED_BINARY, "filesystem/compressed-binary"),
(SOURCE_TYPE_FILESYSTEM_PDF, "filesystem/pdf"),
(SOURCE_TYPE_FILESYSTEM_WINDOWED, "filesystem/windowed"),
(SOURCE_TYPE_FILESYSTEM_BINARY_STRINGS, "filesystem:binary-strings"),
(SOURCE_TYPE_GCS, "gcs"),
(SOURCE_TYPE_GIT, "git"),
(SOURCE_TYPE_GIT_DIFF, "git-diff"),
(SOURCE_TYPE_GIT_HISTORY, "git-history"),
(SOURCE_TYPE_GIT_STAGED, "git-staged"),
(SOURCE_TYPE_GIT_DIFF_SLASH, "git/diff"),
(SOURCE_TYPE_GIT_HEAD, "git/head"),
(SOURCE_TYPE_GIT_HISTORY_SLASH, "git/history"),
(SOURCE_TYPE_GIT_STAGED_SLASH, "git/staged"),
(SOURCE_TYPE_GIT_TAG, "git/tag"),
(SOURCE_TYPE_GIT_UNREACHABLE, "git/unreachable"),
(SOURCE_TYPE_GITHUB, "github"),
(SOURCE_TYPE_S3, "s3"),
(SOURCE_TYPE_SLACK, "slack"),
(SOURCE_TYPE_STDIN, "stdin"),
(SOURCE_TYPE_WEB, "web"),
(SOURCE_TYPE_WEB_JS, "web:js"),
(SOURCE_TYPE_WEB_SOURCEMAP, "web:sourcemap"),
(SOURCE_TYPE_WEB_SOURCEMAP_RAW, "web:sourcemap:raw"),
(SOURCE_TYPE_WEB_WASM, "web:wasm"),
(SOURCE_TYPE_WIRE_HAR_REQUEST, "wire:har:request"),
(SOURCE_TYPE_WIRE_HAR_RESPONSE, "wire:har:response"),
}
pub trait Source: Send + Sync {
fn name(&self) -> &str;
fn chunks(&self) -> Box<dyn Iterator<Item = Result<Chunk, SourceError>> + '_>;
fn as_any(&self) -> &dyn std::any::Any;
fn chunk_identities_are_contiguous(&self) -> bool {
false
}
}
#[derive(Debug, Error)]
pub enum SourceError {
#[error(
"failed to read source: {0}. Fix: check the path exists, is readable, and is not a broken symlink"
)]
Io(#[from] std::io::Error),
#[error(
"failed to access git source: {0}. Fix: run inside a valid git repository and verify the requested refs exist"
)]
Git(String),
#[error(
"source coverage gap ({kind}) in {adapter} surface {surface} at {target}: {detail}. Fix: grant read access or raise the relevant source limit, then rerun the affected surface"
)]
Coverage {
adapter: String,
surface: String,
target: String,
kind: SourceCoverageGapKind,
detail: String,
},
#[error("unknown source '{name}'. Fix: use a source name listed by `keyhog scan --help`")]
UnknownSource {
name: String,
},
#[error(
"source '{source_name}' is unavailable because this KeyHog artifact was built without the '{feature}' feature. Fix: install an artifact that includes '{feature}' or choose an enabled source"
)]
FeatureUnavailable {
source_name: String,
feature: String,
},
#[error(
"invalid configuration for source '{source_name}': {detail}. Fix: use the parameter format documented by `keyhog scan --help`"
)]
InvalidConfiguration {
source_name: String,
detail: String,
},
#[error(
"source name '{name}' is no longer accepted; use '{replacement}'. Fix: update the source identifier and rerun the scan"
)]
DeprecatedSourceName {
name: String,
replacement: String,
},
#[error(
"failed to read source: {0}. Fix: adjust the source settings or input so KeyHog can read plain text safely"
)]
Other(String),
}