Skip to content

Commit

Permalink
feat(scan): implement CI discovery provider
Browse files Browse the repository at this point in the history
closes #88
  • Loading branch information
ostridm committed Dec 9, 2022
1 parent 512f12b commit 144eab6
Show file tree
Hide file tree
Showing 12 changed files with 791 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/SecTester.Scan/CI/CiDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface CiDiscovery
{
CiServer? Server { get; }
bool IsCi { get; }
bool IsPr { get; }
}
47 changes: 46 additions & 1 deletion src/SecTester.Scan/CI/CiServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
41 changes: 40 additions & 1 deletion src/SecTester.Scan/CI/DefaultCiDiscovery.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Text.Json;

namespace SecTester.Scan.CI;

// TODO ([email protected]): 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<Vendor[]>(
ResourceUtils.GetEmbeddedResourceContent<DefaultCiDiscovery>("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<CiServer>()
.FirstOrDefault() ?? new CiServer(vendor.Name);

IsPr = matcher.MatchPrElement(vendor.Pr);
}
}
29 changes: 29 additions & 0 deletions src/SecTester.Scan/CI/ResourceUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.IO;
using System.Reflection;

namespace SecTester.Scan.CI;

internal static class ResourceUtils
{
public static string GetEmbeddedResourceContent<T>(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();
}
}
9 changes: 9 additions & 0 deletions src/SecTester.Scan/CI/Vendor.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
88 changes: 88 additions & 0 deletions src/SecTester.Scan/CI/VendorMatcher.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 144eab6

Please sign in to comment.