- 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.
196 lines
6.0 KiB
C#
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);
|
|
}
|
|
}
|