Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_4300_0002_0001
|
||||
// Task: Evidence Privacy Controls - Evidence model definitions
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle of evidence for a finding.
|
||||
/// </summary>
|
||||
public sealed record EvidenceBundle
|
||||
{
|
||||
/// <summary>
|
||||
/// Reachability analysis evidence.
|
||||
/// </summary>
|
||||
public ReachabilityEvidence? Reachability { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Call stack evidence (runtime or static analysis).
|
||||
/// </summary>
|
||||
public CallStackEvidence? CallStack { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Provenance/build evidence.
|
||||
/// </summary>
|
||||
public ProvenanceEvidence? Provenance { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX statements.
|
||||
/// </summary>
|
||||
public VexEvidence? Vex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// EPSS evidence.
|
||||
/// </summary>
|
||||
public EpssEvidence? Epss { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reachability analysis evidence.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Reachability result.
|
||||
/// </summary>
|
||||
public required string Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score [0,1].
|
||||
/// </summary>
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paths from entrypoints to vulnerable code.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ReachabilityPath> Paths { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of paths (preserved in minimal redaction).
|
||||
/// </summary>
|
||||
public int PathCount => Paths.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the call graph used.
|
||||
/// </summary>
|
||||
public required string GraphDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A path from an entrypoint to vulnerable code.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique path identifier.
|
||||
/// </summary>
|
||||
public required string PathId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Steps in the path.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ReachabilityStep> Steps { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A step in a reachability path.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Node identifier (function/method name).
|
||||
/// </summary>
|
||||
public required string Node { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the file containing this code.
|
||||
/// </summary>
|
||||
public required string FileHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Line range [start, end].
|
||||
/// </summary>
|
||||
public required int[] Lines { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw source code (null when redacted).
|
||||
/// </summary>
|
||||
public string? SourceCode { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call stack evidence.
|
||||
/// </summary>
|
||||
public sealed record CallStackEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Stack frames.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<CallFrame> Frames { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stack trace digest.
|
||||
/// </summary>
|
||||
public string? StackDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A frame in a call stack.
|
||||
/// </summary>
|
||||
public sealed record CallFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// Function/method name.
|
||||
/// </summary>
|
||||
public required string Function { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the file.
|
||||
/// </summary>
|
||||
public required string FileHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Line number.
|
||||
/// </summary>
|
||||
public required int Line { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Function arguments (null when redacted).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Arguments { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Local variables (null when redacted).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Locals { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provenance/build evidence.
|
||||
/// </summary>
|
||||
public sealed record ProvenanceEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Build identifier.
|
||||
/// </summary>
|
||||
public required string BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Build digest.
|
||||
/// </summary>
|
||||
public required string BuildDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether provenance was verified.
|
||||
/// </summary>
|
||||
public required bool Verified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata (null when redacted).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX evidence.
|
||||
/// </summary>
|
||||
public sealed record VexEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// VEX status.
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for not_affected status.
|
||||
/// </summary>
|
||||
public string? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Impact statement.
|
||||
/// </summary>
|
||||
public string? ImpactStatement { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Action statement.
|
||||
/// </summary>
|
||||
public string? ActionStatement { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the VEX statement.
|
||||
/// </summary>
|
||||
public DateTimeOffset? Timestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EPSS evidence.
|
||||
/// </summary>
|
||||
public sealed record EpssEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// EPSS probability score [0,1].
|
||||
/// </summary>
|
||||
public required double Score { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// EPSS percentile rank [0,1].
|
||||
/// </summary>
|
||||
public required double Percentile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Model date.
|
||||
/// </summary>
|
||||
public required DateOnly ModelDate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this evidence was captured.
|
||||
/// </summary>
|
||||
public required DateTimeOffset CapturedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_4300_0002_0001
|
||||
// Task: T1 - Define Redaction Levels
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Privacy;
|
||||
|
||||
/// <summary>
|
||||
/// Redaction levels for evidence data.
|
||||
/// </summary>
|
||||
public enum EvidenceRedactionLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Full evidence including raw source code.
|
||||
/// Requires elevated permissions.
|
||||
/// </summary>
|
||||
Full = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Standard redaction: file hashes, symbol names, line ranges.
|
||||
/// No raw source code.
|
||||
/// </summary>
|
||||
Standard = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Minimal: only digests and counts.
|
||||
/// For external sharing.
|
||||
/// </summary>
|
||||
Minimal = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fields that can be redacted.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum RedactableFields
|
||||
{
|
||||
None = 0,
|
||||
SourceCode = 1 << 0,
|
||||
FilePaths = 1 << 1,
|
||||
LineNumbers = 1 << 2,
|
||||
SymbolNames = 1 << 3,
|
||||
CallArguments = 1 << 4,
|
||||
EnvironmentVars = 1 << 5,
|
||||
InternalUrls = 1 << 6,
|
||||
All = SourceCode | FilePaths | LineNumbers | SymbolNames | CallArguments | EnvironmentVars | InternalUrls
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_4300_0002_0001
|
||||
// Task: T2 - Implement EvidenceRedactionService
|
||||
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Evidence.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Evidence.Privacy;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for redacting evidence based on privacy rules.
|
||||
/// </summary>
|
||||
public interface IEvidenceRedactionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Redacts evidence based on the specified level.
|
||||
/// </summary>
|
||||
EvidenceBundle Redact(EvidenceBundle bundle, EvidenceRedactionLevel level);
|
||||
|
||||
/// <summary>
|
||||
/// Redacts specific fields from evidence.
|
||||
/// </summary>
|
||||
EvidenceBundle RedactFields(EvidenceBundle bundle, RedactableFields fields);
|
||||
|
||||
/// <summary>
|
||||
/// Determines the appropriate redaction level for a user.
|
||||
/// </summary>
|
||||
EvidenceRedactionLevel DetermineLevel(ClaimsPrincipal user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for redacting evidence based on privacy rules.
|
||||
/// </summary>
|
||||
public sealed class EvidenceRedactionService : IEvidenceRedactionService
|
||||
{
|
||||
private readonly ILogger<EvidenceRedactionService> _logger;
|
||||
|
||||
public EvidenceRedactionService(ILogger<EvidenceRedactionService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redacts evidence based on the specified level.
|
||||
/// </summary>
|
||||
public EvidenceBundle Redact(EvidenceBundle bundle, EvidenceRedactionLevel level)
|
||||
{
|
||||
_logger.LogDebug("Redacting evidence to level {Level}", level);
|
||||
|
||||
return level switch
|
||||
{
|
||||
EvidenceRedactionLevel.Full => bundle,
|
||||
EvidenceRedactionLevel.Standard => RedactStandard(bundle),
|
||||
EvidenceRedactionLevel.Minimal => RedactMinimal(bundle),
|
||||
_ => RedactStandard(bundle)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redacts specific fields from evidence.
|
||||
/// </summary>
|
||||
public EvidenceBundle RedactFields(EvidenceBundle bundle, RedactableFields fields)
|
||||
{
|
||||
if (fields == RedactableFields.None)
|
||||
{
|
||||
return bundle;
|
||||
}
|
||||
|
||||
var result = bundle;
|
||||
|
||||
if (fields.HasFlag(RedactableFields.SourceCode))
|
||||
{
|
||||
result = result with
|
||||
{
|
||||
Reachability = result.Reachability is not null
|
||||
? RedactSourceCodeFromReachability(result.Reachability)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
if (fields.HasFlag(RedactableFields.CallArguments))
|
||||
{
|
||||
result = result with
|
||||
{
|
||||
CallStack = result.CallStack is not null
|
||||
? RedactCallStackArguments(result.CallStack)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the appropriate redaction level for a user.
|
||||
/// </summary>
|
||||
public EvidenceRedactionLevel DetermineLevel(ClaimsPrincipal user)
|
||||
{
|
||||
if (user.HasClaim("scope", "evidence:full") ||
|
||||
user.HasClaim("role", "security_admin"))
|
||||
{
|
||||
_logger.LogDebug("User has full evidence access");
|
||||
return EvidenceRedactionLevel.Full;
|
||||
}
|
||||
|
||||
if (user.HasClaim("scope", "evidence:standard") ||
|
||||
user.HasClaim("role", "security_analyst"))
|
||||
{
|
||||
_logger.LogDebug("User has standard evidence access");
|
||||
return EvidenceRedactionLevel.Standard;
|
||||
}
|
||||
|
||||
_logger.LogDebug("User has minimal evidence access (default)");
|
||||
return EvidenceRedactionLevel.Minimal;
|
||||
}
|
||||
|
||||
private EvidenceBundle RedactStandard(EvidenceBundle bundle)
|
||||
{
|
||||
return bundle with
|
||||
{
|
||||
Reachability = bundle.Reachability is not null
|
||||
? RedactReachability(bundle.Reachability)
|
||||
: null,
|
||||
CallStack = bundle.CallStack is not null
|
||||
? RedactCallStack(bundle.CallStack)
|
||||
: null,
|
||||
Provenance = bundle.Provenance // Keep as-is (already redacted at standard level)
|
||||
};
|
||||
}
|
||||
|
||||
private ReachabilityEvidence RedactReachability(ReachabilityEvidence evidence)
|
||||
{
|
||||
return evidence with
|
||||
{
|
||||
Paths = evidence.Paths.Select(p => new ReachabilityPath
|
||||
{
|
||||
PathId = p.PathId,
|
||||
Steps = p.Steps.Select(s => new ReachabilityStep
|
||||
{
|
||||
Node = RedactSymbol(s.Node),
|
||||
FileHash = s.FileHash, // Keep hash
|
||||
Lines = s.Lines, // Keep line range
|
||||
SourceCode = null // Redact source
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private CallStackEvidence RedactCallStack(CallStackEvidence evidence)
|
||||
{
|
||||
return evidence with
|
||||
{
|
||||
Frames = evidence.Frames.Select(f => new CallFrame
|
||||
{
|
||||
Function = RedactSymbol(f.Function),
|
||||
FileHash = f.FileHash,
|
||||
Line = f.Line,
|
||||
Arguments = null, // Redact arguments
|
||||
Locals = null // Redact locals
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private string RedactSymbol(string symbol)
|
||||
{
|
||||
// Keep class and method names, redact arguments
|
||||
// "MyClass.MyMethod(string arg1, int arg2)" -> "MyClass.MyMethod(...)"
|
||||
var parenIndex = symbol.IndexOf('(');
|
||||
if (parenIndex > 0)
|
||||
{
|
||||
return symbol[..parenIndex] + "(...)";
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
private EvidenceBundle RedactMinimal(EvidenceBundle bundle)
|
||||
{
|
||||
return bundle with
|
||||
{
|
||||
Reachability = bundle.Reachability is not null
|
||||
? new ReachabilityEvidence
|
||||
{
|
||||
Result = bundle.Reachability.Result,
|
||||
Confidence = bundle.Reachability.Confidence,
|
||||
Paths = [], // No paths
|
||||
GraphDigest = bundle.Reachability.GraphDigest
|
||||
}
|
||||
: null,
|
||||
CallStack = null, // Remove entirely
|
||||
Provenance = bundle.Provenance is not null
|
||||
? new ProvenanceEvidence
|
||||
{
|
||||
BuildId = bundle.Provenance.BuildId,
|
||||
BuildDigest = bundle.Provenance.BuildDigest,
|
||||
Verified = bundle.Provenance.Verified
|
||||
}
|
||||
: null,
|
||||
Vex = bundle.Vex, // Keep VEX (public data)
|
||||
Epss = bundle.Epss // Keep EPSS (public data)
|
||||
};
|
||||
}
|
||||
|
||||
private ReachabilityEvidence RedactSourceCodeFromReachability(ReachabilityEvidence evidence)
|
||||
{
|
||||
return evidence with
|
||||
{
|
||||
Paths = evidence.Paths.Select(p => new ReachabilityPath
|
||||
{
|
||||
PathId = p.PathId,
|
||||
Steps = p.Steps.Select(s => s with { SourceCode = null }).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private CallStackEvidence RedactCallStackArguments(CallStackEvidence evidence)
|
||||
{
|
||||
return evidence with
|
||||
{
|
||||
Frames = evidence.Frames.Select(f => f with
|
||||
{
|
||||
Arguments = null,
|
||||
Locals = null
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Auth.Security/StellaOps.Auth.Security.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user