Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Emit/Lineage/SbomDiffEngine.cs
StellaOps Bot 5146204f1b feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
2025-12-22 23:21:21 +02:00

196 lines
6.0 KiB
C#

using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Scanner.Emit.Lineage;
/// <summary>
/// Engine for computing semantic diffs between SBOM versions.
/// </summary>
public sealed class SbomDiffEngine
{
/// <summary>
/// Computes the semantic diff between two SBOMs.
/// </summary>
public SbomDiff ComputeDiff(
SbomId fromId,
IReadOnlyList<ComponentRef> fromComponents,
SbomId toId,
IReadOnlyList<ComponentRef> toComponents)
{
var fromByPurl = fromComponents.ToDictionary(c => c.Purl, c => c);
var toByPurl = toComponents.ToDictionary(c => c.Purl, c => c);
var deltas = new List<ComponentDelta>();
var added = 0;
var removed = 0;
var versionChanged = 0;
var otherModified = 0;
var unchanged = 0;
var isBreaking = false;
// Find added and modified components
foreach (var (purl, toComp) in toByPurl)
{
if (!fromByPurl.TryGetValue(purl, out var fromComp))
{
// Added
deltas.Add(new ComponentDelta
{
Type = ComponentDeltaType.Added,
After = toComp
});
added++;
}
else
{
// Possibly modified
var changedFields = CompareComponents(fromComp, toComp);
if (changedFields.Length > 0)
{
var deltaType = changedFields.Contains("Version")
? ComponentDeltaType.VersionChanged
: changedFields.Contains("License")
? ComponentDeltaType.LicenseChanged
: ComponentDeltaType.MetadataChanged;
deltas.Add(new ComponentDelta
{
Type = deltaType,
Before = fromComp,
After = toComp,
ChangedFields = changedFields
});
if (deltaType == ComponentDeltaType.VersionChanged)
versionChanged++;
else
otherModified++;
// Check for version downgrade (breaking)
if (changedFields.Contains("Version") && IsVersionDowngrade(fromComp.Version, toComp.Version))
isBreaking = true;
}
else
{
unchanged++;
}
}
}
// Find removed components
foreach (var (purl, fromComp) in fromByPurl)
{
if (!toByPurl.ContainsKey(purl))
{
deltas.Add(new ComponentDelta
{
Type = ComponentDeltaType.Removed,
Before = fromComp
});
removed++;
isBreaking = true;
}
}
// Sort deltas for determinism
var sortedDeltas = deltas
.OrderBy(d => d.Type)
.ThenBy(d => d.Before?.Purl ?? d.After?.Purl)
.ToImmutableArray();
return new SbomDiff
{
FromId = fromId,
ToId = toId,
Deltas = sortedDeltas,
Summary = new DiffSummary
{
Added = added,
Removed = removed,
VersionChanged = versionChanged,
OtherModified = otherModified,
Unchanged = unchanged,
IsBreaking = isBreaking
},
ComputedAt = DateTimeOffset.UtcNow
};
}
/// <summary>
/// Creates a diff pointer from a diff.
/// </summary>
public SbomDiffPointer CreatePointer(SbomDiff diff)
{
var hash = ComputeDiffHash(diff);
return new SbomDiffPointer
{
ComponentsAdded = diff.Summary.Added,
ComponentsRemoved = diff.Summary.Removed,
ComponentsModified = diff.Summary.VersionChanged + diff.Summary.OtherModified,
DiffHash = hash
};
}
private static ImmutableArray<string> CompareComponents(ComponentRef from, ComponentRef to)
{
var changes = new List<string>();
if (from.Version != to.Version)
changes.Add("Version");
if (from.License != to.License)
changes.Add("License");
if (from.Type != to.Type)
changes.Add("Type");
return [.. changes];
}
private static bool IsVersionDowngrade(string fromVersion, string toVersion)
{
// Simple semver-like comparison
// In production, use proper version comparison per ecosystem
try
{
var fromParts = fromVersion.Split('.').Select(int.Parse).ToArray();
var toParts = toVersion.Split('.').Select(int.Parse).ToArray();
for (var i = 0; i < Math.Min(fromParts.Length, toParts.Length); i++)
{
if (toParts[i] < fromParts[i]) return true;
if (toParts[i] > fromParts[i]) return false;
}
return toParts.Length < fromParts.Length;
}
catch
{
// Fall back to string comparison
return string.Compare(toVersion, fromVersion, StringComparison.Ordinal) < 0;
}
}
private static string ComputeDiffHash(SbomDiff diff)
{
var json = JsonSerializer.Serialize(new
{
diff.FromId,
diff.ToId,
Deltas = diff.Deltas.Select(d => new
{
d.Type,
BeforePurl = d.Before?.Purl,
AfterPurl = d.After?.Purl,
d.ChangedFields
})
}, new JsonSerializerOptions { WriteIndented = false });
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return Convert.ToHexStringLower(hashBytes);
}
}