use std::process::Command;
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
pub const HARDWARE_INVENTORY_SCHEMA_V1: &str = "io.styrene.nex.hardware-inventory.v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum HardwarePlatform {
Darwin,
Linux,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct HardwareInventory {
pub schema: String,
pub platform: HardwarePlatform,
pub arch: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpu: Option<HardwareCpuSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory: Option<HardwareMemorySummary>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disks: Vec<HardwareDisk>,
pub evidence: HardwareEvidence,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<HardwareWarning>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardwareCpuSummary {
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardwareMemorySummary {
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct HardwareProfileMatchReport {
pub schema: String,
pub inventory_schema: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_purpose: Option<String>,
pub hardware_class: HardwareClassCandidate,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recommendations: Vec<HardwareProfileRecommendation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<HardwareWarning>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct HardwareProfileRecommendation {
pub profile_ref: String,
pub score: u8,
pub confidence: ClassificationConfidence,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasons: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub missing: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum HardwareClassCandidate {
MacosArm64,
MacosAmd64,
Amd64AppleT2,
Amd64Generic,
Arm64Generic,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DiskAttestationReport {
pub schema: String,
pub disk: HardwareDisk,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct HardwareDisk {
pub id: String,
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub whole_disk: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub internal: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub removable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ejectable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub solid_state: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bus: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_name: Option<String>,
pub classification: DiskClassification,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence_sources: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DiskClassification {
pub target_attestation: TargetAttestationCandidate,
pub destructive_default: DestructiveDefault,
pub confidence: ClassificationConfidence,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasons: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum TargetAttestationCandidate {
ExternalUsbSsd,
ExternalThunderboltSsd,
InternalAppleStorage,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum DestructiveDefault {
AllowedWithAttestation,
Forbidden,
RequiresOperatorAttestation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ClassificationConfidence {
Strong,
Weak,
Unknown,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardwareEvidence {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HardwareWarning {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DiskEvidence {
pub id: String,
pub path: String,
pub whole_disk: Option<bool>,
pub size_bytes: Option<u64>,
pub internal: Option<bool>,
pub removable: Option<bool>,
pub ejectable: Option<bool>,
pub solid_state: Option<bool>,
pub bus: Option<String>,
pub media_name: Option<String>,
pub io_registry_entry_name: Option<String>,
pub device_tree_path: Option<String>,
pub evidence_sources: Vec<String>,
}
pub fn scan_host() -> Result<HardwareInventory> {
if cfg!(target_os = "macos") {
return collect_darwin_inventory();
}
Ok(degraded_inventory(
HardwarePlatform::Unknown,
"UNSUPPORTED_PLATFORM",
"Live hardware scanning is not implemented for this platform yet.",
))
}
pub fn attest_disk(disk: &str) -> Result<DiskAttestationReport> {
let inventory = scan_host()?;
attest_disk_from_inventory(inventory, disk)
}
pub fn attest_disk_from_inventory(
inventory: HardwareInventory,
disk: &str,
) -> Result<DiskAttestationReport> {
let normalized = normalize_disk_selector(disk);
let disk = inventory
.disks
.into_iter()
.find(|candidate| {
candidate.id == normalized
|| candidate.path == disk
|| candidate.path.strip_prefix("/dev/") == Some(normalized.as_str())
})
.with_context(|| format!("disk {disk:?} was not found in hardware inventory"))?;
Ok(DiskAttestationReport {
schema: "io.styrene.nex.disk-attestation.v1".to_string(),
disk,
})
}
pub fn match_profiles(
inventory: &HardwareInventory,
purpose: Option<&str>,
) -> HardwareProfileMatchReport {
let hardware_class = classify_hardware(inventory);
let mut recommendations = Vec::new();
if let Some(profile_ref) = starter_profile_for(&hardware_class, purpose) {
recommendations.push(HardwareProfileRecommendation {
profile_ref,
score: if purpose.is_some() { 85 } else { 70 },
confidence: if purpose.is_some() {
ClassificationConfidence::Strong
} else {
ClassificationConfidence::Weak
},
reasons: vec![format!(
"inventory matches hardware class {hardware_class:?}"
)],
missing: if purpose.is_some() {
Vec::new()
} else {
vec!["purpose not supplied".to_string()]
},
});
}
HardwareProfileMatchReport {
schema: "io.styrene.nex.hardware-profile-match.v1".to_string(),
inventory_schema: inventory.schema.clone(),
requested_purpose: purpose.map(ToString::to_string),
hardware_class,
recommendations,
warnings: Vec::new(),
}
}
fn classify_hardware(inventory: &HardwareInventory) -> HardwareClassCandidate {
match inventory.platform {
HardwarePlatform::Darwin if inventory.arch == "aarch64" => {
HardwareClassCandidate::MacosArm64
}
HardwarePlatform::Darwin if inventory.arch == "x86_64" => {
if inventory
.model
.as_deref()
.is_some_and(|model| model.starts_with("MacBookPro"))
{
HardwareClassCandidate::Amd64AppleT2
} else {
HardwareClassCandidate::MacosAmd64
}
}
HardwarePlatform::Linux if inventory.arch == "aarch64" => {
HardwareClassCandidate::Arm64Generic
}
HardwarePlatform::Linux if inventory.arch == "x86_64" => {
HardwareClassCandidate::Amd64Generic
}
_ => HardwareClassCandidate::Unknown,
}
}
fn starter_profile_for(
hardware_class: &HardwareClassCandidate,
purpose: Option<&str>,
) -> Option<String> {
let purpose = purpose.unwrap_or("dev");
let hardware = match hardware_class {
HardwareClassCandidate::MacosArm64 => "macos-arm64",
HardwareClassCandidate::MacosAmd64 => "macos-amd64",
HardwareClassCandidate::Amd64AppleT2 => "amd64-apple-t2",
HardwareClassCandidate::Amd64Generic => "amd64",
HardwareClassCandidate::Arm64Generic => "arm64",
HardwareClassCandidate::Unknown => return None,
};
Some(format!("starter/{hardware}-{purpose}"))
}
fn normalize_disk_selector(disk: &str) -> String {
disk.strip_prefix("/dev/").unwrap_or(disk).to_string()
}
fn degraded_inventory(
platform: HardwarePlatform,
code: impl Into<String>,
message: impl Into<String>,
) -> HardwareInventory {
HardwareInventory {
schema: HARDWARE_INVENTORY_SCHEMA_V1.to_string(),
platform,
arch: std::env::consts::ARCH.to_string(),
vendor: None,
model: None,
model_name: None,
cpu: None,
memory: None,
disks: Vec::new(),
evidence: HardwareEvidence::default(),
warnings: vec![HardwareWarning {
code: code.into(),
message: message.into(),
}],
}
}
fn collect_darwin_inventory() -> Result<HardwareInventory> {
let hardware_json = run_capture("system_profiler", &["SPHardwareDataType", "-json"])?;
let hardware = parse_system_profiler_hardware_json(&hardware_json)?;
let disk_list_plist = run_capture("diskutil", &["list", "-plist"])?;
let whole_disks = parse_diskutil_whole_disks(&disk_list_plist)?;
let mut disks = Vec::new();
let mut warnings = Vec::new();
for disk in whole_disks {
match run_capture("diskutil", &["info", "-plist", disk.as_str()]) {
Ok(info) => match parse_diskutil_info_plist(&info) {
Ok(disk) => disks.push(disk),
Err(error) => warnings.push(HardwareWarning {
code: "DISKUTIL_INFO_PARSE_FAILED".to_string(),
message: format!("Failed to parse diskutil info for {disk}: {error:#}"),
}),
},
Err(error) => warnings.push(HardwareWarning {
code: "DISKUTIL_INFO_FAILED".to_string(),
message: format!("Failed to inspect {disk}: {error:#}"),
}),
}
}
Ok(HardwareInventory {
schema: HARDWARE_INVENTORY_SCHEMA_V1.to_string(),
platform: HardwarePlatform::Darwin,
arch: std::env::consts::ARCH.to_string(),
vendor: Some("Apple".to_string()),
model: hardware.machine_model,
model_name: hardware.machine_name,
cpu: hardware
.chip_type
.map(|summary| HardwareCpuSummary { summary }),
memory: hardware
.physical_memory
.map(|summary| HardwareMemorySummary { summary }),
disks,
evidence: HardwareEvidence {
commands: vec![
"system_profiler SPHardwareDataType -json".to_string(),
"diskutil list -plist".to_string(),
"diskutil info -plist <disk>".to_string(),
],
},
warnings,
})
}
fn run_capture(program: &str, args: &[&str]) -> Result<String> {
let output = Command::new(program)
.args(args)
.output()
.with_context(|| format!("running {program}"))?;
if !output.status.success() {
bail!(
"{} {} failed with status {}: {}",
program,
args.join(" "),
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
}
String::from_utf8(output.stdout).with_context(|| format!("decoding {program} stdout as UTF-8"))
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DiskutilList {
#[serde(default, rename = "AllDisksAndPartitions")]
all_disks_and_partitions: Vec<DiskutilListDisk>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DiskutilListDisk {
#[serde(rename = "DeviceIdentifier")]
device_identifier: String,
#[serde(default, rename = "Content")]
content: Option<String>,
}
pub fn parse_diskutil_whole_disks(plist: &str) -> Result<Vec<String>> {
let list: DiskutilList =
plist::from_bytes(plist.as_bytes()).context("decoding diskutil list plist")?;
Ok(list
.all_disks_and_partitions
.into_iter()
.filter(|disk| {
disk.content
.as_deref()
.is_some_and(|content| content.ends_with("partition_scheme"))
})
.map(|disk| disk.device_identifier)
.collect())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SystemProfilerHardware {
#[serde(rename = "SPHardwareDataType")]
sp_hardware_data_type: Vec<SystemProfilerHardwareItem>,
}
#[derive(Debug, Deserialize)]
struct SystemProfilerHardwareItem {
machine_model: Option<String>,
machine_name: Option<String>,
chip_type: Option<String>,
physical_memory: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DarwinHardwareSummary {
pub machine_model: Option<String>,
pub machine_name: Option<String>,
pub chip_type: Option<String>,
pub physical_memory: Option<String>,
}
pub fn parse_system_profiler_hardware_json(input: &str) -> Result<DarwinHardwareSummary> {
let parsed: SystemProfilerHardware =
serde_json::from_str(input).context("decoding system_profiler hardware JSON")?;
let item = parsed
.sp_hardware_data_type
.into_iter()
.next()
.context("system_profiler hardware JSON did not contain SPHardwareDataType")?;
Ok(DarwinHardwareSummary {
machine_model: item.machine_model,
machine_name: item.machine_name,
chip_type: item.chip_type,
physical_memory: item.physical_memory,
})
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DiskutilInfo {
#[serde(rename = "DeviceIdentifier")]
device_identifier: String,
#[serde(rename = "DeviceNode")]
device_node: Option<String>,
#[serde(rename = "WholeDisk")]
whole_disk: Option<bool>,
#[serde(rename = "Size")]
size: Option<u64>,
#[serde(rename = "Internal")]
internal: Option<bool>,
#[serde(rename = "RemovableMedia")]
removable_media: Option<bool>,
#[serde(rename = "Ejectable")]
ejectable: Option<bool>,
#[serde(rename = "SolidState")]
solid_state: Option<bool>,
#[serde(rename = "BusProtocol")]
bus_protocol: Option<String>,
#[serde(rename = "MediaName")]
media_name: Option<String>,
#[serde(rename = "IORegistryEntryName")]
io_registry_entry_name: Option<String>,
#[serde(rename = "DeviceTreePath")]
device_tree_path: Option<String>,
}
pub fn parse_diskutil_info_plist(input: &str) -> Result<HardwareDisk> {
let info: DiskutilInfo =
plist::from_bytes(input.as_bytes()).context("decoding diskutil info plist")?;
let evidence = DiskEvidence {
id: info.device_identifier.clone(),
path: info
.device_node
.clone()
.unwrap_or_else(|| format!("/dev/{}", info.device_identifier)),
whole_disk: info.whole_disk,
size_bytes: info.size,
internal: info.internal,
removable: info.removable_media,
ejectable: info.ejectable,
solid_state: info.solid_state,
bus: info.bus_protocol,
media_name: info.media_name,
io_registry_entry_name: info.io_registry_entry_name,
device_tree_path: info.device_tree_path,
evidence_sources: vec!["diskutil-info-plist".to_string()],
};
Ok(HardwareDisk {
id: evidence.id.clone(),
path: evidence.path.clone(),
whole_disk: evidence.whole_disk,
size_bytes: evidence.size_bytes,
internal: evidence.internal,
removable: evidence.removable,
ejectable: evidence.ejectable,
solid_state: evidence.solid_state,
bus: evidence.bus.clone(),
vendor: apple_storage_evidence(&evidence).then(|| "Apple".to_string()),
model: evidence.media_name.clone(),
media_name: evidence.media_name.clone(),
classification: classify_disk(&evidence),
evidence_sources: evidence.evidence_sources,
})
}
pub fn classify_disk(evidence: &DiskEvidence) -> DiskClassification {
let bus = evidence
.bus
.as_deref()
.unwrap_or_default()
.to_ascii_lowercase();
if evidence.internal == Some(true) && apple_storage_evidence(evidence) {
return DiskClassification {
target_attestation: TargetAttestationCandidate::InternalAppleStorage,
destructive_default: DestructiveDefault::Forbidden,
confidence: ClassificationConfidence::Strong,
reasons: vec![
"disk is internal".to_string(),
"disk evidence identifies Apple internal storage".to_string(),
],
};
}
if evidence.internal == Some(false)
&& evidence.solid_state == Some(true)
&& (evidence.removable == Some(true) || evidence.ejectable == Some(true))
&& bus.contains("usb")
{
return DiskClassification {
target_attestation: TargetAttestationCandidate::ExternalUsbSsd,
destructive_default: DestructiveDefault::AllowedWithAttestation,
confidence: ClassificationConfidence::Strong,
reasons: vec![
"disk is external or removable".to_string(),
"bus protocol is USB".to_string(),
"disk reports solid-state media".to_string(),
],
};
}
if evidence.internal == Some(false)
&& evidence.solid_state == Some(true)
&& bus.contains("thunderbolt")
{
return DiskClassification {
target_attestation: TargetAttestationCandidate::ExternalThunderboltSsd,
destructive_default: DestructiveDefault::AllowedWithAttestation,
confidence: ClassificationConfidence::Strong,
reasons: vec![
"disk is external".to_string(),
"bus protocol is Thunderbolt".to_string(),
"disk reports solid-state media".to_string(),
],
};
}
DiskClassification {
target_attestation: TargetAttestationCandidate::Unknown,
destructive_default: DestructiveDefault::RequiresOperatorAttestation,
confidence: ClassificationConfidence::Unknown,
reasons: vec!["disk evidence is insufficient for safe target attestation".to_string()],
}
}
fn apple_storage_evidence(evidence: &DiskEvidence) -> bool {
let haystack = [
evidence.bus.as_deref(),
evidence.media_name.as_deref(),
evidence.io_registry_entry_name.as_deref(),
evidence.device_tree_path.as_deref(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase();
haystack.contains("apple fabric")
|| haystack.contains("apple ssd")
|| haystack.contains("appleans")
|| haystack.contains("ans@")
}
#[cfg(test)]
mod tests {
use super::*;
const SYSTEM_PROFILER_HARDWARE: &str = r#"{
"SPHardwareDataType": [
{
"_name": "hardware_overview",
"chip_type": "Apple M5 Max",
"machine_model": "Mac17,7",
"machine_name": "MacBook Pro",
"physical_memory": "128 GB",
"platform_UUID": "REDACTED",
"serial_number": "REDACTED"
}
]
}"#;
const DISKUTIL_INFO_INTERNAL_APPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>DeviceIdentifier</key><string>disk0</string>
<key>DeviceNode</key><string>/dev/disk0</string>
<key>WholeDisk</key><true/>
<key>Size</key><integer>4002222325760</integer>
<key>Internal</key><true/>
<key>SolidState</key><true/>
<key>BusProtocol</key><string>Apple Fabric</string>
<key>MediaName</key><string>APPLE SSD AP4096Z</string>
<key>IORegistryEntryName</key><string>APPLE SSD AP4096Z Media</string>
<key>DeviceTreePath</key><string>IODeviceTree:/arm-io@10F00000/ans@19600000/iop-ans-nub/AppleANS3CGv2Controller</string>
<key>RemovableMedia</key><false/>
<key>Ejectable</key><false/>
</dict></plist>"#;
#[test]
fn parses_system_profiler_hardware_summary_without_sensitive_fields() -> Result<()> {
let summary = parse_system_profiler_hardware_json(SYSTEM_PROFILER_HARDWARE)?;
assert_eq!(summary.machine_model.as_deref(), Some("Mac17,7"));
assert_eq!(summary.machine_name.as_deref(), Some("MacBook Pro"));
assert_eq!(summary.chip_type.as_deref(), Some("Apple M5 Max"));
assert_eq!(summary.physical_memory.as_deref(), Some("128 GB"));
Ok(())
}
#[test]
fn classifies_internal_apple_fabric_storage_as_forbidden() -> Result<()> {
let disk = parse_diskutil_info_plist(DISKUTIL_INFO_INTERNAL_APPLE)?;
assert_eq!(disk.id, "disk0");
assert_eq!(disk.internal, Some(true));
assert_eq!(
disk.classification.target_attestation,
TargetAttestationCandidate::InternalAppleStorage
);
assert_eq!(
disk.classification.destructive_default,
DestructiveDefault::Forbidden
);
assert_eq!(
disk.classification.confidence,
ClassificationConfidence::Strong
);
Ok(())
}
#[test]
fn attestation_selector_normalizes_dev_paths() {
assert_eq!(normalize_disk_selector("/dev/disk4"), "disk4");
assert_eq!(normalize_disk_selector("disk4"), "disk4");
}
#[test]
fn matches_macos_arm64_dev_starter() {
let inventory = HardwareInventory {
schema: HARDWARE_INVENTORY_SCHEMA_V1.to_string(),
platform: HardwarePlatform::Darwin,
arch: "aarch64".to_string(),
vendor: Some("Apple".to_string()),
model: Some("Mac17,7".to_string()),
model_name: Some("MacBook Pro".to_string()),
cpu: None,
memory: None,
disks: Vec::new(),
evidence: HardwareEvidence::default(),
warnings: Vec::new(),
};
let report = match_profiles(&inventory, Some("dev"));
assert_eq!(report.hardware_class, HardwareClassCandidate::MacosArm64);
assert_eq!(
report.recommendations[0].profile_ref,
"starter/macos-arm64-dev"
);
assert_eq!(
report.recommendations[0].confidence,
ClassificationConfidence::Strong
);
}
}