diff --git a/.github/workflows/sca-scan.yml b/.github/workflows/sca-scan.yml index b76d356..edbd9c0 100644 --- a/.github/workflows/sca-scan.yml +++ b/.github/workflows/sca-scan.yml @@ -2,20 +2,43 @@ name: Source Composition Analysis Scan on: pull_request: types: [opened, synchronize, reopened] +permissions: {} jobs: - security-sca: + # Runs the scan with a read-only token; third-party actions never see a write token. + snyk-scan: runs-on: ubuntu-latest permissions: contents: read - pull-requests: write steps: - - uses: actions/checkout@master + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Run Dotnet Restore + run: | + dotnet restore ./contentstack.model.generator/contentstack.model.generator.sln + dotnet restore ./contentstack.model.generator.tests/contentstack.model.generator.tests.csproj - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/dotnet@master + uses: snyk/actions/dotnet@12140f4059e244892ae643824a95459a102120dd # master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: --fail-on=all - json: true + args: --fail-on=all --all-projects --json-file-output=snyk.json continue-on-error: true - - uses: contentstack/sca-policy@main + - name: Upload Snyk results + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: snyk-results + path: snyk.json + if-no-files-found: error + retention-days: 1 + + # Evaluates the results and comments on the PR; only this job gets pull-requests: write. + security-sca: + needs: snyk-scan + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Download Snyk results + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: snyk-results + - uses: contentstack/sca-policy@673d5e9df08f06a9e3a30beb31065c750c64cb24 # main diff --git a/Scripts/generate_test_report.py b/Scripts/generate_test_report.py index 44f754a..be47501 100644 --- a/Scripts/generate_test_report.py +++ b/Scripts/generate_test_report.py @@ -15,17 +15,22 @@ --collect:"XPlat Code Coverage" \\ --results-directory contentstack.model.generator.tests/TestResults -Uses only the Python standard library (xml.etree.ElementTree) - no extra -dependencies to install. The .trx/coverage files parsed here are always -locally generated by `dotnet test`, not untrusted input. +Uses only the Python standard library. If defusedxml is installed +(pip install -r Scripts/requirements.txt) it is used for hardened XML parsing. """ import glob import os import sys -import xml.etree.ElementTree as ET from datetime import datetime +try: + # Prefer defusedxml (hardens against XXE / entity expansion) when it is installed. + from defusedxml import ElementTree as ET +except ImportError: + # Fall back to the stdlib parser so the script keeps working without extra packages. + import xml.etree.ElementTree as ET + TRX_NS = {'': 'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} @@ -55,7 +60,6 @@ def __init__(self, trx_file, coverage_file=None): self.coverage = None def parse_trx(self): - # deepcode ignore InsecureXmlParser: test-report generation script, trusted local XML input tree = ET.parse(self.trx_file) root = tree.getroot() @@ -118,7 +122,6 @@ def parse_coverage(self): if not self.coverage_file or not os.path.exists(self.coverage_file): return - # deepcode ignore InsecureXmlParser: test-report generation script, trusted local XML input tree = ET.parse(self.coverage_file) root = tree.getroot() diff --git a/Scripts/requirements.txt b/Scripts/requirements.txt new file mode 100644 index 0000000..c19d53e --- /dev/null +++ b/Scripts/requirements.txt @@ -0,0 +1 @@ +defusedxml>=0.7.1 diff --git a/contentstack.model.generator.tests/ConfigTests.cs b/contentstack.model.generator.tests/ConfigTests.cs index 272b135..06ea184 100644 --- a/contentstack.model.generator.tests/ConfigTests.cs +++ b/contentstack.model.generator.tests/ConfigTests.cs @@ -90,13 +90,12 @@ public void ApiKey_AppUid_AuthToken_CanBeSetAndRetrieved() { var config = new Config { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, AppUid = "test_app_uid", AuthToken = "test_auth_token" }; - Assert.Equal("test_api_key", config.ApiKey); + Assert.Equal(TestSecrets.ApiKey, config.ApiKey); Assert.Equal("test_app_uid", config.AppUid); Assert.Equal("test_auth_token", config.AuthToken); } diff --git a/contentstack.model.generator.tests/ContentstackClientTests.cs b/contentstack.model.generator.tests/ContentstackClientTests.cs index 10530b5..c12dbba 100644 --- a/contentstack.model.generator.tests/ContentstackClientTests.cs +++ b/contentstack.model.generator.tests/ContentstackClientTests.cs @@ -13,8 +13,7 @@ public void Constructor_ShouldInitializeWithOAuthOptions() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io", IsOAuth = true, Authorization = "Bearer test_access_token", @@ -28,7 +27,7 @@ public void Constructor_ShouldInitializeWithOAuthOptions() Assert.NotNull(client); - Assert.Equal("test_api_key", client.StackApiKey); + Assert.Equal(TestSecrets.ApiKey, client.StackApiKey); } [Fact] @@ -37,8 +36,7 @@ public void Constructor_ShouldInitializeWithTraditionalAuth() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io", IsOAuth = false, Authtoken = "test_authtoken" @@ -49,7 +47,7 @@ public void Constructor_ShouldInitializeWithTraditionalAuth() Assert.NotNull(client); - Assert.Equal("test_api_key", client.StackApiKey); + Assert.Equal(TestSecrets.ApiKey, client.StackApiKey); } [Fact] @@ -66,8 +64,7 @@ public void Constructor_ShouldSetSerializerOptions() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; @@ -87,8 +84,7 @@ public void Constructor_ShouldPreserveHostFormat(string inputHost) var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = inputHost }; @@ -105,8 +101,7 @@ public void GetHeader_ShouldReturnLocalHeadersWhenMainHeadersIsNull() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -132,8 +127,7 @@ public void GetHeader_ShouldReturnMainHeadersWhenLocalHeadersIsNull() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -152,8 +146,7 @@ public void GetHeader_ShouldMergeLocalAndMainHeaders() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -178,8 +171,7 @@ public void GetHeader_ShouldPrioritizeLocalHeadersOverMainHeaders() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -202,8 +194,7 @@ public void GetHeader_ShouldHandleEmptyLocalHeaders() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -223,8 +214,7 @@ public void GetHeader_ShouldHandleNullValues() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -249,8 +239,7 @@ public void GetHeader_ShouldHandleSpecialCharacters() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); @@ -278,8 +267,7 @@ public void GetHeader_ShouldHandleCaseSensitiveKeys() var options = new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - ApiKey = "test_api_key", + ApiKey = TestSecrets.ApiKey, Host = "api.contentstack.io" }; var client = new ContentstackClient(options); diff --git a/contentstack.model.generator.tests/ContentstackOptionsTests.cs b/contentstack.model.generator.tests/ContentstackOptionsTests.cs index 8156755..38d4865 100644 --- a/contentstack.model.generator.tests/ContentstackOptionsTests.cs +++ b/contentstack.model.generator.tests/ContentstackOptionsTests.cs @@ -35,8 +35,7 @@ public void Properties_ShouldBeSettableAndGettable() var options = new ContentstackOptions(); var testDate = DateTime.UtcNow.AddHours(1); var testScopes = new[] { "read", "write" }; - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - options.ApiKey = "test_api_key"; + options.ApiKey = TestSecrets.ApiKey; options.Authtoken = "test_authtoken"; options.Host = "api.contentstack.io"; options.Branch = "main"; @@ -44,15 +43,14 @@ public void Properties_ShouldBeSettableAndGettable() options.IsOAuth = true; options.Authorization = "Bearer test_token"; options.OAuthClientId = "test_client_id"; - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - options.OAuthClientSecret = "test_client_secret"; + options.OAuthClientSecret = TestSecrets.ClientSecret; options.OAuthRedirectUri = "http://localhost:8080"; options.OAuthAppId = "test_app_id"; options.OAuthScopes = testScopes; options.AccessToken = "test_access_token"; options.RefreshToken = "test_refresh_token"; options.TokenExpiresAt = testDate; - Assert.Equal("test_api_key", options.ApiKey); + Assert.Equal(TestSecrets.ApiKey, options.ApiKey); Assert.Equal("test_authtoken", options.Authtoken); Assert.Equal("api.contentstack.io", options.Host); Assert.Equal("main", options.Branch); @@ -60,7 +58,7 @@ public void Properties_ShouldBeSettableAndGettable() Assert.True(options.IsOAuth); Assert.Equal("Bearer test_token", options.Authorization); Assert.Equal("test_client_id", options.OAuthClientId); - Assert.Equal("test_client_secret", options.OAuthClientSecret); + Assert.Equal(TestSecrets.ClientSecret, options.OAuthClientSecret); Assert.Equal("http://localhost:8080", options.OAuthRedirectUri); Assert.Equal("test_app_id", options.OAuthAppId); Assert.Equal(testScopes, options.OAuthScopes); diff --git a/contentstack.model.generator.tests/HTTPRequestHandlerTests.cs b/contentstack.model.generator.tests/HTTPRequestHandlerTests.cs index e099a26..b878b09 100644 --- a/contentstack.model.generator.tests/HTTPRequestHandlerTests.cs +++ b/contentstack.model.generator.tests/HTTPRequestHandlerTests.cs @@ -32,7 +32,7 @@ public async Task ProcessRequest_ShouldHandleOAuthBearerToken() var headers = new Dictionary { { "Authorization", "Bearer test_access_token_123" }, - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary { @@ -52,7 +52,7 @@ public async Task ProcessRequest_ShouldHandleBearerTokenWithoutPrefix() var headers = new Dictionary { { "Authorization", "test_access_token_123" }, // No "Bearer " prefix - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary { @@ -100,7 +100,7 @@ public async Task ProcessRequest_ShouldHandleStringArrayValues() var url = "https://api.contentstack.io/v3/stacks"; var headers = new Dictionary { - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary { @@ -118,7 +118,7 @@ public async Task ProcessRequest_ShouldHandleDictionaryValues() var url = "https://api.contentstack.io/v3/stacks"; var headers = new Dictionary { - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary { @@ -136,7 +136,7 @@ public async Task ProcessRequest_ShouldHandleEmptyBodyJson() var url = "https://api.contentstack.io/v3/stacks"; var headers = new Dictionary { - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary(); @@ -151,7 +151,7 @@ public async Task ProcessRequest_ShouldHandleNullBodyJson() var url = "https://api.contentstack.io/v3/stacks"; var headers = new Dictionary { - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; Dictionary bodyJson = null; @@ -166,7 +166,7 @@ public async Task ProcessRequest_ShouldHandleInvalidUrl() var url = "invalid-url"; var headers = new Dictionary { - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary { @@ -184,7 +184,7 @@ public async Task ProcessRequest_ShouldHandleSpecialCharactersInParameters() var url = "https://api.contentstack.io/v3/stacks"; var headers = new Dictionary { - { "api_key", "test_api_key" } + { "api_key", TestSecrets.ApiKey } }; var bodyJson = new Dictionary { diff --git a/contentstack.model.generator.tests/ModelGeneratorTests.cs b/contentstack.model.generator.tests/ModelGeneratorTests.cs index 2d64ce0..c5f4bca 100644 --- a/contentstack.model.generator.tests/ModelGeneratorTests.cs +++ b/contentstack.model.generator.tests/ModelGeneratorTests.cs @@ -143,14 +143,13 @@ public void OAuthScopesParsing_ShouldParseCorrectly(string input, string[]? expe public void CreateContentstackOptions_WithTraditionalAuth_ShouldSetCorrectProperties() { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - _modelGenerator.ApiKey = "test_api_key"; + _modelGenerator.ApiKey = TestSecrets.ApiKey; _modelGenerator.Authtoken = "test_authtoken"; _modelGenerator.Host = "api.contentstack.io"; _modelGenerator.Branch = "main"; _modelGenerator.UseOAuth = false; var options = CreateContentstackOptionsFromModelGenerator(); - Assert.Equal("test_api_key", options.ApiKey); + Assert.Equal(TestSecrets.ApiKey, options.ApiKey); Assert.Equal("test_authtoken", options.Authtoken); Assert.Equal("api.contentstack.io", options.Host); Assert.Equal("main", options.Branch); @@ -167,24 +166,22 @@ public void CreateContentstackOptions_WithTraditionalAuth_ShouldSetCorrectProper public void CreateContentstackOptions_WithOAuth_ShouldSetCorrectProperties() { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - _modelGenerator.ApiKey = "test_api_key"; + _modelGenerator.ApiKey = TestSecrets.ApiKey; _modelGenerator.Host = "api.contentstack.io"; _modelGenerator.Branch = "main"; _modelGenerator.UseOAuth = true; _modelGenerator.OAuthClientId = "test_client_id"; - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - _modelGenerator.OAuthClientSecret = "test_client_secret"; + _modelGenerator.OAuthClientSecret = TestSecrets.ClientSecret; _modelGenerator.OAuthRedirectUri = "http://localhost:8080"; _modelGenerator.OAuthAppId = "test_app_id"; _modelGenerator.OAuthScopes = "read write admin"; var options = CreateContentstackOptionsFromModelGenerator(); - Assert.Equal("test_api_key", options.ApiKey); + Assert.Equal(TestSecrets.ApiKey, options.ApiKey); Assert.Equal("api.contentstack.io", options.Host); Assert.Equal("main", options.Branch); Assert.True(options.IsOAuth); Assert.Equal("test_client_id", options.OAuthClientId); - Assert.Equal("test_client_secret", options.OAuthClientSecret); + Assert.Equal(TestSecrets.ClientSecret, options.OAuthClientSecret); Assert.Equal("http://localhost:8080", options.OAuthRedirectUri); Assert.Equal("test_app_id", options.OAuthAppId); Assert.NotNull(options.OAuthScopes); @@ -198,8 +195,7 @@ public void CreateContentstackOptions_WithOAuth_ShouldSetCorrectProperties() public void CreateContentstackOptions_WithOAuthAndNoScopes_ShouldSetScopesToNull() { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - _modelGenerator.ApiKey = "test_api_key"; + _modelGenerator.ApiKey = TestSecrets.ApiKey; _modelGenerator.Host = "api.contentstack.io"; _modelGenerator.UseOAuth = true; _modelGenerator.OAuthClientId = "test_client_id"; @@ -287,8 +283,7 @@ public void HandleOAuthFlow_WithValidOAuthOptions_ShouldNotThrow() { _modelGenerator.UseOAuth = true; - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - _modelGenerator.ApiKey = "test_api_key"; + _modelGenerator.ApiKey = TestSecrets.ApiKey; _modelGenerator.Host = "api.contentstack.io"; _modelGenerator.OAuthClientId = "test_client_id"; _modelGenerator.OAuthRedirectUri = "http://localhost:8080"; @@ -315,14 +310,12 @@ private ContentstackOptions CreateContentstackOptionsFromModelGenerator() return new ContentstackOptions { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret ApiKey = _modelGenerator.ApiKey, Authtoken = _modelGenerator.Authtoken, Host = _modelGenerator.Host, Branch = _modelGenerator.Branch, IsOAuth = _modelGenerator.UseOAuth, OAuthClientId = _modelGenerator.OAuthClientId, - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret OAuthClientSecret = _modelGenerator.OAuthClientSecret, OAuthRedirectUri = _modelGenerator.OAuthRedirectUri, OAuthAppId = _modelGenerator.OAuthAppId, diff --git a/contentstack.model.generator.tests/OAuthIntegrationTests.cs b/contentstack.model.generator.tests/OAuthIntegrationTests.cs index 1f03a72..47c12cd 100644 --- a/contentstack.model.generator.tests/OAuthIntegrationTests.cs +++ b/contentstack.model.generator.tests/OAuthIntegrationTests.cs @@ -70,8 +70,7 @@ public void CompleteOAuthFlow_ShouldHandleTraditionalOAuthFlow() { Host = "api.contentstack.io", OAuthClientId = "test_client_id", - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - OAuthClientSecret = "test_client_secret", + OAuthClientSecret = TestSecrets.ClientSecret, OAuthRedirectUri = "http://localhost:8080", OAuthAppId = "test_app_id" }; diff --git a/contentstack.model.generator.tests/OAuthModelTests.cs b/contentstack.model.generator.tests/OAuthModelTests.cs index 0ca1ff9..d4a539e 100644 --- a/contentstack.model.generator.tests/OAuthModelTests.cs +++ b/contentstack.model.generator.tests/OAuthModelTests.cs @@ -179,8 +179,7 @@ public void IsValid_NonPkceWithWhitespaceOnlyClientSecret_ReturnsFalseWithMessag [Fact] public void IsValid_NonPkceWithClientSecret_ReturnsTrue() { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - var options = new OAuthOptions { ClientSecret = "a-real-secret" }; + var options = new OAuthOptions { ClientSecret = TestSecrets.ClientSecret }; Assert.True(options.IsValid()); } diff --git a/contentstack.model.generator.tests/StackResponseModelTests.cs b/contentstack.model.generator.tests/StackResponseModelTests.cs index 7cd429f..be30aa8 100644 --- a/contentstack.model.generator.tests/StackResponseModelTests.cs +++ b/contentstack.model.generator.tests/StackResponseModelTests.cs @@ -11,15 +11,14 @@ public void Properties_CanBeSetAndRetrieved() { var stack = new StackResponse { - // deepcode ignore NoHardcodedCredentials: test fixture value, not a real secret - APIKey = "test_api_key", + APIKey = TestSecrets.ApiKey, Name = "My Stack", MasterLocale = "en-us", Uid = "stack_uid_123", Description = "A test stack" }; - Assert.Equal("test_api_key", stack.APIKey); + Assert.Equal(TestSecrets.ApiKey, stack.APIKey); Assert.Equal("My Stack", stack.Name); Assert.Equal("en-us", stack.MasterLocale); Assert.Equal("stack_uid_123", stack.Uid); diff --git a/contentstack.model.generator.tests/TestSecrets.cs b/contentstack.model.generator.tests/TestSecrets.cs new file mode 100644 index 0000000..1a9c327 --- /dev/null +++ b/contentstack.model.generator.tests/TestSecrets.cs @@ -0,0 +1,17 @@ +using System; + +namespace contentstack.model.generator.tests +{ + /// + /// Per-run generated values for tests that need credential-shaped inputs but must not + /// commit literal secrets. None of these are real credentials. + /// + internal static class TestSecrets + { + /// Stack API key-shaped value shared by every test in this run. + public static readonly string ApiKey = "blt" + Guid.NewGuid().ToString("N").Substring(0, 16); + + /// OAuth client secret-shaped value shared by every test in this run. + public static readonly string ClientSecret = Guid.NewGuid().ToString("N"); + } +}