Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management. - Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management. - Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support. - Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
// =============================================================================
|
||||
// BundleVerificationTests.cs
|
||||
// Sprint: SPRINT_3603_0001_0001
|
||||
// Task: 11 - Unit tests for verification
|
||||
// =============================================================================
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Moq;
|
||||
using StellaOps.ExportCenter.Core.OfflineBundle;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ExportCenter.Tests.OfflineBundle;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "3603")]
|
||||
public sealed class BundleVerificationTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly Mock<ILogger<OfflineBundlePackager>> _loggerMock;
|
||||
private readonly OfflineBundlePackager _packager;
|
||||
private readonly List<string> _tempFiles = new();
|
||||
|
||||
public BundleVerificationTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 12, 15, 10, 0, 0, TimeSpan.Zero));
|
||||
_loggerMock = new Mock<ILogger<OfflineBundlePackager>>();
|
||||
_packager = new OfflineBundlePackager(_timeProvider, _loggerMock.Object);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var file in _tempFiles.Where(File.Exists))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "VerifyBundleAsync validates correct hash")]
|
||||
public async Task VerifyBundleAsync_ValidHash_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-verify-1",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
|
||||
// Act
|
||||
var verification = await _packager.VerifyBundleAsync(
|
||||
result.BundlePath!,
|
||||
result.ManifestHash!);
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeTrue();
|
||||
verification.HashValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "VerifyBundleAsync rejects incorrect hash")]
|
||||
public async Task VerifyBundleAsync_IncorrectHash_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-verify-2",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
|
||||
// Act
|
||||
var verification = await _packager.VerifyBundleAsync(
|
||||
result.BundlePath!,
|
||||
"sha256:wrong_hash_value");
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeFalse();
|
||||
verification.HashValid.Should().BeFalse();
|
||||
verification.Errors.Should().Contain(e => e.Contains("hash"));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "VerifyBundleAsync rejects tampered bundle")]
|
||||
public async Task VerifyBundleAsync_TamperedBundle_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-verify-3",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
|
||||
// Tamper with the bundle
|
||||
var bytes = await File.ReadAllBytesAsync(result.BundlePath!);
|
||||
bytes[bytes.Length / 2] ^= 0xFF; // Flip some bits
|
||||
var tamperedPath = result.BundlePath!.Replace(".tgz", ".tampered.tgz");
|
||||
await File.WriteAllBytesAsync(tamperedPath, bytes);
|
||||
_tempFiles.Add(tamperedPath);
|
||||
|
||||
// Act
|
||||
var verification = await _packager.VerifyBundleAsync(
|
||||
tamperedPath,
|
||||
result.ManifestHash!);
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "VerifyBundleAsync rejects non-existent file")]
|
||||
public async Task VerifyBundleAsync_NonExistentFile_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var verification = await _packager.VerifyBundleAsync(
|
||||
"/non/existent/path.tgz",
|
||||
"sha256:abc123");
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeFalse();
|
||||
verification.Errors.Should().Contain(e => e.Contains("not found") || e.Contains("exist"));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "VerifyBundleAsync validates manifest entries")]
|
||||
public async Task VerifyBundleAsync_ValidatesManifestEntries()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-verify-4",
|
||||
ActorId = "user@test.com",
|
||||
IncludeVexHistory = true,
|
||||
IncludeSbomSlice = true
|
||||
};
|
||||
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
|
||||
// Act
|
||||
var verification = await _packager.VerifyBundleAsync(
|
||||
result.BundlePath!,
|
||||
result.ManifestHash!);
|
||||
|
||||
// Assert
|
||||
verification.IsValid.Should().BeTrue();
|
||||
verification.ChainValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "VerifyBundleAsync provides detailed verification result")]
|
||||
public async Task VerifyBundleAsync_ProvidesDetailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-verify-5",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
|
||||
// Act
|
||||
var verification = await _packager.VerifyBundleAsync(
|
||||
result.BundlePath!,
|
||||
result.ManifestHash!);
|
||||
|
||||
// Assert
|
||||
verification.Should().NotBeNull();
|
||||
verification.IsValid.Should().BeTrue();
|
||||
verification.HashValid.Should().BeTrue();
|
||||
verification.ChainValid.Should().BeTrue();
|
||||
verification.VerifiedAt.Should().BeCloseTo(
|
||||
_timeProvider.GetUtcNow(),
|
||||
TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Hash computation is deterministic")]
|
||||
public void HashComputation_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content for hashing";
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
// Act
|
||||
var hash1 = ComputeHash(bytes);
|
||||
var hash2 = ComputeHash(bytes);
|
||||
|
||||
// Assert
|
||||
hash1.Should().Be(hash2);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Hash format follows sha256: prefix")]
|
||||
public void HashFormat_FollowsSha256Prefix()
|
||||
{
|
||||
// Arrange
|
||||
var content = "test content";
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
// Act
|
||||
var hash = ComputeHash(bytes);
|
||||
|
||||
// Assert
|
||||
hash.Should().StartWith("sha256:");
|
||||
hash.Should().HaveLength(71); // "sha256:" + 64 hex chars
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] content)
|
||||
{
|
||||
var hashBytes = SHA256.HashData(content);
|
||||
return $"sha256:{Convert.ToHexString(hashBytes).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// =============================================================================
|
||||
// OfflineBundlePackagerTests.cs
|
||||
// Sprint: SPRINT_3603_0001_0001
|
||||
// Task: 10 - Unit tests for packaging
|
||||
// =============================================================================
|
||||
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Moq;
|
||||
using StellaOps.ExportCenter.Core.OfflineBundle;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.ExportCenter.Tests.OfflineBundle;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Sprint", "3603")]
|
||||
public sealed class OfflineBundlePackagerTests : IDisposable
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly Mock<ILogger<OfflineBundlePackager>> _loggerMock;
|
||||
private readonly OfflineBundlePackager _packager;
|
||||
private readonly List<string> _tempFiles = new();
|
||||
|
||||
public OfflineBundlePackagerTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 12, 15, 10, 0, 0, TimeSpan.Zero));
|
||||
_loggerMock = new Mock<ILogger<OfflineBundlePackager>>();
|
||||
_packager = new OfflineBundlePackager(_timeProvider, _loggerMock.Object);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var file in _tempFiles.Where(File.Exists))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync creates valid tarball")]
|
||||
public async Task CreateBundleAsync_CreatesValidTarball()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-123",
|
||||
ActorId = "user@test.com",
|
||||
IncludeVexHistory = true,
|
||||
IncludeSbomSlice = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Success.Should().BeTrue();
|
||||
result.BundleId.Should().NotBeNullOrEmpty();
|
||||
result.Content.Should().NotBeNull();
|
||||
result.Content.Length.Should().BeGreaterThan(0);
|
||||
|
||||
// Verify it's a valid gzip
|
||||
result.Content.Position = 0;
|
||||
using var gzip = new GZipStream(result.Content, CompressionMode.Decompress, leaveOpen: true);
|
||||
var buffer = new byte[2];
|
||||
var read = await gzip.ReadAsync(buffer);
|
||||
read.Should().BeGreaterThan(0);
|
||||
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync includes manifest")]
|
||||
public async Task CreateBundleAsync_IncludesManifest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-456",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ManifestHash.Should().NotBeNullOrEmpty();
|
||||
result.ManifestHash.Should().StartWith("sha256:");
|
||||
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync rejects null request")]
|
||||
public async Task CreateBundleAsync_NullRequest_Throws()
|
||||
{
|
||||
// Act
|
||||
var act = () => _packager.CreateBundleAsync(null!);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync rejects empty alertId")]
|
||||
public async Task CreateBundleAsync_EmptyAlertId_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var act = () => _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync generates unique bundle IDs")]
|
||||
public async Task CreateBundleAsync_GeneratesUniqueBundleIds()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-789",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result1 = await _packager.CreateBundleAsync(request);
|
||||
var result2 = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result1.BundleId.Should().NotBe(result2.BundleId);
|
||||
|
||||
_tempFiles.Add(result1.BundlePath ?? "");
|
||||
_tempFiles.Add(result2.BundlePath ?? "");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync sets correct content type")]
|
||||
public async Task CreateBundleAsync_SetsCorrectContentType()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-content",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result.ContentType.Should().Be("application/gzip");
|
||||
result.FileName.Should().Contain(".stella.bundle.tgz");
|
||||
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync includes metadata directory")]
|
||||
public async Task CreateBundleAsync_IncludesMetadataDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-meta",
|
||||
ActorId = "user@test.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Entries.Should().Contain(e => e.Path.StartsWith("metadata/"));
|
||||
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync with VEX history includes vex directory")]
|
||||
public async Task CreateBundleAsync_WithVexHistory_IncludesVexDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-vex",
|
||||
ActorId = "user@test.com",
|
||||
IncludeVexHistory = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Entries.Should().Contain(e => e.Path.StartsWith("vex/"));
|
||||
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "CreateBundleAsync with SBOM slice includes sbom directory")]
|
||||
public async Task CreateBundleAsync_WithSbomSlice_IncludesSbomDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var request = new BundleRequest
|
||||
{
|
||||
AlertId = "alert-sbom",
|
||||
ActorId = "user@test.com",
|
||||
IncludeSbomSlice = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _packager.CreateBundleAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Entries.Should().Contain(e => e.Path.StartsWith("sbom/"));
|
||||
|
||||
_tempFiles.Add(result.BundlePath ?? "");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user