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.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user