Skip to content

Commit

Permalink
Adding CratesApiKey validator (#531)
Browse files Browse the repository at this point in the history
* Adding CratesApiKey validator

* Addressing PR feedback
  • Loading branch information
eddynaka authored Aug 11, 2021
1 parent 0b9c1b0 commit a8fb06e
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Src/Plugins/Security/SEC101.SecurePlaintextSecrets.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,12 @@
"IntrafileRegexes": [ "$SEC101/046.DiscordApiCredentialsId", "$SEC101/046.DiscordApiCredentialsSecret" ],
"MessageArguments": { "secretKind": "Discord API credential" }
},
{
"Id": "SEC101/047",
"Name": "DoNotExposePlaintextSecrets/CratesApiKey",
"ContentsRegex": "$SEC101/047.CratesApiKey",
"MessageArguments": { "secretKind": "Crates API key" }
},
{
"Id": "SEC101/102",
"Name": "DoNotExposePlaintextSecrets/AdoPat",
Expand Down
107 changes: 107 additions & 0 deletions Src/Plugins/Security/SEC101_047.CratesApiKeyValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;

using Microsoft.CodeAnalysis.Sarif.PatternMatcher.Sdk;
using Microsoft.RE2.Managed;

namespace Microsoft.CodeAnalysis.Sarif.PatternMatcher.Plugins.Security
{
public class CratesApiKeyValidator : ValidatorBase
{
internal static CratesApiKeyValidator Instance;

static CratesApiKeyValidator()
{
Instance = new CratesApiKeyValidator();
}

public static IEnumerable<ValidationResult> IsValidStatic(Dictionary<string, FlexMatch> groups)
{
return IsValidStatic(Instance, groups);
}

public static ValidationState IsValidDynamic(ref Fingerprint fingerprint,
ref string message,
Dictionary<string, string> options,
ref ResultLevelKind resultLevelKind)
{
return IsValidDynamic(Instance,
ref fingerprint,
ref message,
options,
ref resultLevelKind);
}

protected override IEnumerable<ValidationResult> IsValidStaticHelper(Dictionary<string, FlexMatch> groups)
{
FlexMatch secret = groups["secret"];

if (!ContainsDigitAndChar(secret.Value))
{
return ValidationResult.CreateNoMatch();
}

var validationResult = new ValidationResult
{
Fingerprint = new Fingerprint
{
Secret = secret.Value,
Platform = nameof(AssetPlatform.Crates),
},
ValidationState = ValidationState.Unknown,
};

return new[] { validationResult };
}

protected override ValidationState IsValidDynamicHelper(ref Fingerprint fingerprint,
ref string message,
Dictionary<string, string> options,
ref ResultLevelKind resultLevelKind)
{
string secret = fingerprint.Secret;

const string uri = "https://crates.io/api/v1/crates/sarif-pattern-matcher/owners";

try
{
HttpClient client = CreateOrRetrieveCachedHttpClient();

using var request = new HttpRequestMessage(HttpMethod.Delete, uri);
request.Headers.Add("Authorization", secret);

using HttpResponseMessage response = client
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
.GetAwaiter()
.GetResult();

switch (response.StatusCode)
{
case HttpStatusCode.OK:
{
return ValidationState.Authorized;
}

case HttpStatusCode.Forbidden:
{
return ValidationState.Unauthorized;
}

default:
{
return ReturnUnexpectedResponseCode(ref message, response.StatusCode);
}
}
}
catch (Exception e)
{
return ReturnUnhandledException(ref message, e);
}
}
}
}
1 change: 1 addition & 0 deletions Src/Plugins/Security/Security.SharedStrings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,5 @@
$SEC101/044.NpmCredentialsUserPassword=(?is)(?:(?:(?:registry\s*=\s*https:\/\/(?P<host>[^\s]+)(?:$|\s))|(?:username\s*=\s*(?P<id>[^\s]+)(?:$|\s))|(?:_password\s*=\s*(?P<secret>[^\s]+)(?:$|\s)))(?:.{0,200})?){3}
$SEC101/044.NpmCredentialsPassword=(?is)(?:(?:(?:registry\s*=\s*https:\/\/(?P<host>[^\s]+)(?:$|\s))|(?:_password\s*=\s*(?P<secret>[^\s]+)(?:$|\s)))(?:.{0,200})?){2}
$SEC101/045.PostmanApiKey=\b(?P<secret>PMAK-[0-9a-z]{24}-[0-9a-z]{34})(?:[^0-9a-z]|$)
$SEC101/047.CratesApiKey=(?i)(?:[^c]|^)(?P<secret>cio[0-9a-z]{32})(?:[^0-9a-z]|$)
$SEC101/102.AdoPat=\b(?P<secret>[2-7a-z]{52})(?:\b|$)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "testhost",
"organization": "Microsoft Corporation",
"product": "Microsoft.TestHost",
"fullName": "testhost 15.0.0.0",
"version": "15.0.0.0",
"semanticVersion": "15.0.0",
"rules": [
{
"id": "SEC101/047",
"name": "DoNotExposePlaintextSecrets/CratesApiKey",
"fullDescription": {
"text": "Do not expose plaintext (or base64-encoded plaintext) secrets in versioned engineering content."
},
"messageStrings": {
"NotApplicable_InvalidMetadata": {
"text": "'{0}' was not evaluated for check '{1}' because the analysis is not relevant for the following reason: {2}."
},
"Default": {
"text": "'{0}' is {1}{2}{3}{4}{5}."
}
},
"helpUri": "https://github.com/microsoft/sarif-pattern-matcher"
}
]
}
},
"invocations": [
{
"executionSuccessful": true
}
],
"results": [
{
"ruleId": "SEC101/047",
"ruleIndex": 0,
"level": "note",
"message": {
"id": "Default",
"arguments": [
"ciodea…",
"an apparent ",
"",
"Crates API key",
"",
" (no validation occurred as it was not enabled. Pass '--dynamic-validation' on the command-line to validate this match)"
]
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/Plugins/Tests.Security/TestData/SecurePlaintextSecrets/Inputs/SEC101_047.CratesApiKey.ps1",
"uriBaseId": "SRC_ROOT"
},
"region": {
"startLine": 1,
"startColumn": 1,
"endLine": 1,
"endColumn": 36,
"charOffset": 0,
"charLength": 35,
"snippet": {
"text": "ciodeaddeaddead123412341234dead1234"
}
}
}
}
],
"fingerprints": {
"AssetFingerprint/v1": "[platform=Crates]",
"ValidationFingerprint/v1": "[secret=ciodeaddeaddead123412341234dead1234]",
"ValidationFingerprintHash/v1": "9ae3596b73b37748eac0f276b2ce08fa96a6ceebcd3270e5d4ec573df0e27e28",
"AssetFingerprint/v2": "{\"platform\":\"Crates\"}",
"ValidationFingerprint/v2": "{\"secret\":\"ciodeaddeaddead123412341234dead1234\"}"
},
"rank": 43.89
}
],
"columnKind": "utf16CodeUnits"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ciodeaddeaddead123412341234dead1234
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;

using FluentAssertions;

using Microsoft.CodeAnalysis.Sarif.PatternMatcher.Plugins.Security.Helpers;
using Microsoft.CodeAnalysis.Sarif.PatternMatcher.Sdk;

using Xunit;

namespace Microsoft.CodeAnalysis.Sarif.PatternMatcher.Plugins.Security.Validators
{
public class CratesApiKeyValidatorTests
{
[Fact]
public void DiscordCredentialsValidator_MockHttpTests()
{
string unknownMessage = null;
const string fingerprintText = "[secret=b]";
ValidatorBase.ReturnUnexpectedResponseCode(ref unknownMessage, HttpStatusCode.NotFound);

var testCases = new HttpMockTestCase[]
{
new HttpMockTestCase
{
Title = "Testing Valid Credentials",
HttpStatusCodes = new List<HttpStatusCode>{ HttpStatusCode.OK },
ExpectedValidationState = ValidationState.Authorized,
},
new HttpMockTestCase
{
Title = "Testing Invalid Credentials",
HttpStatusCodes = new List<HttpStatusCode>{ HttpStatusCode.Forbidden },
ExpectedValidationState = ValidationState.Unauthorized,
},
new HttpMockTestCase
{
Title = "Testing NotFound StatusCode",
HttpStatusCodes = new List<HttpStatusCode>{ HttpStatusCode.NotFound },
ExpectedValidationState = ValidationState.Unknown,
ExpectedMessage = unknownMessage
},
};

var sb = new StringBuilder();
foreach (HttpMockTestCase testCase in testCases)
{
string message = null;
ResultLevelKind resultLevelKind = default;
var fingerprint = new Fingerprint(fingerprintText);
var keyValuePairs = new Dictionary<string, string>();

ValidatorHelper.ResetStaticInstance<CratesApiKeyValidator>();

using var httpClient = new HttpClient(HttpMockHelper.Mock(testCase.HttpStatusCodes[0], null));
CratesApiKeyValidator.Instance.SetHttpClient(httpClient);

ValidationState currentState = CratesApiKeyValidator.IsValidDynamic(ref fingerprint,
ref message,
keyValuePairs,
ref resultLevelKind);
if (currentState != testCase.ExpectedValidationState)
{
sb.AppendLine($"The test case '{testCase.Title}' was expecting '{testCase.ExpectedValidationState}' but found '{currentState}'.");
}

if (message != testCase.ExpectedMessage)
{
sb.AppendLine($"The test case '{testCase.Title}' was expecting '{testCase.ExpectedMessage}' but found '{message}'.");
}
}

sb.Length.Should().Be(0, sb.ToString());
}
}
}
5 changes: 5 additions & 0 deletions Src/Sarif.PatternMatcher.Sdk/AssetPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public enum AssetPlatform
/// </summary>
Cloudant,

/// <summary>
/// Crates.io platform. https://crates.io/
/// </summary>
Crates,

/// <summary>
/// Discord platform. https://discord.com/developers
/// </summary>
Expand Down

0 comments on commit a8fb06e

Please sign in to comment.