up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Homebrew;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin that registers the Homebrew package analyzer for macOS Cellar discovery.
|
||||
/// </summary>
|
||||
public sealed class HomebrewAnalyzerPlugin : IOSAnalyzerPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "StellaOps.Scanner.Analyzers.OS.Homebrew";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IOSPackageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
return new HomebrewPackageAnalyzer(loggerFactory.CreateLogger<HomebrewPackageAnalyzer>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Analyzers;
|
||||
using StellaOps.Scanner.Analyzers.OS.Helpers;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Homebrew;
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes Homebrew Cellar directories to extract installed formula information.
|
||||
/// Scans /usr/local/Cellar (Intel) and /opt/homebrew/Cellar (Apple Silicon) directories.
|
||||
/// </summary>
|
||||
internal sealed class HomebrewPackageAnalyzer : OsPackageAnalyzerBase
|
||||
{
|
||||
private static readonly IReadOnlyList<OSPackageRecord> EmptyPackages =
|
||||
new ReadOnlyCollection<OSPackageRecord>(Array.Empty<OSPackageRecord>());
|
||||
|
||||
/// <summary>
|
||||
/// Default paths to scan for Homebrew Cellar directories.
|
||||
/// </summary>
|
||||
private static readonly string[] CellarPaths =
|
||||
[
|
||||
"usr/local/Cellar", // Intel Macs
|
||||
"opt/homebrew/Cellar", // Apple Silicon Macs
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Maximum traversal depth within Cellar to prevent runaway scanning.
|
||||
/// Formula structure: Cellar/{formula}/{version}/...
|
||||
/// </summary>
|
||||
private const int MaxTraversalDepth = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum formula size in bytes (200MB default per design spec).
|
||||
/// </summary>
|
||||
private const long MaxFormulaSizeBytes = 200L * 1024L * 1024L;
|
||||
|
||||
private readonly HomebrewReceiptParser _parser = new();
|
||||
|
||||
public HomebrewPackageAnalyzer(ILogger<HomebrewPackageAnalyzer> logger)
|
||||
: base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string AnalyzerId => "homebrew";
|
||||
|
||||
protected override ValueTask<IReadOnlyList<OSPackageRecord>> ExecuteCoreAsync(
|
||||
OSPackageAnalyzerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var records = new List<OSPackageRecord>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
foreach (var cellarRelativePath in CellarPaths)
|
||||
{
|
||||
var cellarPath = Path.Combine(context.RootPath, cellarRelativePath);
|
||||
if (!Directory.Exists(cellarPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Scanning Homebrew Cellar at {Path}", cellarPath);
|
||||
|
||||
try
|
||||
{
|
||||
DiscoverFormulas(cellarPath, records, warnings, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to scan Homebrew Cellar at {Path}", cellarPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (records.Count == 0)
|
||||
{
|
||||
Logger.LogInformation("No Homebrew formulas found; skipping analyzer.");
|
||||
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(EmptyPackages);
|
||||
}
|
||||
|
||||
foreach (var warning in warnings)
|
||||
{
|
||||
Logger.LogWarning("Homebrew scan warning: {Warning}", warning);
|
||||
}
|
||||
|
||||
Logger.LogInformation("Discovered {Count} Homebrew formulas", records.Count);
|
||||
|
||||
// Sort for deterministic output
|
||||
records.Sort();
|
||||
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(records);
|
||||
}
|
||||
|
||||
private void DiscoverFormulas(
|
||||
string cellarPath,
|
||||
List<OSPackageRecord> records,
|
||||
List<string> warnings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Enumerate formula directories (e.g., /usr/local/Cellar/openssl@3)
|
||||
foreach (var formulaDir in EnumerateDirectoriesSafe(cellarPath))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var formulaName = Path.GetFileName(formulaDir);
|
||||
if (string.IsNullOrWhiteSpace(formulaName) || formulaName.StartsWith('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enumerate version directories (e.g., /usr/local/Cellar/openssl@3/3.1.0)
|
||||
foreach (var versionDir in EnumerateDirectoriesSafe(formulaDir))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var versionName = Path.GetFileName(versionDir);
|
||||
if (string.IsNullOrWhiteSpace(versionName) || versionName.StartsWith('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check size guardrail
|
||||
if (!CheckFormulaSizeGuardrail(versionDir, out var sizeWarning))
|
||||
{
|
||||
warnings.Add(sizeWarning!);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look for INSTALL_RECEIPT.json
|
||||
var receiptPath = Path.Combine(versionDir, "INSTALL_RECEIPT.json");
|
||||
if (File.Exists(receiptPath))
|
||||
{
|
||||
var record = ParseReceiptAndCreateRecord(receiptPath, formulaName, versionName, versionDir);
|
||||
if (record is not null)
|
||||
{
|
||||
records.Add(record);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: create record from directory structure
|
||||
var record = CreateRecordFromDirectory(formulaName, versionName, versionDir);
|
||||
if (record is not null)
|
||||
{
|
||||
records.Add(record);
|
||||
warnings.Add($"No INSTALL_RECEIPT.json for {formulaName}@{versionName}; using directory-based discovery.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OSPackageRecord? ParseReceiptAndCreateRecord(
|
||||
string receiptPath,
|
||||
string formulaName,
|
||||
string versionFromDir,
|
||||
string versionDir)
|
||||
{
|
||||
var receipt = _parser.Parse(receiptPath);
|
||||
if (receipt is null)
|
||||
{
|
||||
Logger.LogWarning("Failed to parse INSTALL_RECEIPT.json at {Path}", receiptPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use receipt version if available, fallback to directory name
|
||||
var version = !string.IsNullOrWhiteSpace(receipt.Version) ? receipt.Version : versionFromDir;
|
||||
|
||||
// Build PURL per spec: pkg:brew/<tap>/<formula>@<version>?revision=<revision>
|
||||
var purl = PackageUrlBuilder.BuildHomebrew(
|
||||
receipt.Tap ?? "homebrew/core",
|
||||
receipt.Name ?? formulaName,
|
||||
version,
|
||||
receipt.Revision);
|
||||
|
||||
var vendorMetadata = BuildVendorMetadata(receipt, versionDir);
|
||||
var files = DiscoverFormulaFiles(versionDir);
|
||||
|
||||
return new OSPackageRecord(
|
||||
AnalyzerId,
|
||||
purl,
|
||||
receipt.Name ?? formulaName,
|
||||
version,
|
||||
receipt.Architecture,
|
||||
PackageEvidenceSource.HomebrewCellar,
|
||||
epoch: null,
|
||||
release: receipt.Revision > 0 ? receipt.Revision.ToString() : null,
|
||||
sourcePackage: receipt.Tap,
|
||||
license: receipt.License,
|
||||
cveHints: null,
|
||||
provides: null,
|
||||
depends: receipt.RuntimeDependencies,
|
||||
files: files,
|
||||
vendorMetadata: vendorMetadata);
|
||||
}
|
||||
|
||||
private OSPackageRecord? CreateRecordFromDirectory(
|
||||
string formulaName,
|
||||
string version,
|
||||
string versionDir)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(formulaName) || string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var purl = PackageUrlBuilder.BuildHomebrew("homebrew/core", formulaName, version, revision: 0);
|
||||
var architecture = DetectArchitectureFromPath(versionDir);
|
||||
var files = DiscoverFormulaFiles(versionDir);
|
||||
|
||||
var vendorMetadata = new Dictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["brew:discovery_method"] = "directory",
|
||||
["brew:install_path"] = versionDir,
|
||||
};
|
||||
|
||||
return new OSPackageRecord(
|
||||
AnalyzerId,
|
||||
purl,
|
||||
formulaName,
|
||||
version,
|
||||
architecture,
|
||||
PackageEvidenceSource.HomebrewCellar,
|
||||
epoch: null,
|
||||
release: null,
|
||||
sourcePackage: "homebrew/core",
|
||||
license: null,
|
||||
cveHints: null,
|
||||
provides: null,
|
||||
depends: null,
|
||||
files: files,
|
||||
vendorMetadata: vendorMetadata);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> BuildVendorMetadata(HomebrewReceipt receipt, string versionDir)
|
||||
{
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["brew:tap"] = receipt.Tap,
|
||||
["brew:poured_from_bottle"] = receipt.PouredFromBottle.ToString().ToLowerInvariant(),
|
||||
["brew:installed_as_dependency"] = receipt.InstalledAsDependency.ToString().ToLowerInvariant(),
|
||||
["brew:installed_on_request"] = receipt.InstalledOnRequest.ToString().ToLowerInvariant(),
|
||||
["brew:install_path"] = versionDir,
|
||||
};
|
||||
|
||||
if (receipt.InstallTime.HasValue)
|
||||
{
|
||||
var installTime = DateTimeOffset.FromUnixTimeSeconds(receipt.InstallTime.Value);
|
||||
metadata["brew:install_time"] = installTime.ToString("o");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(receipt.Description))
|
||||
{
|
||||
metadata["description"] = receipt.Description;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(receipt.Homepage))
|
||||
{
|
||||
metadata["homepage"] = receipt.Homepage;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(receipt.SourceUrl))
|
||||
{
|
||||
metadata["brew:source_url"] = receipt.SourceUrl;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(receipt.SourceChecksum))
|
||||
{
|
||||
metadata["brew:source_checksum"] = receipt.SourceChecksum;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(receipt.BottleChecksum))
|
||||
{
|
||||
metadata["brew:bottle_checksum"] = receipt.BottleChecksum;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private List<OSPackageFileEvidence> DiscoverFormulaFiles(string versionDir)
|
||||
{
|
||||
var files = new List<OSPackageFileEvidence>();
|
||||
|
||||
try
|
||||
{
|
||||
// Only discover key files to avoid excessive enumeration
|
||||
// Focus on bin/, lib/, include/, share/man directories
|
||||
var keyDirs = new[] { "bin", "lib", "include", "sbin" };
|
||||
|
||||
foreach (var keyDir in keyDirs)
|
||||
{
|
||||
var keyPath = Path.Combine(versionDir, keyDir);
|
||||
if (!Directory.Exists(keyPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(keyPath, "*", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(versionDir, file);
|
||||
files.Add(new OSPackageFileEvidence(
|
||||
relativePath,
|
||||
layerDigest: null,
|
||||
sha256: null,
|
||||
sizeBytes: null,
|
||||
isConfigFile: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Ignore file enumeration errors
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static bool CheckFormulaSizeGuardrail(string versionDir, out string? warning)
|
||||
{
|
||||
warning = null;
|
||||
|
||||
try
|
||||
{
|
||||
long totalSize = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(versionDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var info = new FileInfo(file);
|
||||
totalSize += info.Length;
|
||||
|
||||
if (totalSize > MaxFormulaSizeBytes)
|
||||
{
|
||||
warning = $"Formula at {versionDir} exceeds {MaxFormulaSizeBytes / (1024 * 1024)}MB size limit; skipping.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Allow if we can't determine size
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string DetectArchitectureFromPath(string path)
|
||||
{
|
||||
// /opt/homebrew is Apple Silicon (arm64)
|
||||
// /usr/local is Intel (x86_64)
|
||||
if (path.Contains("/opt/homebrew/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "arm64";
|
||||
}
|
||||
|
||||
return "x86_64";
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateDirectoriesSafe(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateDirectories(path);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.OS.Homebrew;
|
||||
|
||||
/// <summary>
|
||||
/// Parses Homebrew INSTALL_RECEIPT.json files to extract formula metadata.
|
||||
/// </summary>
|
||||
internal sealed class HomebrewReceiptParser
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Homebrew INSTALL_RECEIPT.json stream.
|
||||
/// </summary>
|
||||
public HomebrewReceipt? Parse(Stream stream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
try
|
||||
{
|
||||
var rawReceipt = JsonSerializer.Deserialize<RawHomebrewReceipt>(stream, SerializerOptions);
|
||||
if (rawReceipt is null || string.IsNullOrWhiteSpace(rawReceipt.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HomebrewReceipt(
|
||||
Name: rawReceipt.Name.Trim(),
|
||||
Version: rawReceipt.Versions?.Stable?.Trim() ?? rawReceipt.Version?.Trim() ?? string.Empty,
|
||||
Revision: rawReceipt.Revision ?? 0,
|
||||
Tap: rawReceipt.TappedFrom?.Trim() ?? rawReceipt.Tap?.Trim() ?? "homebrew/core",
|
||||
PouredFromBottle: rawReceipt.PouredFromBottle ?? false,
|
||||
InstallTime: rawReceipt.InstallTime ?? rawReceipt.Time,
|
||||
InstalledAsDependency: rawReceipt.InstalledAsDependency ?? false,
|
||||
InstalledOnRequest: rawReceipt.InstalledOnRequest ?? true,
|
||||
RuntimeDependencies: ExtractDependencies(rawReceipt.RuntimeDependencies),
|
||||
BuildDependencies: ExtractDependencies(rawReceipt.BuildDependencies),
|
||||
SourceUrl: rawReceipt.Source?.Url?.Trim(),
|
||||
SourceChecksum: rawReceipt.Source?.Checksum?.Trim(),
|
||||
BottleChecksum: rawReceipt.BottleChecksum?.Trim(),
|
||||
Description: rawReceipt.Description?.Trim(),
|
||||
Homepage: rawReceipt.Homepage?.Trim(),
|
||||
License: rawReceipt.License?.Trim(),
|
||||
Architecture: NormalizeArchitecture(rawReceipt.TabJson?.Arch ?? rawReceipt.Arch),
|
||||
TabPath: rawReceipt.TabPath?.Trim());
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Homebrew INSTALL_RECEIPT.json from a file path.
|
||||
/// </summary>
|
||||
public HomebrewReceipt? Parse(string receiptPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(receiptPath);
|
||||
|
||||
if (!File.Exists(receiptPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(receiptPath);
|
||||
return Parse(stream, cancellationToken);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ExtractDependencies(RawDependency[]? dependencies)
|
||||
{
|
||||
if (dependencies is null or { Length: 0 })
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var result = new List<string>(dependencies.Length);
|
||||
foreach (var dep in dependencies)
|
||||
{
|
||||
var name = dep.FullName?.Trim() ?? dep.Name?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
result.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
result.Sort(StringComparer.Ordinal);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeArchitecture(string? arch)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(arch))
|
||||
{
|
||||
return "x86_64"; // Default for Intel Macs
|
||||
}
|
||||
|
||||
var normalized = arch.Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"arm64" or "aarch64" => "arm64",
|
||||
"x86_64" or "amd64" or "x64" => "x86_64",
|
||||
_ => normalized,
|
||||
};
|
||||
}
|
||||
|
||||
// Raw JSON models for deserialization
|
||||
private sealed class RawHomebrewReceipt
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[JsonPropertyName("versions")]
|
||||
public RawVersions? Versions { get; set; }
|
||||
|
||||
[JsonPropertyName("revision")]
|
||||
public int? Revision { get; set; }
|
||||
|
||||
[JsonPropertyName("tap")]
|
||||
public string? Tap { get; set; }
|
||||
|
||||
[JsonPropertyName("tapped_from")]
|
||||
public string? TappedFrom { get; set; }
|
||||
|
||||
[JsonPropertyName("poured_from_bottle")]
|
||||
public bool? PouredFromBottle { get; set; }
|
||||
|
||||
[JsonPropertyName("time")]
|
||||
public long? Time { get; set; }
|
||||
|
||||
[JsonPropertyName("install_time")]
|
||||
public long? InstallTime { get; set; }
|
||||
|
||||
[JsonPropertyName("installed_as_dependency")]
|
||||
public bool? InstalledAsDependency { get; set; }
|
||||
|
||||
[JsonPropertyName("installed_on_request")]
|
||||
public bool? InstalledOnRequest { get; set; }
|
||||
|
||||
[JsonPropertyName("runtime_dependencies")]
|
||||
public RawDependency[]? RuntimeDependencies { get; set; }
|
||||
|
||||
[JsonPropertyName("build_dependencies")]
|
||||
public RawDependency[]? BuildDependencies { get; set; }
|
||||
|
||||
[JsonPropertyName("source")]
|
||||
public RawSource? Source { get; set; }
|
||||
|
||||
[JsonPropertyName("bottle_checksum")]
|
||||
public string? BottleChecksum { get; set; }
|
||||
|
||||
[JsonPropertyName("desc")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[JsonPropertyName("homepage")]
|
||||
public string? Homepage { get; set; }
|
||||
|
||||
[JsonPropertyName("license")]
|
||||
public string? License { get; set; }
|
||||
|
||||
[JsonPropertyName("arch")]
|
||||
public string? Arch { get; set; }
|
||||
|
||||
[JsonPropertyName("HOMEBREW_INSTALL_PATH")]
|
||||
public string? TabPath { get; set; }
|
||||
|
||||
[JsonPropertyName("tab")]
|
||||
public RawTab? TabJson { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawVersions
|
||||
{
|
||||
[JsonPropertyName("stable")]
|
||||
public string? Stable { get; set; }
|
||||
|
||||
[JsonPropertyName("head")]
|
||||
public string? Head { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawDependency
|
||||
{
|
||||
[JsonPropertyName("full_name")]
|
||||
public string? FullName { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawSource
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonPropertyName("checksum")]
|
||||
public string? Checksum { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RawTab
|
||||
{
|
||||
[JsonPropertyName("arch")]
|
||||
public string? Arch { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents parsed Homebrew formula receipt metadata.
|
||||
/// </summary>
|
||||
internal sealed record HomebrewReceipt(
|
||||
string Name,
|
||||
string Version,
|
||||
int Revision,
|
||||
string Tap,
|
||||
bool PouredFromBottle,
|
||||
long? InstallTime,
|
||||
bool InstalledAsDependency,
|
||||
bool InstalledOnRequest,
|
||||
IReadOnlyList<string> RuntimeDependencies,
|
||||
IReadOnlyList<string> BuildDependencies,
|
||||
string? SourceUrl,
|
||||
string? SourceChecksum,
|
||||
string? BottleChecksum,
|
||||
string? Description,
|
||||
string? Homepage,
|
||||
string? License,
|
||||
string Architecture,
|
||||
string? TabPath);
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Scanner.Analyzers.OS.Homebrew.Tests")]
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.OS\StellaOps.Scanner.Analyzers.OS.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user