diff --git a/src/SecTester.Scan/CI/CiDiscovery.cs b/src/SecTester.Scan/CI/CiDiscovery.cs index 20879dd..87a40e0 100644 --- a/src/SecTester.Scan/CI/CiDiscovery.cs +++ b/src/SecTester.Scan/CI/CiDiscovery.cs @@ -4,4 +4,5 @@ public interface CiDiscovery { CiServer? Server { get; } bool IsCi { get; } + bool IsPr { get; } } diff --git a/src/SecTester.Scan/CI/CiServer.cs b/src/SecTester.Scan/CI/CiServer.cs index 326604e..294918e 100644 --- a/src/SecTester.Scan/CI/CiServer.cs +++ b/src/SecTester.Scan/CI/CiServer.cs @@ -2,5 +2,50 @@ namespace SecTester.Scan.CI; public record CiServer(string? ServerName = default) { - public static readonly CiServer? Unknown; + public static CiServer APPCENTER { get; } = new("Visual Studio App Center"); + public static CiServer APPCIRCLE { get; } = new("Appcircle"); + public static CiServer APPVEYOR { get; } = new("AppVeyor"); + public static CiServer AZURE_PIPELINES { get; } = new("Azure Pipelines"); + public static CiServer BAMBOO { get; } = new("Bamboo"); + public static CiServer BITBUCKET { get; } = new("Bitbucket Pipelines"); + public static CiServer BITRISE { get; } = new("Bitrise"); + public static CiServer BUDDY { get; } = new("Buddy"); + public static CiServer BUILDKITE { get; } = new("Buildkite"); + public static CiServer CIRCLE { get; } = new("CircleCI"); + public static CiServer CIRRUS { get; } = new("Cirrus CI"); + public static CiServer CODEBUILD { get; } = new("AWS CodeBuild"); + public static CiServer CODEFRESH { get; } = new("Codefresh"); + public static CiServer CODEMAGIC { get; } = new("Codemagic"); + public static CiServer CODESHIP { get; } = new("Codeship"); + public static CiServer DRONE { get; } = new("Drone"); + public static CiServer DSARI { get; } = new("dsari"); + public static CiServer EAS { get; } = new("Expo Application Services"); + public static CiServer GERRIT { get; } = new("Gerrit"); + public static CiServer GITHUB_ACTIONS { get; } = new("GitHub Actions"); + public static CiServer GITLAB { get; } = new("GitLab CI"); + public static CiServer GOCD { get; } = new("GoCD"); + public static CiServer GOOGLE_CLOUD_BUILD { get; } = new("Google Cloud Build"); + public static CiServer HEROKU { get; } = new("Heroku"); + public static CiServer HUDSON { get; } = new("Hudson"); + public static CiServer JENKINS { get; } = new("Jenkins"); + public static CiServer LAYERCI { get; } = new("LayerCI"); + public static CiServer MAGNUM { get; } = new("Magnum CI"); + public static CiServer NETLIFY { get; } = new("Netlify CI"); + public static CiServer NEVERCODE { get; } = new("Nevercode"); + public static CiServer RELEASEHUB { get; } = new("ReleaseHub"); + public static CiServer RENDER { get; } = new("Render"); + public static CiServer SAIL { get; } = new("Sail CI"); + public static CiServer SCREWDRIVER { get; } = new("Screwdriver"); + public static CiServer SEMAPHORE { get; } = new("Semaphore"); + public static CiServer SHIPPABLE { get; } = new("Shippable"); + public static CiServer SOLANO { get; } = new("Solano CI"); + public static CiServer SOURCEHUT { get; } = new("Sourcehut"); + public static CiServer STRIDER { get; } = new("Strider CD"); + public static CiServer TASKCLUSTER { get; } = new("TaskCluster"); + public static CiServer TEAMCITY { get; } = new("TeamCity"); + public static CiServer TRAVIS { get; } = new("Travis CI"); + public static CiServer VERCEL { get; } = new("Vercel"); + public static CiServer WOODPECKER { get; } = new("Woodpecker"); + public static CiServer XCODE_CLOUD { get; } = new("Xcode Cloud"); + public static CiServer XCODE_SERVER { get; } = new("Xcode Server"); } diff --git a/src/SecTester.Scan/CI/DefaultCiDiscovery.cs b/src/SecTester.Scan/CI/DefaultCiDiscovery.cs index 871e7cf..bcdd2b1 100644 --- a/src/SecTester.Scan/CI/DefaultCiDiscovery.cs +++ b/src/SecTester.Scan/CI/DefaultCiDiscovery.cs @@ -1,9 +1,48 @@ +using System; +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text.Json; + namespace SecTester.Scan.CI; -// TODO (dmitry.osrikov@brightsec.com): rework using 'ci-info' compatible implementation internal class DefaultCiDiscovery : CiDiscovery { public CiServer? Server { get; } public bool IsCi => Server != null; + + public bool IsPr { get; } + + public DefaultCiDiscovery(IDictionary? env = default) + { + env ??= Environment.GetEnvironmentVariables(); + + var vendors = JsonSerializer.Deserialize( + ResourceUtils.GetEmbeddedResourceContent("SecTester.Scan.CI.vendors.json"), + new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + + if (vendors is null) + { + return; + } + + var matcher = new VendorMatcher(env); + + var vendor = vendors.FirstOrDefault(x => matcher.MatchEnvElement(x.Env)); + + if (vendor is null) + { + return; + } + + Server = typeof(CiServer) + .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(x => x.Name.Equals(vendor.Constant, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.GetValue(null)) + .Cast() + .FirstOrDefault() ?? new CiServer(vendor.Name); + + IsPr = matcher.MatchPrElement(vendor.Pr); + } } diff --git a/src/SecTester.Scan/CI/ResourceUtils.cs b/src/SecTester.Scan/CI/ResourceUtils.cs new file mode 100644 index 0000000..fddedce --- /dev/null +++ b/src/SecTester.Scan/CI/ResourceUtils.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Reflection; + +namespace SecTester.Scan.CI; + +internal static class ResourceUtils +{ + public static string GetEmbeddedResourceContent(string resourceName) + where T : class + { + if (string.IsNullOrWhiteSpace(resourceName)) + { + throw new ArgumentNullException(nameof(resourceName)); + } + + var assembly = typeof(T).GetTypeInfo().Assembly; + using var stream = assembly.GetManifestResourceStream(resourceName); + + if (stream is null) + { + throw new InvalidOperationException($"Could not get stream for {resourceName} resource."); + } + + using var reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } +} diff --git a/src/SecTester.Scan/CI/Vendor.cs b/src/SecTester.Scan/CI/Vendor.cs new file mode 100644 index 0000000..91527fe --- /dev/null +++ b/src/SecTester.Scan/CI/Vendor.cs @@ -0,0 +1,9 @@ +using System.Text.Json; + +namespace SecTester.Scan.CI; + +internal record Vendor(string Name, string Constant) +{ + public JsonElement Env { get; init; } + public JsonElement Pr { get; init; } +} diff --git a/src/SecTester.Scan/CI/VendorMatcher.cs b/src/SecTester.Scan/CI/VendorMatcher.cs new file mode 100644 index 0000000..bd73f43 --- /dev/null +++ b/src/SecTester.Scan/CI/VendorMatcher.cs @@ -0,0 +1,88 @@ +using System.Collections; +using System.Linq; +using System.Text.Json; + +namespace SecTester.Scan.CI; + +internal class VendorMatcher +{ + private readonly IDictionary _env; + + public VendorMatcher(IDictionary env) + { + _env = env; + } + + public bool MatchEnvElement(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString() is not null && _env.Contains(element.GetString()!), + JsonValueKind.Object => MatchInnerAny(element) || MatchInnerEnvIncludes(element) || + MatchOwnProperties(element), + JsonValueKind.Array => element.EnumerateArray() + .All(x => x.ValueKind == JsonValueKind.String && x.GetString() is not null && _env.Contains(x.GetString()!)), + _ => false + }; + } + + + public bool MatchPrElement(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString() is not null && _env.Contains(element.GetString()!), + JsonValueKind.Object => MatchInnerEnvNe(element) || MatchEnvElement(element), + _ => false + }; + } + + private bool MatchOwnProperties(JsonElement element) + { + return element.EnumerateObject().All(x => + x.Value.ValueKind == JsonValueKind.String && _env.Contains(x.Name) && + x.Value.ValueEquals(_env[x.Name]?.ToString())); + } + + private bool MatchInnerAny(JsonElement element) + { + var any = element.EnumerateObject().FirstOrDefault(x => x.NameEquals("any")); + + return any.Value.ValueKind == JsonValueKind.Array && any.Value.EnumerateArray() + .Any(x => x.ValueKind == JsonValueKind.String && x.GetString() is not null && _env.Contains(x.GetString()!)); + } + + private bool MatchInnerEnvIncludes(JsonElement element) + { + var env = GetInnerPropertyValue(element, "env"); + var includes = GetInnerPropertyValue(element, "includes"); + + if (env is null || includes is null || !_env.Contains(env)) + { + return false; + } + + var envVarValue = _env[env]?.ToString(); + return envVarValue?.Contains(includes) ?? false; + } + + private bool MatchInnerEnvNe(JsonElement element) + { + var env = GetInnerPropertyValue(element, "env"); + var ne = GetInnerPropertyValue(element, "ne"); + + if (env is null || ne is null || !_env.Contains(env) || ne != "false") + { + return false; + } + + var envVarValue = _env[env]?.ToString(); + return !string.IsNullOrEmpty(envVarValue); + } + + private static string? GetInnerPropertyValue(JsonElement element, string name) + { + var property = element.EnumerateObject().FirstOrDefault(x => x.NameEquals(name)); + return property.Value.ValueKind == JsonValueKind.String ? property.Value.GetString() : default; + } +} diff --git a/src/SecTester.Scan/CI/vendors.json b/src/SecTester.Scan/CI/vendors.json new file mode 100644 index 0000000..00c1601 --- /dev/null +++ b/src/SecTester.Scan/CI/vendors.json @@ -0,0 +1,258 @@ +[ + { + "name": "Appcircle", + "constant": "APPCIRCLE", + "env": "AC_APPCIRCLE" + }, + { + "name": "AppVeyor", + "constant": "APPVEYOR", + "env": "APPVEYOR", + "pr": "APPVEYOR_PULL_REQUEST_NUMBER" + }, + { + "name": "AWS CodeBuild", + "constant": "CODEBUILD", + "env": "CODEBUILD_BUILD_ARN" + }, + { + "name": "Azure Pipelines", + "constant": "AZURE_PIPELINES", + "env": "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", + "pr": "SYSTEM_PULLREQUEST_PULLREQUESTID" + }, + { + "name": "Bamboo", + "constant": "BAMBOO", + "env": "bamboo_planKey" + }, + { + "name": "Bitbucket Pipelines", + "constant": "BITBUCKET", + "env": "BITBUCKET_COMMIT", + "pr": "BITBUCKET_PR_ID" + }, + { + "name": "Bitrise", + "constant": "BITRISE", + "env": "BITRISE_IO", + "pr": "BITRISE_PULL_REQUEST" + }, + { + "name": "Buddy", + "constant": "BUDDY", + "env": "BUDDY_WORKSPACE_ID", + "pr": "BUDDY_EXECUTION_PULL_REQUEST_ID" + }, + { + "name": "Buildkite", + "constant": "BUILDKITE", + "env": "BUILDKITE", + "pr": { "env": "BUILDKITE_PULL_REQUEST", "ne": "false" } + }, + { + "name": "CircleCI", + "constant": "CIRCLE", + "env": "CIRCLECI", + "pr": "CIRCLE_PULL_REQUEST" + }, + { + "name": "Cirrus CI", + "constant": "CIRRUS", + "env": "CIRRUS_CI", + "pr": "CIRRUS_PR" + }, + { + "name": "Codefresh", + "constant": "CODEFRESH", + "env": "CF_BUILD_ID", + "pr": { "any": ["CF_PULL_REQUEST_NUMBER", "CF_PULL_REQUEST_ID"] } + }, + { + "name": "Codemagic", + "constant": "CODEMAGIC", + "env": "CM_BUILD_ID", + "pr": "CM_PULL_REQUEST" + }, + { + "name": "Codeship", + "constant": "CODESHIP", + "env": { "CI_NAME": "codeship" } + }, + { + "name": "Drone", + "constant": "DRONE", + "env": "DRONE", + "pr": { "DRONE_BUILD_EVENT": "pull_request" } + }, + { + "name": "dsari", + "constant": "DSARI", + "env": "DSARI" + }, + { + "name": "Expo Application Services", + "constant": "EAS", + "env": "EAS_BUILD" + }, + { + "name": "GitHub Actions", + "constant": "GITHUB_ACTIONS", + "env": "GITHUB_ACTIONS", + "pr": { "GITHUB_EVENT_NAME": "pull_request" } + }, + { + "name": "GitLab CI", + "constant": "GITLAB", + "env": "GITLAB_CI", + "pr": "CI_MERGE_REQUEST_ID" + }, + { + "name": "GoCD", + "constant": "GOCD", + "env": "GO_PIPELINE_LABEL" + }, + { + "name": "Google Cloud Build", + "constant": "GOOGLE_CLOUD_BUILD", + "env": "BUILDER_OUTPUT" + }, + { + "name": "LayerCI", + "constant": "LAYERCI", + "env": "LAYERCI", + "pr": "LAYERCI_PULL_REQUEST" + }, + { + "name": "Gerrit", + "constant": "GERRIT", + "env": "GERRIT_PROJECT" + }, + { + "name": "Heroku", + "constant": "HEROKU", + "env": { "env": "NODE", "includes": "/app/.heroku/node/bin/node" } + }, + { + "name": "Hudson", + "constant": "HUDSON", + "env": "HUDSON_URL" + }, + { + "name": "Jenkins", + "constant": "JENKINS", + "env": ["JENKINS_URL", "BUILD_ID"], + "pr": { "any": ["ghprbPullId", "CHANGE_ID"] } + }, + { + "name": "Magnum CI", + "constant": "MAGNUM", + "env": "MAGNUM" + }, + { + "name": "Netlify CI", + "constant": "NETLIFY", + "env": "NETLIFY", + "pr": { "env": "PULL_REQUEST", "ne": "false" } + }, + { + "name": "Nevercode", + "constant": "NEVERCODE", + "env": "NEVERCODE", + "pr": { "env": "NEVERCODE_PULL_REQUEST", "ne": "false" } + }, + { + "name": "ReleaseHub", + "constant": "RELEASEHUB", + "env": "RELEASE_BUILD_ID" + }, + { + "name": "Render", + "constant": "RENDER", + "env": "RENDER", + "pr": { "IS_PULL_REQUEST": "true" } + }, + { + "name": "Sail CI", + "constant": "SAIL", + "env": "SAILCI", + "pr": "SAIL_PULL_REQUEST_NUMBER" + }, + { + "name": "Screwdriver", + "constant": "SCREWDRIVER", + "env": "SCREWDRIVER", + "pr": { "env": "SD_PULL_REQUEST", "ne": "false" } + }, + { + "name": "Semaphore", + "constant": "SEMAPHORE", + "env": "SEMAPHORE", + "pr": "PULL_REQUEST_NUMBER" + }, + { + "name": "Shippable", + "constant": "SHIPPABLE", + "env": "SHIPPABLE", + "pr": { "IS_PULL_REQUEST": "true" } + }, + { + "name": "Solano CI", + "constant": "SOLANO", + "env": "TDDIUM", + "pr": "TDDIUM_PR_ID" + }, + { + "name": "Sourcehut", + "constant": "SOURCEHUT", + "env": { "CI_NAME": "sourcehut" } + }, + { + "name": "Strider CD", + "constant": "STRIDER", + "env": "STRIDER" + }, + { + "name": "TaskCluster", + "constant": "TASKCLUSTER", + "env": ["TASK_ID", "RUN_ID"] + }, + { + "name": "TeamCity", + "constant": "TEAMCITY", + "env": "TEAMCITY_VERSION" + }, + { + "name": "Travis CI", + "constant": "TRAVIS", + "env": "TRAVIS", + "pr": { "env": "TRAVIS_PULL_REQUEST", "ne": "false" } + }, + { + "name": "Vercel", + "constant": "VERCEL", + "env": { "any": ["NOW_BUILDER", "VERCEL"] } + }, + { + "name": "Visual Studio App Center", + "constant": "APPCENTER", + "env": "APPCENTER_BUILD_ID" + }, + { + "name": "Woodpecker", + "constant": "WOODPECKER", + "env": { "CI": "woodpecker" }, + "pr": { "CI_BUILD_EVENT": "pull_request" } + }, + { + "name": "Xcode Cloud", + "constant": "XCODE_CLOUD", + "env": "CI_XCODE_PROJECT", + "pr": "CI_PULL_REQUEST_NUMBER" + }, + { + "name": "Xcode Server", + "constant": "XCODE_SERVER", + "env": "XCS" + } +] diff --git a/src/SecTester.Scan/SecTester.Scan.csproj b/src/SecTester.Scan/SecTester.Scan.csproj index 734a2b7..019eea8 100644 --- a/src/SecTester.Scan/SecTester.Scan.csproj +++ b/src/SecTester.Scan/SecTester.Scan.csproj @@ -15,7 +15,6 @@ - @@ -28,4 +27,8 @@ <_Parameter1>$(MSBuildProjectName).Tests + + + + diff --git a/test/SecTester.Scan.Tests/CI/DefaultCiDiscoveryTests.cs b/test/SecTester.Scan.Tests/CI/DefaultCiDiscoveryTests.cs index f0e1d17..e80148e 100644 --- a/test/SecTester.Scan.Tests/CI/DefaultCiDiscoveryTests.cs +++ b/test/SecTester.Scan.Tests/CI/DefaultCiDiscoveryTests.cs @@ -2,19 +2,42 @@ namespace SecTester.Scan.Tests.CI; public class DefaultCiDiscoveryTests { - private readonly DefaultCiDiscovery _sut = new(); + [Fact] + public void Constructor_GivenEmptyEnvironment_CreatesInstanceWithDefaultValues() + { + // act + var sut = new DefaultCiDiscovery(new Dictionary()); + + // assert + sut.Server.Should().BeNull(); + sut.IsCi.Should().BeFalse(); + sut.IsPr.Should().BeFalse(); + } [Fact] - public void Constructor_InitializesServerWithNull() + public void Constructor_GivenEnvironmentWithCI_CreatesInstance() { + // act + var sut = new DefaultCiDiscovery(new Dictionary() { { "GITHUB_ACTIONS", "" } }); + // assert - _sut.Server.Should().BeNull(); + sut.Server.Should().BeSameAs(CiServer.GITHUB_ACTIONS); + sut.IsCi.Should().BeTrue(); + sut.IsPr.Should().BeFalse(); } [Fact] - public void IsCi_ServerIsNull_ReturnsFalse() + public void Constructor_GivenEnvironmentWithCIAndPR_CreatesInstance() { + // act + var sut = new DefaultCiDiscovery(new Dictionary() + { + { "GITHUB_ACTIONS", "" }, { "GITHUB_EVENT_NAME", "pull_request" } + }); + // assert - _sut.IsCi.Should().BeFalse(); + sut.Server.Should().BeSameAs(CiServer.GITHUB_ACTIONS); + sut.IsCi.Should().BeTrue(); + sut.IsPr.Should().BeTrue(); } } diff --git a/test/SecTester.Scan.Tests/CI/ResourceUtilsTests.cs b/test/SecTester.Scan.Tests/CI/ResourceUtilsTests.cs new file mode 100644 index 0000000..1cf1af7 --- /dev/null +++ b/test/SecTester.Scan.Tests/CI/ResourceUtilsTests.cs @@ -0,0 +1,34 @@ +namespace SecTester.Scan.Tests.CI; + +public class ResourceUtilsTests +{ + [Fact] + public void GetEmbeddedResourceContent_GivenEmptyResourceName_ThrowsError() + { + // act + var act = () => ResourceUtils.GetEmbeddedResourceContent(""); + + // assert + act.Should().Throw().WithMessage("*resourceName*"); + } + + [Fact] + public void GetEmbeddedResourceContent_GivenNonExistingResourceName_ThrowsError() + { + // act + var act = () => ResourceUtils.GetEmbeddedResourceContent("ResourceName"); + + // assert + act.Should().Throw().WithMessage($"Could not get stream for ResourceName resource."); + } + + [Fact] + public void GetEmbeddedResourceContent_GivenExistingResourceName_ReturnsContent() + { + // act + var act = () => ResourceUtils.GetEmbeddedResourceContent("SecTester.Scan.CI.vendors.json"); + + // assert + act.Should().NotBeNull(); + } +} diff --git a/test/SecTester.Scan.Tests/CI/VendorMatcherTests.cs b/test/SecTester.Scan.Tests/CI/VendorMatcherTests.cs new file mode 100644 index 0000000..a5874ff --- /dev/null +++ b/test/SecTester.Scan.Tests/CI/VendorMatcherTests.cs @@ -0,0 +1,252 @@ +using System.Collections.Specialized; + +namespace SecTester.Scan.Tests.CI; + +public class VendorMatcherTests +{ + private readonly JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static readonly IEnumerable MatchEnvInput = new List + { + new object[] + { + @"{ + ""env"": { ""env"": ""NODE"", ""includes"": ""/app/.heroku/node/bin/node""} + }", + new Dictionary { { "NODE", "/app/.heroku/node/bin/node/123" } }, true + }, + new object[] + { + @"{ + ""env"": { ""env"": ""NODE"", ""includes"": ""/app/.heroku/node/bin/node""} + }", + new Dictionary { { "NODE", "/app/.heroku/node/bin/" } }, false + }, + new object[] + { + @"{ + ""env"": { ""env"": ""NODE-1"", ""includes"": ""/app/.heroku/node/bin/node""} + }", + new Dictionary(), false + }, + new object[] + { + @"{ + ""env"": { ""CI_NAME"": ""sourcehut"" } + }", + new Dictionary { { "CI_NAME", "sourcehut" } }, true + }, + new object[] + { + @"{ + ""env"": { ""CI_NAME"": ""sourcehut"" } + }", + new Dictionary { { "CI_NAME", "sourcehut1" } }, false + }, + new object[] + { + @"{ + ""env"": { ""CI_NAME"": ""sourcehut"" } + }", + new Dictionary(), false + }, + new object[] + { + @"{ + ""env"": { ""any"": [""NOW_BUILDER"", ""VERCEL""] } + }", + new Dictionary { { "VERCEL", "value" } }, true + }, + new object[] + { + @"{ + ""env"": { ""any"": [""NOW_BUILDER"", ""VERCEL""] } + }", + new Dictionary(), false + }, + new object[] + { + @"{ + ""env"": [""JENKINS_URL"", ""BUILD_ID""] + }", + new Dictionary { { "JENKINS_URL", "jenkins_url" }, { "BUILD_ID", "build_id" } }, true + }, + new object[] + { + @"{ + ""env"": [""JENKINS_URL"", ""BUILD_ID""] + }", + new Dictionary { { "JENKINS_URL", "jenkins_url" } }, false + }, + new object[] + { + @"{ + ""env"": [""JENKINS_URL"", ""BUILD_ID""] + }", + new Dictionary(), false + }, + new object[] + { + @"{ + ""env"": ""MAGNUM"" + }", + new Dictionary { { "MAGNUM", "value" } }, true + }, + new object[] + { + @"{ + ""env"": ""MAGNUM"" + }", + new Dictionary(), false + } + }; + + public static readonly IEnumerable MatchPrInput = new List + { + new object[] + { + @"{ + ""pr"": ""SAIL_PULL_REQUEST_NUMBER"" + }", + new Dictionary { { "SAIL_PULL_REQUEST_NUMBER", "" } }, true + }, + new object[] + { + @"{ + ""pr"": ""SAIL_PULL_REQUEST_NUMBER"" + }", + new Dictionary(), false + }, + new object[] + { + @"{ + ""pr"": { ""CI_BUILD_EVENT"": ""pull_request"" } + }", + new Dictionary { { "CI_BUILD_EVENT", "pull_request" } }, true + }, + new object[] + { + @"{ + ""pr"": { ""CI_BUILD_EVENT"": ""pull_request"" } + }", + new Dictionary { { "CI_BUILD_EVENT", "pull_request1" } }, false + }, + new object[] + { + @"{ + ""pr"": { ""CI_BUILD_EVENT"": ""pull_request"" } + }", + new Dictionary(), false + }, + + new object[] + { + @"{ + ""pr"": { ""env"": ""TRAVIS_PULL_REQUEST"", ""ne"": ""false"" } + }", + new Dictionary { { "TRAVIS_PULL_REQUEST", "some" } }, true + }, + new object[] + { + @"{ + ""pr"": { ""env"": ""TRAVIS_PULL_REQUEST"", ""ne"": ""false"" } + }", + new Dictionary { { "TRAVIS_PULL_REQUEST", "" } }, false + }, + new object[] + { + @"{ + ""pr"": { ""env"": ""TRAVIS_PULL_REQUEST"", ""ne"": ""false"" } + }", + new Dictionary { { "TRAVIS_PULL_REQUEST", null! } }, false + }, + new object[] + { + @"{ + ""pr"": { ""env"": ""TRAVIS_PULL_REQUEST"", ""ne"": ""false"" } + }", + new Dictionary (), false + }, + new object[] + { + @"{ + ""pr"": { ""any"": [""CF_PULL_REQUEST_NUMBER"", ""CF_PULL_REQUEST_ID""] } + }", + new Dictionary { { "CF_PULL_REQUEST_NUMBER", "" } }, true + }, + new object[] + { + @"{ + ""pr"": { ""any"": [""CF_PULL_REQUEST_NUMBER"", ""CF_PULL_REQUEST_ID""] } + }", + new Dictionary (), false + }, + }; + + + + [Theory] + [MemberData(nameof(MatchEnvInput))] + public void MatchEnvElement_ReturnsExpected(string vendorInput, IDictionary environmentInput, bool expected) + { + // arrange + var sut = new VendorMatcher(environmentInput); + var vendor = JsonSerializer.Deserialize(vendorInput, _options)!; + + // act + var result = sut.MatchEnvElement(vendor.Env); + + //assert + result.Should().Be(expected); + } + + [Theory] + [MemberData(nameof(MatchPrInput))] + public void MatchPrElement_ReturnsExpected(string vendorInput, IDictionary environmentInput, bool expected) + { + // arrange + var sut = new VendorMatcher(environmentInput); + var vendor = JsonSerializer.Deserialize(vendorInput, _options)!; + + // act + var result = sut.MatchPrElement(vendor.Pr); + + //assert + result.Should().Be(expected); + } + + [Fact] + public void MatchEnvElement_GivenAnyEnv_Returns() + { + // arrange + var sut = new VendorMatcher(new Dictionary()); + var vendors = JsonSerializer.Deserialize( + ResourceUtils.GetEmbeddedResourceContent("SecTester.Scan.CI.vendors.json"), + new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + + // act + var result = vendors!.FirstOrDefault(x=> sut.MatchEnvElement(x.Env)); + + // assert + result.Should().BeNull(); + } + + [Fact] + public void MatchPrElement_GivenAnyPr_Returns() + { + // arrange + var sut = new VendorMatcher(new Dictionary()); + var vendors = JsonSerializer.Deserialize( + ResourceUtils.GetEmbeddedResourceContent("SecTester.Scan.CI.vendors.json"), + new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + + // act + var result = vendors!.FirstOrDefault(x=> sut.MatchPrElement(x.Pr)); + + // assert + result.Should().BeNull(); + } +} diff --git a/test/SecTester.Scan.Tests/Usings.cs b/test/SecTester.Scan.Tests/Usings.cs index 05a4ce2..ddedd5b 100644 --- a/test/SecTester.Scan.Tests/Usings.cs +++ b/test/SecTester.Scan.Tests/Usings.cs @@ -1,5 +1,7 @@ global using System.Net.Http.Headers; global using System.Net.Http.Json; +global using System.Collections; +global using System.Text.Json; global using System.Text; global using System.Text.RegularExpressions; global using FluentAssertions;