From 8c0f4d2047b0b9135c821622d8090764b5819655 Mon Sep 17 00:00:00 2001 From: wenjiefan Date: Fri, 7 Aug 2026 11:49:01 +0200 Subject: [PATCH] Add per-article BCQuality coverage for code-review dataset (AB#646153) Annotate code-review gold entries with the BCQuality knowledge article(s) they exercise and report per-article coverage. - ReviewComment.article and metadata.articles (both optional, backward-compatible) carry the / association; FP-guard entries (expected_comments=[]) use the entry-level list. - New dataset/coverage.py aggregates declared articles and, given a BCQuality checkout (--bcquality-root / BCQUALITY_ROOT), flags zero-coverage and unknown slugs; degrades to declared-only when absent. - 'bcbench dataset coverage' renders per-domain coverage with a JSON --github-output. - Backfill the security domain (18 entries, 10 articles) as a worked example; 671 passed / 1 skipped. --- dataset/codereview.jsonl | 36 +++---- src/bcbench/commands/dataset.py | 86 +++++++++++++++++ src/bcbench/dataset/__init__.py | 14 +++ src/bcbench/dataset/codereview.py | 15 +++ src/bcbench/dataset/coverage.py | 138 +++++++++++++++++++++++++++ src/bcbench/dataset/dataset_entry.py | 4 + tests/test_codereview_coverage.py | 116 ++++++++++++++++++++++ 7 files changed, 391 insertions(+), 18 deletions(-) create mode 100644 src/bcbench/dataset/coverage.py create mode 100644 tests/test_codereview_coverage.py diff --git a/dataset/codereview.jsonl b/dataset/codereview.jsonl index 0f0a2bb46..d6d0b4155 100644 --- a/dataset/codereview.jsonl +++ b/dataset/codereview.jsonl @@ -1,18 +1,18 @@ -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-001", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureAPIManager.Codeunit.al b/src/SecureAPIManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureAPIManager.Codeunit.al\n@@ -0,0 +1,46 @@\n+codeunit 50100 \"Secure API Manager\"\n+{\n+ Access = Internal;\n+\n+ var\n+ KeyNotConfiguredErr: Label 'API key is not configured for %1.', Comment = '%1 = configuration code';\n+ RequestFailedErr: Label 'The API request failed. Check the configuration.', Comment = 'Shown when an outbound API call fails.';\n+ EndpointTok: Label 'https://api.businesscentral.dynamics.com/v2.0/data', Locked = true;\n+ BearerTok: Label 'Bearer %1', Locked = true;\n+ StorageKeyTok: Label 'ApiKey_%1', Locked = true;\n+\n+ [NonDebuggable]\n+ procedure StoreKey(ConfigCode: Code[20]; KeyValue: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(GetStorageKey(ConfigCode), KeyValue, DataScope::Module);\n+ end;\n+\n+ [NonDebuggable]\n+ procedure GetKey(ConfigCode: Code[20]) Result: SecretText\n+ begin\n+ if not IsolatedStorage.Contains(GetStorageKey(ConfigCode), DataScope::Module) then\n+ Error(KeyNotConfiguredErr, ConfigCode);\n+ IsolatedStorage.Get(GetStorageKey(ConfigCode), DataScope::Module, Result);\n+ end;\n+\n+ procedure CallEndpoint(ConfigCode: Code[20])\n+ var\n+ Client: HttpClient;\n+ Headers: HttpHeaders;\n+ Response: HttpResponseMessage;\n+ AuthHeader: SecretText;\n+ begin\n+ AuthHeader := SecretStrSubstNo(BearerTok, GetKey(ConfigCode));\n+ Headers := Client.DefaultRequestHeaders();\n+ Headers.Add('Authorization', AuthHeader);\n+ if not Client.Get(EndpointTok, Response) then\n+ Error(RequestFailedErr);\n+ if not Response.IsSuccessStatusCode() then\n+ Error(RequestFailedErr);\n+ end;\n+\n+ local procedure GetStorageKey(ConfigCode: Code[20]): Text[50]\n+ begin\n+ exit(CopyStr(StrSubstNo(StorageKeyTok, ConfigCode), 1, 50));\n+ end;\n+}\ndiff --git a/src/HardcodedSecretClient.Codeunit.al b/src/HardcodedSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/HardcodedSecretClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50301 \"Hardcoded Secret Client\"\n+{\n+ procedure CallApi()\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('X-Api-Key', this.GetApiKey());\n+ HttpClient.Get('https://api.contoso.com/data', HttpResponseMessage);\n+ end;\n+\n+ local procedure GetApiKey(): Text\n+ begin\n+ exit('sk-1234567890abcdef');\n+ end;\n+}\n", "expected_comments": [{"file": "src/HardcodedSecretClient.Codeunit.al", "line_start": 16, "line_end": 16, "severity": "critical", "domain": "security", "body": "Hardcoded API key in source code. Retrieve secrets from encrypted isolated storage or another secure store instead."}], "category": "code-review", "description": "Clean codeunit using SecretText, NonDebuggable, IsolatedStorage.SetEncrypted, and HTTPS enforcement with no security issues", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-002", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureKeyManager.Codeunit.al b/src/SecureKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureKeyManager.Codeunit.al\n@@ -0,0 +1,26 @@\n+codeunit 50101 \"Secure Key Manager\"\n+{\n+ Access = Internal;\n+\n+ var\n+ KeyNotFoundErr: Label 'The requested key was not found. Configure it before use.';\n+\n+ [NonDebuggable]\n+ procedure StoreEncryptedKey(KeyName: Code[20]; KeyValue: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, KeyValue, DataScope::Module);\n+ end;\n+\n+ [NonDebuggable]\n+ procedure RetrieveKey(KeyName: Code[20]) Result: SecretText\n+ begin\n+ if not IsolatedStorage.Contains(KeyName, DataScope::Module) then\n+ Error(KeyNotFoundErr);\n+ IsolatedStorage.Get(KeyName, DataScope::Module, Result);\n+ end;\n+\n+ procedure HasKey(KeyName: Code[20]): Boolean\n+ begin\n+ exit(IsolatedStorage.Contains(KeyName, DataScope::Module));\n+ end;\n+}\ndiff --git a/src/PlainTokenHeaderClient.Codeunit.al b/src/PlainTokenHeaderClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PlainTokenHeaderClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50302 \"Plain Token Header Client\"\n+{\n+ procedure SendRequest(AccessToken: Text)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', 'Bearer ' + AccessToken);\n+ HttpClient.Get(this.GetOrdersEndpoint(), HttpResponseMessage);\n+ end;\n+\n+ local procedure GetOrdersEndpoint(): Text\n+ begin\n+ exit('https://api.contoso.com/orders');\n+ end;\n+}\n", "expected_comments": [{"file": "src/PlainTokenHeaderClient.Codeunit.al", "line_start": 10, "line_end": 10, "severity": "high", "domain": "security", "body": "Bearer token is concatenated into a plain Text authorization header. Build the header with SecretStrSubstNo() and add it as SecretText."}], "category": "code-review", "description": "Clean codeunit correctly storing and retrieving API keys using IsolatedStorage.SetEncrypted, SecretText, and NonDebuggable", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-003", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SafeErrorHandler.Codeunit.al b/src/SafeErrorHandler.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SafeErrorHandler.Codeunit.al\n@@ -0,0 +1,41 @@\n+codeunit 50102 \"Safe Error Handler\"\n+{\n+ Access = Internal;\n+\n+ var\n+ InvalidRequestTxt: Label 'Invalid request. Please check your input.';\n+ AuthFailedTxt: Label 'Authentication failed. Please verify your credentials.';\n+ ForbiddenTxt: Label 'You do not have permission for this operation.';\n+ NotFoundTxt: Label 'The requested resource was not found.';\n+ UnexpectedTxt: Label 'An unexpected error occurred. Contact your administrator.';\n+ PostFailedErr: Label 'Could not post document %1.', Comment = '%1 = document number';\n+\n+ procedure GetApiResponseMessage(StatusCode: Integer): Text\n+ begin\n+ case StatusCode of\n+ 200, 201:\n+ exit('');\n+ 400:\n+ exit(InvalidRequestTxt);\n+ 401:\n+ exit(AuthFailedTxt);\n+ 403:\n+ exit(ForbiddenTxt);\n+ 404:\n+ exit(NotFoundTxt);\n+ else\n+ exit(UnexpectedTxt);\n+ end;\n+ end;\n+\n+ procedure PostDocument(DocNo: Code[20])\n+ begin\n+ if not TryPost(DocNo) then\n+ Error(PostFailedErr, DocNo);\n+ end;\n+\n+ [TryFunction]\n+ local procedure TryPost(DocNo: Code[20])\n+ begin\n+ end;\n+}\ndiff --git a/src/UnwrappedSecretClient.Codeunit.al b/src/UnwrappedSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/UnwrappedSecretClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50303 \"Unwrapped Secret Client\"\n+{\n+ procedure SendRequest(SessionToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', this.BuildHeader(SessionToken));\n+ HttpClient.Get('https://api.contoso.com/data', HttpResponseMessage);\n+ end;\n+\n+ local procedure BuildHeader(SessionToken: SecretText): Text\n+ begin\n+ exit('Bearer ' + SessionToken.Unwrap());\n+ end;\n+}\n", "expected_comments": [{"file": "src/UnwrappedSecretClient.Codeunit.al", "line_start": 16, "line_end": 16, "severity": "high", "domain": "security", "body": "SecretText.Unwrap() exposes the secret as plain Text without a [NonDebuggable] procedure. Add [NonDebuggable] or avoid unwrapping by using SecretStrSubstNo()."}], "category": "code-review", "description": "Clean codeunit with proper error handling: generic user-facing messages, no system details exposed, no GetLastErrorText shown to user", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-004", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/AppConstants.Codeunit.al b/src/AppConstants.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/AppConstants.Codeunit.al\n@@ -0,0 +1,30 @@\n+codeunit 50103 \"App Constants\"\n+{\n+ Access = Internal;\n+\n+ var\n+ ApiVersionTok: Label 'v2.0', Locked = true;\n+ DefaultCurrencyTok: Label 'USD', Locked = true;\n+ DateFormatTok: Label 'yyyy-MM-dd', Locked = true;\n+ AppIdTok: Label 'BC-INVENTORY-APP', Locked = true;\n+\n+ procedure GetApiVersion(): Text\n+ begin\n+ exit(ApiVersionTok);\n+ end;\n+\n+ procedure GetDefaultCurrency(): Code[10]\n+ begin\n+ exit(DefaultCurrencyTok);\n+ end;\n+\n+ procedure GetDateFormat(): Text\n+ begin\n+ exit(DateFormatTok);\n+ end;\n+\n+ procedure GetAppId(): Text\n+ begin\n+ exit(AppIdTok);\n+ end;\n+}\ndiff --git a/src/QueryStringSecretClient.Codeunit.al b/src/QueryStringSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/QueryStringSecretClient.Codeunit.al\n@@ -0,0 +1,15 @@\n+codeunit 50304 \"Query String Secret Client\"\n+{\n+ procedure FetchAccount(ApiKey: Text)\n+ var\n+ HttpClient: HttpClient;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpClient.Get(this.BuildAccountUrl(ApiKey), HttpResponseMessage);\n+ end;\n+\n+ local procedure BuildAccountUrl(ApiKey: Text): Text\n+ begin\n+ exit('https://api.contoso.com/accounts?api_key=' + ApiKey);\n+ end;\n+}\n", "expected_comments": [{"file": "src/QueryStringSecretClient.Codeunit.al", "line_start": 13, "line_end": 13, "severity": "high", "domain": "security", "body": "API key is placed in the URL query string. Use an Authorization header, or SetSecretRequestUri() if a secret URI is unavoidable."}], "category": "code-review", "description": "Clean codeunit with configuration constants that are not secrets: API version, currency codes, labels, and format strings", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-005", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ValidatedImportConfig.Table.al b/src/ValidatedImportConfig.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ValidatedImportConfig.Table.al\n@@ -0,0 +1,34 @@\n+table 50104 \"Validated Import Config\"\n+{\n+ Caption = 'Validated Import Configuration';\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Code\"; Code[20])\n+ {\n+ Caption = 'Code';\n+ NotBlank = true;\n+ }\n+ field(2; \"Source Table ID\"; Integer)\n+ {\n+ Caption = 'Source Table';\n+ TableRelation = AllObjWithCaption.\"Object ID\" where(\"Object Type\" = const(Table));\n+ ValidateTableRelation = true;\n+ }\n+ field(3; \"Max Records\"; Integer)\n+ {\n+ Caption = 'Maximum Records';\n+ MinValue = 1;\n+ MaxValue = 10000;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Code\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+}\ndiff --git a/src/BroadFinanceAccess.PermissionSet.al b/src/BroadFinanceAccess.PermissionSet.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/BroadFinanceAccess.PermissionSet.al\n@@ -0,0 +1,15 @@\n+permissionset 50305 \"Broad Finance Access\"\n+{\n+ Assignable = true;\n+ Caption = 'Broad Finance Access', Locked = true;\n+ Permissions =\n+ tabledata * = RIMD,\n+ table * = X,\n+ tabledata Customer = R,\n+ tabledata Vendor = R,\n+ tabledata Item = R,\n+ tabledata \"Sales Header\" = R,\n+ tabledata \"Sales Line\" = R,\n+ codeunit \"Release Sales Document\" = X,\n+ codeunit \"Sales-Post\" = X;\n+}\n", "expected_comments": [{"file": "src/BroadFinanceAccess.PermissionSet.al", "line_start": 6, "line_end": 6, "severity": "critical", "domain": "security", "body": "Permission set grants RIMD on all table data. Replace the wildcard with the minimum specific tabledata permissions required."}, {"file": "src/BroadFinanceAccess.PermissionSet.al", "line_start": 7, "line_end": 7, "severity": "high", "domain": "security", "body": "Permission set grants execute permission on all tables. Grant execute only on the specific objects this role requires."}], "category": "code-review", "description": "Clean table with proper input validation: ValidateTableRelation, OnValidate triggers, MinValue/MaxValue, Editable=false on system fields", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-006", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/InventoryReader.PermissionSet.al b/src/InventoryReader.PermissionSet.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/InventoryReader.PermissionSet.al\n@@ -0,0 +1,11 @@\n+permissionset 50106 \"Inventory Reader\"\n+{\n+ Caption = 'Inventory Reader';\n+ Assignable = true;\n+\n+ Permissions =\n+ tabledata Item = r,\n+ tabledata \"Item Ledger Entry\" = r,\n+ tabledata \"Item Category\" = r,\n+ codeunit \"Inventory Lookup\" = X;\n+}\ndiff --git a/src/InventoryLookup.Codeunit.al b/src/InventoryLookup.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/InventoryLookup.Codeunit.al\n@@ -0,0 +1,15 @@\n+codeunit 50108 \"Inventory Lookup\"\n+{\n+ Access = Internal;\n+ Permissions = tabledata Item = r;\n+\n+ procedure GetItemDescription(ItemNo: Code[20]): Text[100]\n+ var\n+ Item: Record Item;\n+ begin\n+ Item.SetLoadFields(Description);\n+ if Item.Get(ItemNo) then\n+ exit(Item.Description);\n+ exit('');\n+ end;\n+}\ndiff --git a/src/ExcessiveInherentAccess.Codeunit.al b/src/ExcessiveInherentAccess.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExcessiveInherentAccess.Codeunit.al\n@@ -0,0 +1,17 @@\n+codeunit 50306 \"Excessive Inherent Access\"\n+{\n+ procedure LookupCustomerName(CustomerNo: Code[20]): Text\n+ begin\n+ exit(this.GetCustomerName(CustomerNo));\n+ end;\n+\n+ [InherentPermissions(PermissionObjectType::TableData, Database::Customer, 'RIMD')]\n+ local procedure GetCustomerName(CustomerNo: Code[20]): Text\n+ var\n+ Customer: Record Customer;\n+ begin\n+ if Customer.Get(CustomerNo) then\n+ exit(Customer.Name);\n+ exit('');\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExcessiveInherentAccess.Codeunit.al", "line_start": 8, "line_end": 8, "severity": "high", "domain": "security", "body": "InherentPermissions grants RIMD tabledata access even though this procedure only reads Customer. Reduce the permission to the minimal read access required."}], "category": "code-review", "description": "Clean permission sets with least-privilege access: read-only for readers, read-insert for editors, no RIMD grants", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-001", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureAPIManager.Codeunit.al b/src/SecureAPIManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureAPIManager.Codeunit.al\n@@ -0,0 +1,46 @@\n+codeunit 50100 \"Secure API Manager\"\n+{\n+ Access = Internal;\n+\n+ var\n+ KeyNotConfiguredErr: Label 'API key is not configured for %1.', Comment = '%1 = configuration code';\n+ RequestFailedErr: Label 'The API request failed. Check the configuration.', Comment = 'Shown when an outbound API call fails.';\n+ EndpointTok: Label 'https://api.businesscentral.dynamics.com/v2.0/data', Locked = true;\n+ BearerTok: Label 'Bearer %1', Locked = true;\n+ StorageKeyTok: Label 'ApiKey_%1', Locked = true;\n+\n+ [NonDebuggable]\n+ procedure StoreKey(ConfigCode: Code[20]; KeyValue: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(GetStorageKey(ConfigCode), KeyValue, DataScope::Module);\n+ end;\n+\n+ [NonDebuggable]\n+ procedure GetKey(ConfigCode: Code[20]) Result: SecretText\n+ begin\n+ if not IsolatedStorage.Contains(GetStorageKey(ConfigCode), DataScope::Module) then\n+ Error(KeyNotConfiguredErr, ConfigCode);\n+ IsolatedStorage.Get(GetStorageKey(ConfigCode), DataScope::Module, Result);\n+ end;\n+\n+ procedure CallEndpoint(ConfigCode: Code[20])\n+ var\n+ Client: HttpClient;\n+ Headers: HttpHeaders;\n+ Response: HttpResponseMessage;\n+ AuthHeader: SecretText;\n+ begin\n+ AuthHeader := SecretStrSubstNo(BearerTok, GetKey(ConfigCode));\n+ Headers := Client.DefaultRequestHeaders();\n+ Headers.Add('Authorization', AuthHeader);\n+ if not Client.Get(EndpointTok, Response) then\n+ Error(RequestFailedErr);\n+ if not Response.IsSuccessStatusCode() then\n+ Error(RequestFailedErr);\n+ end;\n+\n+ local procedure GetStorageKey(ConfigCode: Code[20]): Text[50]\n+ begin\n+ exit(CopyStr(StrSubstNo(StorageKeyTok, ConfigCode), 1, 50));\n+ end;\n+}\ndiff --git a/src/HardcodedSecretClient.Codeunit.al b/src/HardcodedSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/HardcodedSecretClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50301 \"Hardcoded Secret Client\"\n+{\n+ procedure CallApi()\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('X-Api-Key', this.GetApiKey());\n+ HttpClient.Get('https://api.contoso.com/data', HttpResponseMessage);\n+ end;\n+\n+ local procedure GetApiKey(): Text\n+ begin\n+ exit('sk-1234567890abcdef');\n+ end;\n+}\n", "expected_comments": [{"file": "src/HardcodedSecretClient.Codeunit.al", "line_start": 16, "line_end": 16, "severity": "critical", "domain": "security", "body": "Hardcoded API key in source code. Retrieve secrets from encrypted isolated storage or another secure store instead.", "article": "security/isolatedstorage-setencrypted-for-sensitive-values"}], "category": "code-review", "description": "Clean codeunit using SecretText, NonDebuggable, IsolatedStorage.SetEncrypted, and HTTPS enforcement with no security issues", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-002", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureKeyManager.Codeunit.al b/src/SecureKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureKeyManager.Codeunit.al\n@@ -0,0 +1,26 @@\n+codeunit 50101 \"Secure Key Manager\"\n+{\n+ Access = Internal;\n+\n+ var\n+ KeyNotFoundErr: Label 'The requested key was not found. Configure it before use.';\n+\n+ [NonDebuggable]\n+ procedure StoreEncryptedKey(KeyName: Code[20]; KeyValue: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, KeyValue, DataScope::Module);\n+ end;\n+\n+ [NonDebuggable]\n+ procedure RetrieveKey(KeyName: Code[20]) Result: SecretText\n+ begin\n+ if not IsolatedStorage.Contains(KeyName, DataScope::Module) then\n+ Error(KeyNotFoundErr);\n+ IsolatedStorage.Get(KeyName, DataScope::Module, Result);\n+ end;\n+\n+ procedure HasKey(KeyName: Code[20]): Boolean\n+ begin\n+ exit(IsolatedStorage.Contains(KeyName, DataScope::Module));\n+ end;\n+}\ndiff --git a/src/PlainTokenHeaderClient.Codeunit.al b/src/PlainTokenHeaderClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PlainTokenHeaderClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50302 \"Plain Token Header Client\"\n+{\n+ procedure SendRequest(AccessToken: Text)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', 'Bearer ' + AccessToken);\n+ HttpClient.Get(this.GetOrdersEndpoint(), HttpResponseMessage);\n+ end;\n+\n+ local procedure GetOrdersEndpoint(): Text\n+ begin\n+ exit('https://api.contoso.com/orders');\n+ end;\n+}\n", "expected_comments": [{"file": "src/PlainTokenHeaderClient.Codeunit.al", "line_start": 10, "line_end": 10, "severity": "high", "domain": "security", "body": "Bearer token is concatenated into a plain Text authorization header. Build the header with SecretStrSubstNo() and add it as SecretText.", "article": "security/secretstrsubstno-for-composing-secrets"}], "category": "code-review", "description": "Clean codeunit correctly storing and retrieving API keys using IsolatedStorage.SetEncrypted, SecretText, and NonDebuggable", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-003", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SafeErrorHandler.Codeunit.al b/src/SafeErrorHandler.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SafeErrorHandler.Codeunit.al\n@@ -0,0 +1,41 @@\n+codeunit 50102 \"Safe Error Handler\"\n+{\n+ Access = Internal;\n+\n+ var\n+ InvalidRequestTxt: Label 'Invalid request. Please check your input.';\n+ AuthFailedTxt: Label 'Authentication failed. Please verify your credentials.';\n+ ForbiddenTxt: Label 'You do not have permission for this operation.';\n+ NotFoundTxt: Label 'The requested resource was not found.';\n+ UnexpectedTxt: Label 'An unexpected error occurred. Contact your administrator.';\n+ PostFailedErr: Label 'Could not post document %1.', Comment = '%1 = document number';\n+\n+ procedure GetApiResponseMessage(StatusCode: Integer): Text\n+ begin\n+ case StatusCode of\n+ 200, 201:\n+ exit('');\n+ 400:\n+ exit(InvalidRequestTxt);\n+ 401:\n+ exit(AuthFailedTxt);\n+ 403:\n+ exit(ForbiddenTxt);\n+ 404:\n+ exit(NotFoundTxt);\n+ else\n+ exit(UnexpectedTxt);\n+ end;\n+ end;\n+\n+ procedure PostDocument(DocNo: Code[20])\n+ begin\n+ if not TryPost(DocNo) then\n+ Error(PostFailedErr, DocNo);\n+ end;\n+\n+ [TryFunction]\n+ local procedure TryPost(DocNo: Code[20])\n+ begin\n+ end;\n+}\ndiff --git a/src/UnwrappedSecretClient.Codeunit.al b/src/UnwrappedSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/UnwrappedSecretClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50303 \"Unwrapped Secret Client\"\n+{\n+ procedure SendRequest(SessionToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', this.BuildHeader(SessionToken));\n+ HttpClient.Get('https://api.contoso.com/data', HttpResponseMessage);\n+ end;\n+\n+ local procedure BuildHeader(SessionToken: SecretText): Text\n+ begin\n+ exit('Bearer ' + SessionToken.Unwrap());\n+ end;\n+}\n", "expected_comments": [{"file": "src/UnwrappedSecretClient.Codeunit.al", "line_start": 16, "line_end": 16, "severity": "high", "domain": "security", "body": "SecretText.Unwrap() exposes the secret as plain Text without a [NonDebuggable] procedure. Add [NonDebuggable] or avoid unwrapping by using SecretStrSubstNo().", "article": "security/nondebuggable-required-when-unwrapping-secrettext"}], "category": "code-review", "description": "Clean codeunit with proper error handling: generic user-facing messages, no system details exposed, no GetLastErrorText shown to user", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-004", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/AppConstants.Codeunit.al b/src/AppConstants.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/AppConstants.Codeunit.al\n@@ -0,0 +1,30 @@\n+codeunit 50103 \"App Constants\"\n+{\n+ Access = Internal;\n+\n+ var\n+ ApiVersionTok: Label 'v2.0', Locked = true;\n+ DefaultCurrencyTok: Label 'USD', Locked = true;\n+ DateFormatTok: Label 'yyyy-MM-dd', Locked = true;\n+ AppIdTok: Label 'BC-INVENTORY-APP', Locked = true;\n+\n+ procedure GetApiVersion(): Text\n+ begin\n+ exit(ApiVersionTok);\n+ end;\n+\n+ procedure GetDefaultCurrency(): Code[10]\n+ begin\n+ exit(DefaultCurrencyTok);\n+ end;\n+\n+ procedure GetDateFormat(): Text\n+ begin\n+ exit(DateFormatTok);\n+ end;\n+\n+ procedure GetAppId(): Text\n+ begin\n+ exit(AppIdTok);\n+ end;\n+}\ndiff --git a/src/QueryStringSecretClient.Codeunit.al b/src/QueryStringSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/QueryStringSecretClient.Codeunit.al\n@@ -0,0 +1,15 @@\n+codeunit 50304 \"Query String Secret Client\"\n+{\n+ procedure FetchAccount(ApiKey: Text)\n+ var\n+ HttpClient: HttpClient;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpClient.Get(this.BuildAccountUrl(ApiKey), HttpResponseMessage);\n+ end;\n+\n+ local procedure BuildAccountUrl(ApiKey: Text): Text\n+ begin\n+ exit('https://api.contoso.com/accounts?api_key=' + ApiKey);\n+ end;\n+}\n", "expected_comments": [{"file": "src/QueryStringSecretClient.Codeunit.al", "line_start": 13, "line_end": 13, "severity": "high", "domain": "security", "body": "API key is placed in the URL query string. Use an Authorization header, or SetSecretRequestUri() if a secret URI is unavoidable.", "article": "security/secrettext-with-httpclient"}], "category": "code-review", "description": "Clean codeunit with configuration constants that are not secrets: API version, currency codes, labels, and format strings", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-005", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ValidatedImportConfig.Table.al b/src/ValidatedImportConfig.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ValidatedImportConfig.Table.al\n@@ -0,0 +1,34 @@\n+table 50104 \"Validated Import Config\"\n+{\n+ Caption = 'Validated Import Configuration';\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Code\"; Code[20])\n+ {\n+ Caption = 'Code';\n+ NotBlank = true;\n+ }\n+ field(2; \"Source Table ID\"; Integer)\n+ {\n+ Caption = 'Source Table';\n+ TableRelation = AllObjWithCaption.\"Object ID\" where(\"Object Type\" = const(Table));\n+ ValidateTableRelation = true;\n+ }\n+ field(3; \"Max Records\"; Integer)\n+ {\n+ Caption = 'Maximum Records';\n+ MinValue = 1;\n+ MaxValue = 10000;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Code\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+}\ndiff --git a/src/BroadFinanceAccess.PermissionSet.al b/src/BroadFinanceAccess.PermissionSet.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/BroadFinanceAccess.PermissionSet.al\n@@ -0,0 +1,15 @@\n+permissionset 50305 \"Broad Finance Access\"\n+{\n+ Assignable = true;\n+ Caption = 'Broad Finance Access', Locked = true;\n+ Permissions =\n+ tabledata * = RIMD,\n+ table * = X,\n+ tabledata Customer = R,\n+ tabledata Vendor = R,\n+ tabledata Item = R,\n+ tabledata \"Sales Header\" = R,\n+ tabledata \"Sales Line\" = R,\n+ codeunit \"Release Sales Document\" = X,\n+ codeunit \"Sales-Post\" = X;\n+}\n", "expected_comments": [{"file": "src/BroadFinanceAccess.PermissionSet.al", "line_start": 6, "line_end": 6, "severity": "critical", "domain": "security", "body": "Permission set grants RIMD on all table data. Replace the wildcard with the minimum specific tabledata permissions required.", "article": "security/permission-set-avoid-wildcard-grants"}, {"file": "src/BroadFinanceAccess.PermissionSet.al", "line_start": 7, "line_end": 7, "severity": "high", "domain": "security", "body": "Permission set grants execute permission on all tables. Grant execute only on the specific objects this role requires.", "article": "security/permission-set-avoid-wildcard-grants"}], "category": "code-review", "description": "Clean table with proper input validation: ValidateTableRelation, OnValidate triggers, MinValue/MaxValue, Editable=false on system fields", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-006", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/InventoryReader.PermissionSet.al b/src/InventoryReader.PermissionSet.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/InventoryReader.PermissionSet.al\n@@ -0,0 +1,11 @@\n+permissionset 50106 \"Inventory Reader\"\n+{\n+ Caption = 'Inventory Reader';\n+ Assignable = true;\n+\n+ Permissions =\n+ tabledata Item = r,\n+ tabledata \"Item Ledger Entry\" = r,\n+ tabledata \"Item Category\" = r,\n+ codeunit \"Inventory Lookup\" = X;\n+}\ndiff --git a/src/InventoryLookup.Codeunit.al b/src/InventoryLookup.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/InventoryLookup.Codeunit.al\n@@ -0,0 +1,15 @@\n+codeunit 50108 \"Inventory Lookup\"\n+{\n+ Access = Internal;\n+ Permissions = tabledata Item = r;\n+\n+ procedure GetItemDescription(ItemNo: Code[20]): Text[100]\n+ var\n+ Item: Record Item;\n+ begin\n+ Item.SetLoadFields(Description);\n+ if Item.Get(ItemNo) then\n+ exit(Item.Description);\n+ exit('');\n+ end;\n+}\ndiff --git a/src/ExcessiveInherentAccess.Codeunit.al b/src/ExcessiveInherentAccess.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExcessiveInherentAccess.Codeunit.al\n@@ -0,0 +1,17 @@\n+codeunit 50306 \"Excessive Inherent Access\"\n+{\n+ procedure LookupCustomerName(CustomerNo: Code[20]): Text\n+ begin\n+ exit(this.GetCustomerName(CustomerNo));\n+ end;\n+\n+ [InherentPermissions(PermissionObjectType::TableData, Database::Customer, 'RIMD')]\n+ local procedure GetCustomerName(CustomerNo: Code[20]): Text\n+ var\n+ Customer: Record Customer;\n+ begin\n+ if Customer.Get(CustomerNo) then\n+ exit(Customer.Name);\n+ exit('');\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExcessiveInherentAccess.Codeunit.al", "line_start": 8, "line_end": 8, "severity": "high", "domain": "security", "body": "InherentPermissions grants RIMD tabledata access even though this procedure only reads Customer. Reduce the permission to the minimal read access required.", "article": "security/inherent-permissions-minimal-grant"}], "category": "code-review", "description": "Clean permission sets with least-privilege access: read-only for readers, read-insert for editors, no RIMD grants", "expect_findings": true, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__security-007", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SafeRecordQuery.Codeunit.al b/src/SafeRecordQuery.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SafeRecordQuery.Codeunit.al\n@@ -0,0 +1,31 @@\n+codeunit 50109 \"Safe Record Query\"\n+{\n+ Access = Internal;\n+\n+ procedure CustomerExists(CustomerNo: Code[20]): Boolean\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.SetLoadFields(\"No.\");\n+ exit(Customer.Get(CustomerNo));\n+ end;\n+\n+ procedure HasOpenSalesOrders(CustomerNo: Code[20]): Boolean\n+ var\n+ SalesHeader: Record \"Sales Header\";\n+ begin\n+ SalesHeader.SetRange(\"Document Type\", SalesHeader.\"Document Type\"::Order);\n+ SalesHeader.SetRange(\"Sell-to Customer No.\", CustomerNo);\n+ exit(not SalesHeader.IsEmpty());\n+ end;\n+\n+ procedure GetItemDescription(ItemNo: Code[20]): Text[100]\n+ var\n+ Item: Record Item;\n+ begin\n+ Item.SetLoadFields(Description);\n+ if Item.Get(ItemNo) then\n+ exit(Item.Description);\n+ exit('');\n+ end;\n+}\ndiff --git a/src/InsecureEndpointClient.Codeunit.al b/src/InsecureEndpointClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/InsecureEndpointClient.Codeunit.al\n@@ -0,0 +1,17 @@\n+codeunit 50307 \"Insecure Endpoint Client\"\n+{\n+ procedure SendSession(SessionToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpContent: HttpContent;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpContent.WriteFrom(SessionToken);\n+ HttpClient.Post(this.GetEndpoint(), HttpContent, HttpResponseMessage);\n+ end;\n+\n+ local procedure GetEndpoint(): Text\n+ begin\n+ exit('http://api.contoso.com/session');\n+ end;\n+}\n", "expected_comments": [{"file": "src/InsecureEndpointClient.Codeunit.al", "line_start": 15, "line_end": 15, "severity": "high", "domain": "security", "body": "External service endpoint uses HTTP instead of HTTPS. Use HTTPS for all external HTTP calls, especially when sending session tokens."}], "category": "code-review", "description": "Clean codeunit using proper BC record operations: SetRange, SetFilter, FindSet, Count — no string concatenation or dynamic SQL", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-008", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/PartnerConfigKeyManager.Codeunit.al b/src/PartnerConfigKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PartnerConfigKeyManager.Codeunit.al\n@@ -0,0 +1,9 @@\n+codeunit 50100 \"Partner Config Key Manager\"\n+{\n+ Access = Internal;\n+\n+ internal procedure StoreApiKey(KeyName: Text; ApiKey: Text)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, ApiKey, DataScope::Module);\n+ end;\n+}\n", "expected_comments": [{"file": "src/PartnerConfigKeyManager.Codeunit.al", "line_start": 5, "line_end": 5, "domain": "security", "severity": "high", "body": "The API key is accepted as a plain Text parameter instead of SecretText, so the secret is exposed in memory and to anyone inspecting the call stack or debugger."}], "category": "code-review", "description": "True positive security findings: encryption (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-009", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/OutlookAddinDeployer.Codeunit.al b/src/OutlookAddinDeployer.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/OutlookAddinDeployer.Codeunit.al\n@@ -0,0 +1,37 @@\n+namespace Microsoft.Integration.Outlook;\n+\n+codeunit 50104 \"Outlook Addin Deployer\"\n+{\n+ Access = Internal;\n+\n+ var\n+ EndpointTok: Label 'https://outlook.office365.com/api/v2.0/addins/deploy', Locked = true;\n+ StatusErr: Label 'Deployment failed (HTTP %1): %2', Comment = '%1 is the HTTP status code, %2 is the raw response body.';\n+ ConnectErr: Label 'Failed to connect to the deployment service: %1', Comment = '%1 is the underlying error text.';\n+\n+ procedure DeployAddin(ManifestPath: Text)\n+ var\n+ Client: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ Headers: HttpHeaders;\n+ Payload: Text;\n+ ResponseText: Text;\n+ begin\n+ Payload := '{\"manifest\":\"' + ManifestPath + '\"}';\n+ Content.WriteFrom(Payload);\n+ Content.GetHeaders(Headers);\n+ Headers.Add('Authorization', 'Bearer ' + GetAccessToken());\n+ if Client.Post(EndpointTok, Content, Response) then begin\n+ Response.Content.ReadAs(ResponseText);\n+ if not Response.IsSuccessStatusCode() then\n+ Error(StatusErr, Response.HttpStatusCode(), ResponseText);\n+ end else\n+ Error(ConnectErr, GetLastErrorText());\n+ end;\n+\n+ local procedure GetAccessToken(): Text\n+ begin\n+ exit('dummy_access_token_for_testing');\n+ end;\n+}\n", "expected_comments": [{"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 21, "line_end": 21, "domain": "security", "severity": "medium", "body": "The manifest path is concatenated directly into a JSON payload, allowing JSON injection. Build the payload with a JsonObject so values are escaped."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 28, "line_end": 28, "domain": "security", "severity": "medium", "body": "The error surfaces the raw HTTP status code and full response body to the user, leaking internal service details. Log the details and show a generic message."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 30, "line_end": 30, "domain": "security", "severity": "medium", "body": "GetLastErrorText() is shown to the user, exposing internal system details. Log the raw error and present a sanitized message."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 33, "line_end": 33, "domain": "security", "severity": "high", "body": "The access token is returned as plain Text instead of SecretText, exposing it in memory and to the debugger. Return and handle it as SecretText."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 35, "line_end": 35, "domain": "security", "severity": "high", "body": "A hardcoded access token is embedded in source code. Retrieve the token from a secure store or OAuth flow instead of hardcoding it."}], "category": "code-review", "description": "True positive security findings: error_exposure (verified line numbers)", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-011", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ElecVATSubmission.Codeunit.al b/src/ElecVATSubmission.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ElecVATSubmission.Codeunit.al\n@@ -0,0 +1,24 @@\n+namespace Microsoft.Finance.VAT;\n+\n+codeunit 13610 \"Elec VAT Submission\"\n+{\n+ Access = Internal;\n+\n+ procedure SubmitReturn(AuthorityUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ begin\n+ Content.WriteFrom('{}');\n+ exit(Client.Post(AuthorityUrl, Content, Response));\n+ end;\n+\n+ procedure CheckHealth(ServiceUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Response: HttpResponseMessage;\n+ begin\n+ exit(Client.Get(ServiceUrl, Response));\n+ end;\n+}\n", "expected_comments": [{"file": "src/ElecVATSubmission.Codeunit.al", "line_start": 14, "line_end": 14, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Post without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints."}, {"file": "src/ElecVATSubmission.Codeunit.al", "line_start": 22, "line_end": 22, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Get without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints."}], "category": "code-review", "description": "True positive security findings: input_validation (trimmed to core input validation cases)", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-012", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/HttpAuthenticationBasic.Codeunit.al b/src/HttpAuthenticationBasic.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/HttpAuthenticationBasic.Codeunit.al\n@@ -0,0 +1,37 @@\n+codeunit 2359 \"Http Authentication Basic\"\n+{\n+ Access = Public;\n+ InherentEntitlements = X;\n+ InherentPermissions = X;\n+\n+ var\n+ Credential: SecretText;\n+ UsernameDomainTok: Label '%1\\%2', Comment = '%1 = domain, %2 = user name', Locked = true;\n+\n+ [NonDebuggable]\n+ procedure Initialize(Username: SecretText; Domain: Text; Password: SecretText)\n+ begin\n+ Credential := SecretStrSubstNo('%1:%2', QualifyUser(Username, Domain), Password);\n+ end;\n+\n+ procedure GetAuthorizationHeader() Header: SecretText\n+ begin\n+ Header := ToBase64(Credential);\n+ end;\n+\n+ [NonDebuggable]\n+ local procedure QualifyUser(Username: SecretText; Domain: Text): SecretText\n+ begin\n+ if Domain = '' then\n+ exit(Username);\n+ exit(SecretStrSubstNo(UsernameDomainTok, Domain, Username));\n+ end;\n+\n+ local procedure ToBase64(Value: SecretText) Base64Value: SecretText\n+ var\n+ Convert: DotNet Convert;\n+ Encoding: DotNet Encoding;\n+ begin\n+ Base64Value := Convert.ToBase64String(Encoding.UTF8().GetBytes(Value.Unwrap()));\n+ end;\n+}\n", "expected_comments": [{"file": "src/HttpAuthenticationBasic.Codeunit.al", "line_start": 30, "line_end": 30, "domain": "security", "severity": "medium", "body": "ToBase64 transforms SecretText credential material and calls Unwrap() without [NonDebuggable], so the plaintext credential is visible in the debugger."}], "category": "code-review", "description": "True positive security findings: procedures handling passwords or SecretText values without [NonDebuggable]", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-013", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ExpenseAgentAdmin.PermissionSet.al b/src/ExpenseAgentAdmin.PermissionSet.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExpenseAgentAdmin.PermissionSet.al\n@@ -0,0 +1,15 @@\n+// ------------------------------------------------------------------------------------------------\n+// Copyright (c) Microsoft Corporation. All rights reserved.\n+// Licensed under the MIT License. See License.txt in the project root for license information.\n+// ------------------------------------------------------------------------------------------------\n+namespace Microsoft.Agents.Expense;\n+\n+permissionset 50700 \"Expense Agent Admin\"\n+{\n+ Assignable = true;\n+ Caption = 'Expense Agent Administration';\n+\n+ Permissions =\n+ tabledata \"Agent Creation Control\" = RIMD,\n+ tabledata \"Expense Report Rule Violation\" = IMD;\n+}\ndiff --git a/src/ExpenseAgentConsumption.Table.al b/src/ExpenseAgentConsumption.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExpenseAgentConsumption.Table.al\n@@ -0,0 +1,51 @@\n+// ------------------------------------------------------------------------------------------------\n+// Copyright (c) Microsoft Corporation. All rights reserved.\n+// Licensed under the MIT License. See License.txt in the project root for license information.\n+// ------------------------------------------------------------------------------------------------\n+namespace Microsoft.Agents.Expense;\n+\n+table 50600 \"Expense Agent Consumption\"\n+{\n+ Caption = 'Expense Agent Consumption';\n+ DataClassification = CustomerContent;\n+ InherentEntitlements = RIX;\n+ InherentPermissions = RIX;\n+\n+ fields\n+ {\n+ field(1; \"Entry No.\"; Integer)\n+ {\n+ Caption = 'Entry No.';\n+ DataClassification = SystemMetadata;\n+ AutoIncrement = true;\n+ }\n+ field(10; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DataClassification = CustomerContent;\n+ }\n+ field(20; \"User Security ID\"; Guid)\n+ {\n+ Caption = 'User Security ID';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Entry No.\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ procedure LogConsumption(CallerSecurityId: Guid; ConsumptionAmount: Decimal)\n+ var\n+ ConsumptionEntry: Record \"Expense Agent Consumption\";\n+ begin\n+ ConsumptionEntry.Init();\n+ ConsumptionEntry.\"User Security ID\" := CallerSecurityId;\n+ ConsumptionEntry.Amount := ConsumptionAmount;\n+ ConsumptionEntry.Insert();\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExpenseAgentConsumption.Table.al", "line_start": 11, "line_end": 11, "domain": "security", "severity": "medium", "body": "InherentEntitlements and InherentPermissions of RIX grant read/insert/execute to every user regardless of assigned permission sets. Remove the inherent grants and control access explicitly."}, {"file": "src/ExpenseAgentConsumption.Table.al", "line_start": 42, "line_end": 42, "domain": "security", "severity": "medium", "body": "The procedure accepts an arbitrary UserSecurityId, letting a caller log consumption against any user's identity. Derive the user from UserSecurityId() instead of trusting the parameter."}, {"file": "src/ExpenseAgentAdmin.PermissionSet.al", "line_start": 13, "line_end": 13, "domain": "security", "severity": "medium", "body": "RIMD on Agent Creation Control lets assigned users delete creation-control records, removing a security guardrail. Grant only the permissions actually required."}, {"file": "src/ExpenseAgentAdmin.PermissionSet.al", "line_start": 14, "line_end": 14, "domain": "security", "severity": "medium", "body": "IMD on Expense Report Rule Violation lets users delete recorded policy violations, enabling them to hide their own violations. Remove delete and modify access."}], "category": "code-review", "description": "True positive security findings: permission (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-014", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureOperationHelper.Codeunit.al b/src/SecureOperationHelper.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureOperationHelper.Codeunit.al\n@@ -0,0 +1,13 @@\n+codeunit 50105 \"Secure Operation Helper\"\n+{\n+ Access = Internal;\n+\n+ internal procedure DeleteAllRecords(TableNo: Integer)\n+ var\n+ RecRef: RecordRef;\n+ begin\n+ RecRef.Open(TableNo);\n+ RecRef.DeleteAll();\n+ RecRef.Close();\n+ end;\n+}\n", "expected_comments": [{"file": "src/SecureOperationHelper.Codeunit.al", "line_start": 9, "line_end": 10, "domain": "security", "severity": "high", "body": "A caller-provided table number is opened with RecordRef.Open and then DeleteAll is called, letting any caller delete every record in an arbitrary table. Restrict the allowed tables and enforce permission checks."}], "category": "code-review", "description": "True positive: public procedure uses RecordRef.Open with caller-provided table number, allowing any extension to delete all records from any table through this codeunit's permissions", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-015", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ExternalIntegrationMgt.Codeunit.al b/src/ExternalIntegrationMgt.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExternalIntegrationMgt.Codeunit.al\n@@ -0,0 +1,24 @@\n+namespace Microsoft.Integration.Partner;\n+\n+codeunit 50205 \"External Integration Mgt.\"\n+{\n+ Access = Internal;\n+\n+ procedure PostToPartner(EndpointUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ begin\n+ Content.WriteFrom('{}');\n+ exit(Client.Post(EndpointUrl, Content, Response));\n+ end;\n+\n+ procedure GetFromProvider(EndpointUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Response: HttpResponseMessage;\n+ begin\n+ exit(Client.Get(EndpointUrl, Response));\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExternalIntegrationMgt.Codeunit.al", "line_start": 14, "line_end": 14, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Post without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints."}, {"file": "src/ExternalIntegrationMgt.Codeunit.al", "line_start": 22, "line_end": 22, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Get without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints."}], "category": "code-review", "description": "True positive security findings: URLs from table fields used in HTTP requests without validation (SSRF risk). Three procedures use user-configurable URLs directly, while two procedures correctly validate using Uri.AreURIsHaveSameHost and Uri.IsValidURIPattern.", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-016", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ExpenseHtmlNotifier.Codeunit.al b/src/ExpenseHtmlNotifier.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExpenseHtmlNotifier.Codeunit.al\n@@ -0,0 +1,12 @@\n+codeunit 50900 \"Expense Html Notifier\"\n+{\n+ Access = Internal;\n+\n+ var\n+ BodyTemplateTok: Label '

Dear %1,

%2

', Locked = true;\n+\n+ internal procedure BuildNotificationBody(EmployeeName: Text; Description: Text): Text\n+ begin\n+ exit(StrSubstNo(BodyTemplateTok, EmployeeName, Description));\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExpenseHtmlNotifier.Codeunit.al", "line_start": 10, "line_end": 10, "domain": "security", "severity": "high", "body": "User-supplied EmployeeName and Description are substituted into the HTML body without encoding, enabling stored or reflected XSS. HTML-encode the values before embedding them."}], "category": "code-review", "description": "True positive security findings: xss (user-supplied data embedded in HTML without encoding)", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-008", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/PartnerConfigKeyManager.Codeunit.al b/src/PartnerConfigKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PartnerConfigKeyManager.Codeunit.al\n@@ -0,0 +1,9 @@\n+codeunit 50100 \"Partner Config Key Manager\"\n+{\n+ Access = Internal;\n+\n+ internal procedure StoreApiKey(KeyName: Text; ApiKey: Text)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, ApiKey, DataScope::Module);\n+ end;\n+}\n", "expected_comments": [{"file": "src/PartnerConfigKeyManager.Codeunit.al", "line_start": 5, "line_end": 5, "domain": "security", "severity": "high", "body": "The API key is accepted as a plain Text parameter instead of SecretText, so the secret is exposed in memory and to anyone inspecting the call stack or debugger.", "article": "security/secrettext-for-credentials"}], "category": "code-review", "description": "True positive security findings: encryption (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-009", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/OutlookAddinDeployer.Codeunit.al b/src/OutlookAddinDeployer.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/OutlookAddinDeployer.Codeunit.al\n@@ -0,0 +1,37 @@\n+namespace Microsoft.Integration.Outlook;\n+\n+codeunit 50104 \"Outlook Addin Deployer\"\n+{\n+ Access = Internal;\n+\n+ var\n+ EndpointTok: Label 'https://outlook.office365.com/api/v2.0/addins/deploy', Locked = true;\n+ StatusErr: Label 'Deployment failed (HTTP %1): %2', Comment = '%1 is the HTTP status code, %2 is the raw response body.';\n+ ConnectErr: Label 'Failed to connect to the deployment service: %1', Comment = '%1 is the underlying error text.';\n+\n+ procedure DeployAddin(ManifestPath: Text)\n+ var\n+ Client: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ Headers: HttpHeaders;\n+ Payload: Text;\n+ ResponseText: Text;\n+ begin\n+ Payload := '{\"manifest\":\"' + ManifestPath + '\"}';\n+ Content.WriteFrom(Payload);\n+ Content.GetHeaders(Headers);\n+ Headers.Add('Authorization', 'Bearer ' + GetAccessToken());\n+ if Client.Post(EndpointTok, Content, Response) then begin\n+ Response.Content.ReadAs(ResponseText);\n+ if not Response.IsSuccessStatusCode() then\n+ Error(StatusErr, Response.HttpStatusCode(), ResponseText);\n+ end else\n+ Error(ConnectErr, GetLastErrorText());\n+ end;\n+\n+ local procedure GetAccessToken(): Text\n+ begin\n+ exit('dummy_access_token_for_testing');\n+ end;\n+}\n", "expected_comments": [{"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 21, "line_end": 21, "domain": "security", "severity": "medium", "body": "The manifest path is concatenated directly into a JSON payload, allowing JSON injection. Build the payload with a JsonObject so values are escaped."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 28, "line_end": 28, "domain": "security", "severity": "medium", "body": "The error surfaces the raw HTTP status code and full response body to the user, leaking internal service details. Log the details and show a generic message."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 30, "line_end": 30, "domain": "security", "severity": "medium", "body": "GetLastErrorText() is shown to the user, exposing internal system details. Log the raw error and present a sanitized message."}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 33, "line_end": 33, "domain": "security", "severity": "high", "body": "The access token is returned as plain Text instead of SecretText, exposing it in memory and to the debugger. Return and handle it as SecretText.", "article": "security/secrettext-for-credentials"}, {"file": "src/OutlookAddinDeployer.Codeunit.al", "line_start": 35, "line_end": 35, "domain": "security", "severity": "high", "body": "A hardcoded access token is embedded in source code. Retrieve the token from a secure store or OAuth flow instead of hardcoding it.", "article": "security/isolatedstorage-setencrypted-for-sensitive-values"}], "category": "code-review", "description": "True positive security findings: error_exposure (verified line numbers)", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-011", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ElecVATSubmission.Codeunit.al b/src/ElecVATSubmission.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ElecVATSubmission.Codeunit.al\n@@ -0,0 +1,24 @@\n+namespace Microsoft.Finance.VAT;\n+\n+codeunit 13610 \"Elec VAT Submission\"\n+{\n+ Access = Internal;\n+\n+ procedure SubmitReturn(AuthorityUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ begin\n+ Content.WriteFrom('{}');\n+ exit(Client.Post(AuthorityUrl, Content, Response));\n+ end;\n+\n+ procedure CheckHealth(ServiceUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Response: HttpResponseMessage;\n+ begin\n+ exit(Client.Get(ServiceUrl, Response));\n+ end;\n+}\n", "expected_comments": [{"file": "src/ElecVATSubmission.Codeunit.al", "line_start": 14, "line_end": 14, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Post without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints.", "article": "security/validate-user-configurable-urls"}, {"file": "src/ElecVATSubmission.Codeunit.al", "line_start": 22, "line_end": 22, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Get without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints.", "article": "security/validate-user-configurable-urls"}], "category": "code-review", "description": "True positive security findings: input_validation (trimmed to core input validation cases)", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-012", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/HttpAuthenticationBasic.Codeunit.al b/src/HttpAuthenticationBasic.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/HttpAuthenticationBasic.Codeunit.al\n@@ -0,0 +1,37 @@\n+codeunit 2359 \"Http Authentication Basic\"\n+{\n+ Access = Public;\n+ InherentEntitlements = X;\n+ InherentPermissions = X;\n+\n+ var\n+ Credential: SecretText;\n+ UsernameDomainTok: Label '%1\\%2', Comment = '%1 = domain, %2 = user name', Locked = true;\n+\n+ [NonDebuggable]\n+ procedure Initialize(Username: SecretText; Domain: Text; Password: SecretText)\n+ begin\n+ Credential := SecretStrSubstNo('%1:%2', QualifyUser(Username, Domain), Password);\n+ end;\n+\n+ procedure GetAuthorizationHeader() Header: SecretText\n+ begin\n+ Header := ToBase64(Credential);\n+ end;\n+\n+ [NonDebuggable]\n+ local procedure QualifyUser(Username: SecretText; Domain: Text): SecretText\n+ begin\n+ if Domain = '' then\n+ exit(Username);\n+ exit(SecretStrSubstNo(UsernameDomainTok, Domain, Username));\n+ end;\n+\n+ local procedure ToBase64(Value: SecretText) Base64Value: SecretText\n+ var\n+ Convert: DotNet Convert;\n+ Encoding: DotNet Encoding;\n+ begin\n+ Base64Value := Convert.ToBase64String(Encoding.UTF8().GetBytes(Value.Unwrap()));\n+ end;\n+}\n", "expected_comments": [{"file": "src/HttpAuthenticationBasic.Codeunit.al", "line_start": 30, "line_end": 30, "domain": "security", "severity": "medium", "body": "ToBase64 transforms SecretText credential material and calls Unwrap() without [NonDebuggable], so the plaintext credential is visible in the debugger.", "article": "security/nondebuggable-required-when-unwrapping-secrettext"}], "category": "code-review", "description": "True positive security findings: procedures handling passwords or SecretText values without [NonDebuggable]", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-013", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ExpenseAgentAdmin.PermissionSet.al b/src/ExpenseAgentAdmin.PermissionSet.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExpenseAgentAdmin.PermissionSet.al\n@@ -0,0 +1,15 @@\n+// ------------------------------------------------------------------------------------------------\n+// Copyright (c) Microsoft Corporation. All rights reserved.\n+// Licensed under the MIT License. See License.txt in the project root for license information.\n+// ------------------------------------------------------------------------------------------------\n+namespace Microsoft.Agents.Expense;\n+\n+permissionset 50700 \"Expense Agent Admin\"\n+{\n+ Assignable = true;\n+ Caption = 'Expense Agent Administration';\n+\n+ Permissions =\n+ tabledata \"Agent Creation Control\" = RIMD,\n+ tabledata \"Expense Report Rule Violation\" = IMD;\n+}\ndiff --git a/src/ExpenseAgentConsumption.Table.al b/src/ExpenseAgentConsumption.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExpenseAgentConsumption.Table.al\n@@ -0,0 +1,51 @@\n+// ------------------------------------------------------------------------------------------------\n+// Copyright (c) Microsoft Corporation. All rights reserved.\n+// Licensed under the MIT License. See License.txt in the project root for license information.\n+// ------------------------------------------------------------------------------------------------\n+namespace Microsoft.Agents.Expense;\n+\n+table 50600 \"Expense Agent Consumption\"\n+{\n+ Caption = 'Expense Agent Consumption';\n+ DataClassification = CustomerContent;\n+ InherentEntitlements = RIX;\n+ InherentPermissions = RIX;\n+\n+ fields\n+ {\n+ field(1; \"Entry No.\"; Integer)\n+ {\n+ Caption = 'Entry No.';\n+ DataClassification = SystemMetadata;\n+ AutoIncrement = true;\n+ }\n+ field(10; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DataClassification = CustomerContent;\n+ }\n+ field(20; \"User Security ID\"; Guid)\n+ {\n+ Caption = 'User Security ID';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Entry No.\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ procedure LogConsumption(CallerSecurityId: Guid; ConsumptionAmount: Decimal)\n+ var\n+ ConsumptionEntry: Record \"Expense Agent Consumption\";\n+ begin\n+ ConsumptionEntry.Init();\n+ ConsumptionEntry.\"User Security ID\" := CallerSecurityId;\n+ ConsumptionEntry.Amount := ConsumptionAmount;\n+ ConsumptionEntry.Insert();\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExpenseAgentConsumption.Table.al", "line_start": 11, "line_end": 11, "domain": "security", "severity": "medium", "body": "InherentEntitlements and InherentPermissions of RIX grant read/insert/execute to every user regardless of assigned permission sets. Remove the inherent grants and control access explicitly.", "article": "security/inherent-permissions-minimal-grant"}, {"file": "src/ExpenseAgentConsumption.Table.al", "line_start": 42, "line_end": 42, "domain": "security", "severity": "medium", "body": "The procedure accepts an arbitrary UserSecurityId, letting a caller log consumption against any user's identity. Derive the user from UserSecurityId() instead of trusting the parameter."}, {"file": "src/ExpenseAgentAdmin.PermissionSet.al", "line_start": 13, "line_end": 13, "domain": "security", "severity": "medium", "body": "RIMD on Agent Creation Control lets assigned users delete creation-control records, removing a security guardrail. Grant only the permissions actually required.", "article": "security/inherent-permissions-minimal-grant"}, {"file": "src/ExpenseAgentAdmin.PermissionSet.al", "line_start": 14, "line_end": 14, "domain": "security", "severity": "medium", "body": "IMD on Expense Report Rule Violation lets users delete recorded policy violations, enabling them to hide their own violations. Remove delete and modify access.", "article": "security/inherent-permissions-minimal-grant"}], "category": "code-review", "description": "True positive security findings: permission (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-014", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureOperationHelper.Codeunit.al b/src/SecureOperationHelper.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureOperationHelper.Codeunit.al\n@@ -0,0 +1,13 @@\n+codeunit 50105 \"Secure Operation Helper\"\n+{\n+ Access = Internal;\n+\n+ internal procedure DeleteAllRecords(TableNo: Integer)\n+ var\n+ RecRef: RecordRef;\n+ begin\n+ RecRef.Open(TableNo);\n+ RecRef.DeleteAll();\n+ RecRef.Close();\n+ end;\n+}\n", "expected_comments": [{"file": "src/SecureOperationHelper.Codeunit.al", "line_start": 9, "line_end": 10, "domain": "security", "severity": "high", "body": "A caller-provided table number is opened with RecordRef.Open and then DeleteAll is called, letting any caller delete every record in an arbitrary table. Restrict the allowed tables and enforce permission checks.", "article": "security/recordref-open-with-caller-table-must-not-be-public"}], "category": "code-review", "description": "True positive: public procedure uses RecordRef.Open with caller-provided table number, allowing any extension to delete all records from any table through this codeunit's permissions", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-015", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ExternalIntegrationMgt.Codeunit.al b/src/ExternalIntegrationMgt.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExternalIntegrationMgt.Codeunit.al\n@@ -0,0 +1,24 @@\n+namespace Microsoft.Integration.Partner;\n+\n+codeunit 50205 \"External Integration Mgt.\"\n+{\n+ Access = Internal;\n+\n+ procedure PostToPartner(EndpointUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ begin\n+ Content.WriteFrom('{}');\n+ exit(Client.Post(EndpointUrl, Content, Response));\n+ end;\n+\n+ procedure GetFromProvider(EndpointUrl: Text): Boolean\n+ var\n+ Client: HttpClient;\n+ Response: HttpResponseMessage;\n+ begin\n+ exit(Client.Get(EndpointUrl, Response));\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExternalIntegrationMgt.Codeunit.al", "line_start": 14, "line_end": 14, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Post without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints.", "article": "security/validate-user-configurable-urls"}, {"file": "src/ExternalIntegrationMgt.Codeunit.al", "line_start": 22, "line_end": 22, "domain": "security", "severity": "high", "body": "A caller-supplied URL is passed to HttpClient.Get without host or scheme validation, allowing SSRF requests to internal or attacker-chosen endpoints.", "article": "security/validate-user-configurable-urls"}], "category": "code-review", "description": "True positive security findings: URLs from table fields used in HTTP requests without validation (SSRF risk). Three procedures use user-configurable URLs directly, while two procedures correctly validate using Uri.AreURIsHaveSameHost and Uri.IsValidURIPattern.", "expect_findings": true, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-016", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/ExpenseHtmlNotifier.Codeunit.al b/src/ExpenseHtmlNotifier.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ExpenseHtmlNotifier.Codeunit.al\n@@ -0,0 +1,12 @@\n+codeunit 50900 \"Expense Html Notifier\"\n+{\n+ Access = Internal;\n+\n+ var\n+ BodyTemplateTok: Label '

Dear %1,

%2

', Locked = true;\n+\n+ internal procedure BuildNotificationBody(EmployeeName: Text; Description: Text): Text\n+ begin\n+ exit(StrSubstNo(BodyTemplateTok, EmployeeName, Description));\n+ end;\n+}\n", "expected_comments": [{"file": "src/ExpenseHtmlNotifier.Codeunit.al", "line_start": 10, "line_end": 10, "domain": "security", "severity": "high", "body": "User-supplied EmployeeName and Description are substituted into the HTML body without encoding, enabling stored or reflected XSS. HTML-encode the values before embedding them.", "article": "security/al-has-no-built-in-htmlencode"}], "category": "code-review", "description": "True positive security findings: xss (user-supplied data embedded in HTML without encoding)", "expect_findings": true, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-001", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "diff --git a/src/FADepreciationBook.Table.al b/src/FADepreciationBook.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/FADepreciationBook.Table.al\n@@ -0,0 +1,78 @@\n+table 50200 \"FA Depreciation Book FP\"\n+{\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"FA No.\"; Code[20])\n+ {\n+ Caption = 'FA No.';\n+ TableRelation = \"Fixed Asset\";\n+ }\n+\n+ field(2; \"Depreciation Book Code\"; Code[10])\n+ {\n+ Caption = 'Depreciation Book Code';\n+ TableRelation = \"Depreciation Book\";\n+ }\n+\n+ field(3; Depreciation; Decimal)\n+ {\n+ FieldClass = FlowField;\n+ CalcFormula = sum(\"FA Ledger Entry\".Amount where(\"FA No.\" = field(\"FA No.\"),\n+ \"Depreciation Book Code\" = field(\"Depreciation Book Code\"),\n+ \"FA Posting Category\" = const(Depreciation)));\n+ Caption = 'Depreciation';\n+ }\n+\n+ field(4; \"Bonus Depr. Applied Amount\"; Decimal)\n+ {\n+ FieldClass = FlowField;\n+ CalcFormula = sum(\"FA Ledger Entry\".Amount where(\"FA No.\" = field(\"FA No.\"),\n+ \"Depreciation Book Code\" = field(\"Depreciation Book Code\"),\n+ \"FA Posting Type\" = const(\"Bonus Depreciation\")));\n+ Caption = 'Bonus Depr. Applied Amount';\n+ }\n+\n+ field(5; \"Use Half-Year Convention\"; Boolean)\n+ {\n+ Caption = 'Use Half-Year Convention';\n+\n+ trigger OnValidate()\n+ var\n+ CannotChangeHalfYearErr: Label 'Cannot change half-year convention after depreciation has been posted.';\n+ CannotChangeBonusErr: Label 'Cannot change half-year convention when bonus depreciation has been applied.';\n+ begin\n+ // CORRECT: CalcFields in OnValidate runs once per user edit, not in a loop\n+ // This is appropriate for validation logic that needs current flowfield values\n+ CalcFields(Depreciation);\n+ if Depreciation <> 0 then\n+ Error(CannotChangeHalfYearErr);\n+\n+ CalcFields(\"Bonus Depr. Applied Amount\");\n+ if \"Bonus Depr. Applied Amount\" <> 0 then\n+ Error(CannotChangeBonusErr);\n+ end;\n+ }\n+\n+ field(6; \"Depreciation Method\"; Option)\n+ {\n+ OptionCaption = 'Straight-Line,Declining-Balance 1,Declining-Balance 2';\n+ OptionMembers = \"Straight-Line\",\"Declining-Balance 1\",\"Declining-Balance 2\";\n+ Caption = 'Depreciation Method';\n+ }\n+\n+ field(7; \"Starting Date\"; Date)\n+ {\n+ Caption = 'Depreciation Starting Date';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"FA No.\", \"Depreciation Book Code\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+}\ndiff --git a/src/SalesOrderCard.Page.al b/src/SalesOrderCard.Page.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SalesOrderCard.Page.al\n@@ -0,0 +1,87 @@\n+page 50201 \"Sales Order Card FP\"\n+{\n+ PageType = Card;\n+ SourceTable = \"Sales Header\";\n+ Caption = 'Sales Order Card FP';\n+\n+ layout\n+ {\n+ area(Content)\n+ {\n+ group(General)\n+ {\n+ Caption = 'General';\n+\n+ field(\"No.\"; Rec.\"No.\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the number of the sales order.';\n+ }\n+\n+ field(\"Sell-to Customer No.\"; Rec.\"Sell-to Customer No.\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the number of the customer who will receive the products on the sales order.';\n+ }\n+\n+ field(\"Document Date\"; Rec.\"Document Date\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the date when the sales order was created.';\n+ }\n+\n+ field(\"Total Amount\"; TotalAmount)\n+ {\n+ Caption = 'Total Amount Including VAT';\n+ ApplicationArea = All;\n+ Editable = false;\n+ ToolTip = 'Specifies the total amount including VAT for the sales order.';\n+ }\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(Processing)\n+ {\n+ action(RefreshTotals)\n+ {\n+ Caption = 'Refresh Totals';\n+ ApplicationArea = All;\n+ ToolTip = 'Recalculates and refreshes the total amount for the sales order.';\n+ Image = Refresh;\n+\n+ trigger OnAction()\n+ var\n+ TotalRefreshedMsg: Label 'Total refreshed: %1', Comment = '%1 = total amount including VAT';\n+ begin\n+ // CORRECT: Manual refresh action - user-initiated, runs once\n+ Rec.CalcFields(\"Amount Including VAT\");\n+ TotalAmount := Rec.\"Amount Including VAT\";\n+ Message(TotalRefreshedMsg, TotalAmount);\n+ end;\n+ }\n+ }\n+ }\n+\n+ var\n+ TotalAmount: Decimal;\n+\n+ // CORRECT: OnAfterGetCurrRecord fires once per record selection, not per row\n+ // This is the appropriate place to calculate values when user navigates to a record\n+ trigger OnAfterGetCurrRecord()\n+ begin\n+ // Calculate total amount when user selects a sales order\n+ // This runs once when the record is loaded/selected, not in a loop\n+ Rec.CalcFields(\"Amount Including VAT\");\n+ TotalAmount := Rec.\"Amount Including VAT\";\n+ end;\n+\n+ trigger OnNewRecord(BelowxRec: Boolean)\n+ begin\n+ // CORRECT: Initialize values for new record - runs once per new record creation\n+ TotalAmount := 0;\n+ Rec.\"Document Date\" := WorkDate();\n+ end;\n+}\ndiff --git a/src/CustLedgerEntryAggregator.Codeunit.al b/src/CustLedgerEntryAggregator.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/CustLedgerEntryAggregator.Codeunit.al\n@@ -0,0 +1,20 @@\n+codeunit 50202 \"Cust Ledger Entry Aggregator\"\n+{\n+ procedure SumOpenRemainingAmount(CustomerNo: Code[20]): Decimal\n+ var\n+ CustLedgerEntry: Record \"Cust. Ledger Entry\";\n+ Customer: Record Customer;\n+ Total: Decimal;\n+ begin\n+ CustLedgerEntry.SetRange(\"Customer No.\", CustomerNo);\n+ CustLedgerEntry.SetRange(Open, true);\n+ if CustLedgerEntry.FindSet() then\n+ repeat\n+ CustLedgerEntry.CalcFields(\"Remaining Amount\");\n+ Customer.Get(CustLedgerEntry.\"Customer No.\");\n+ if Customer.\"Application Method\" = Customer.\"Application Method\"::Manual then\n+ Total += CustLedgerEntry.\"Remaining Amount\";\n+ until CustLedgerEntry.Next() = 0;\n+ exit(Total);\n+ end;\n+}\n", "expected_comments": [{"file": "src/CustLedgerEntryAggregator.Codeunit.al", "line_start": 13, "line_end": 13, "body": "CalcFields(\"Remaining Amount\") inside a repeat..until loop over \"Cust. Ledger Entry\" (up to 10M rows) issues one SQL query per iteration — classic N+1 against a hot table. — Replace the loop with `CustLedgerEntry.CalcSums(\"Remaining Amount\")` which executes as a single SUM query, or use a SIFT-backed key.", "severity": "high", "domain": "performance"}, {"file": "src/CustLedgerEntryAggregator.Codeunit.al", "line_start": 14, "line_end": 14, "body": "Customer.Get(CustLedgerEntry.\"Customer No.\") inside a repeat..until over Cust. Ledger Entry is an N+1 query: one Customer lookup per ledger row, redundant since every row already has the same Customer No. (the loop is filtered by CustomerNo). — Move the Customer.Get above the loop (single lookup), and add `Customer.SetLoadFields(\"Application Method\")` since only one field is read.", "severity": "high", "domain": "performance"}], "category": "code-review", "description": "False positive performance findings: calcfields_false_positive (30 false positives). Agent flagged these but reviewers rejected them. Enriched with 2 true-positive findings in addition to the false-positive bait.", "expect_findings": true, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-002", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "diff --git a/src/SetupReader.Codeunit.al b/src/SetupReader.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SetupReader.Codeunit.al\n@@ -0,0 +1,67 @@\n+codeunit 50211 \"Setup Reader\"\n+{\n+ procedure GetSetupValues()\n+ var\n+ GLSetup: Record \"General Ledger Setup\";\n+ SalesSetup: Record \"Sales & Receivables Setup\";\n+ InventorySetup: Record \"Inventory Setup\";\n+ PurchSetup: Record \"Purchases & Payables Setup\";\n+ begin\n+ // CORRECT: Setup tables typically have only 1 record per company\n+ // Any access pattern (Get, FindSet, FindFirst) is fine for singleton tables\n+ GLSetup.Get();\n+ SalesSetup.Get();\n+ InventorySetup.Get();\n+ PurchSetup.Get();\n+\n+ if GLSetup.\"Additional Reporting Currency\" <> '' then\n+ ProcessACYSettings(GLSetup);\n+\n+ if SalesSetup.\"Credit Warnings\" <> SalesSetup.\"Credit Warnings\"::\"No Warning\" then\n+ EnableCreditWarnings(SalesSetup);\n+ end;\n+\n+ procedure ValidateCompanySettings(): Boolean\n+ var\n+ CompanyInfo: Record \"Company Information\";\n+ begin\n+ // CORRECT: Company Information is a singleton table (1 record per company)\n+ // Get() is the appropriate method for singleton tables\n+ if not CompanyInfo.Get() then\n+ exit(false);\n+\n+ if CompanyInfo.Name = '' then\n+ exit(false);\n+\n+ if CompanyInfo.\"Country/Region Code\" = '' then\n+ exit(false);\n+\n+ exit(true);\n+ end;\n+\n+ procedure GetUserSetupForCurrentUser(var UserSetup: Record \"User Setup\"): Boolean\n+ begin\n+ // CORRECT: Looking up single user's setup record\n+ // Get() with UserId is appropriate for single-record lookup\n+ UserSetup.Reset();\n+ if UserSetup.Get(UserId) then\n+ exit(true);\n+ exit(false);\n+ end;\n+\n+ local procedure ProcessACYSettings(GLSetup: Record \"General Ledger Setup\")\n+ var\n+ ACYEnabledMsg: Label 'ACY is enabled: %1', Comment = '%1 = additional reporting currency';\n+ begin\n+ // Process additional currency settings\n+ Message(ACYEnabledMsg, GLSetup.\"Additional Reporting Currency\");\n+ end;\n+\n+ local procedure EnableCreditWarnings(SalesSetup: Record \"Sales & Receivables Setup\")\n+ var\n+ CreditWarningsEnabledMsg: Label 'Credit warnings are enabled';\n+ begin\n+ // Enable credit warning processing\n+ Message(CreditWarningsEnabledMsg);\n+ end;\n+}\ndiff --git a/src/TempBufferProcessor.Codeunit.al b/src/TempBufferProcessor.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/TempBufferProcessor.Codeunit.al\n@@ -0,0 +1,61 @@\n+codeunit 50210 \"Temp Buffer Processor\"\n+{\n+ procedure ProcessBufferEntries(var TempBuffer: Record \"Integer\" temporary)\n+ var\n+ ProcessedCount: Integer;\n+ TotalAmount: Decimal;\n+ ProcessedEntriesMsg: Label 'Processed %1 entries with total %2', Comment = '%1 = number of entries, %2 = total amount';\n+ begin\n+ // CORRECT: TempBuffer is temporary — all operations are in-memory, no SQL queries\n+ // Any access pattern (FindSet, Get, loops) on temp tables is performant\n+ ProcessedCount := 0;\n+ TotalAmount := 0;\n+\n+ if TempBuffer.FindSet() then\n+ repeat\n+ // This might look suspicious, but it's CORRECT because:\n+ // 1. TempBuffer is temporary (in-memory)\n+ // 2. No database round trips are happening\n+ // 3. All data is already loaded in memory\n+ TotalAmount += TempBuffer.Number;\n+ ProcessedCount += 1;\n+\n+ // Even modifying temp records in a loop is fine\n+ TempBuffer.Number := TempBuffer.Number * 2;\n+ TempBuffer.Modify();\n+\n+ until TempBuffer.Next() = 0;\n+\n+ Message(ProcessedEntriesMsg, ProcessedCount, TotalAmount);\n+ end;\n+\n+ procedure BuildTempData(var TempBuffer: Record \"Integer\" temporary)\n+ var\n+ i: Integer;\n+ begin\n+ // CORRECT: Building temp data - all operations are in-memory\n+ TempBuffer.Reset();\n+ TempBuffer.DeleteAll();\n+\n+ for i := 1 to 100 do begin\n+ TempBuffer.Init();\n+ TempBuffer.Number := Random(1000);\n+ TempBuffer.Insert();\n+ end;\n+ end;\n+\n+ procedure FindMaxValue(var TempBuffer: Record \"Integer\" temporary): Integer\n+ var\n+ MaxValue: Integer;\n+ begin\n+ // CORRECT: Finding max in temp table - no performance concern\n+ MaxValue := 0;\n+ if TempBuffer.FindSet() then\n+ repeat\n+ if TempBuffer.Number > MaxValue then\n+ MaxValue := TempBuffer.Number;\n+ until TempBuffer.Next() = 0;\n+\n+ exit(MaxValue);\n+ end;\n+}\ndiff --git a/src/CustomerLookup.Codeunit.al b/src/CustomerLookup.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/CustomerLookup.Codeunit.al\n@@ -0,0 +1,20 @@\n+codeunit 50212 \"Customer Lookup\"\n+{\n+ procedure GetCustomerName(CustomerNo: Code[20]): Text[100]\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.SetRange(\"No.\", CustomerNo);\n+ if Customer.FindFirst() then\n+ exit(Customer.Name);\n+ exit('');\n+ end;\n+\n+ procedure HasCustomersInCountry(CountryRegionCode: Code[10]): Boolean\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.SetRange(\"Country/Region Code\", CountryRegionCode);\n+ exit(Customer.Count() > 0);\n+ end;\n+}\n", "expected_comments": [{"file": "src/CustomerLookup.Codeunit.al", "line_start": 8, "line_end": 8, "body": "FindFirst() after SetRange on the full primary key (\"No.\") of Customer (up to 800k rows). This still does a SQL SELECT TOP 1 with a range predicate instead of a direct key lookup. — Replace with `if Customer.Get(CustomerNo) then exit(Customer.Name);` which is a direct PK lookup (CodeCop AA0233).", "severity": "medium", "domain": "performance"}, {"file": "src/CustomerLookup.Codeunit.al", "line_start": 18, "line_end": 18, "body": "Count() > 0 on Customer (up to 800k rows) for a pure existence check. Count() materializes a SQL COUNT(*) over the filtered set instead of stopping at the first matching row. — Replace with `exit(not Customer.IsEmpty());` which stops at the first match and is significantly cheaper on large tables.", "severity": "medium", "domain": "performance"}], "category": "code-review", "description": "False positive performance findings: findset_false_positive (69 false positives). Agent flagged these but reviewers rejected them. Enriched with 2 true-positive findings in addition to the false-positive bait.", "expect_findings": true, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-003", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "diff --git a/src/MigrationSetupHandler.Codeunit.al b/src/MigrationSetupHandler.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/MigrationSetupHandler.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50221 \"Migration Setup Handler\"\n+{\n+ procedure CountMigratablePermissionSets(): Integer\n+ var\n+ PermissionSet: Record \"Permission Set\";\n+ begin\n+ PermissionSet.SetFilter(\"Role ID\", '%1|%2', 'D365 BASIC', 'D365 READ');\n+ exit(PermissionSet.Count());\n+ end;\n+\n+ procedure CountObsoleteRegisters(): Integer\n+ var\n+ DateComprRegister: Record \"Date Compr. Register\";\n+ begin\n+ DateComprRegister.SetFilter(\"Ending Date\", '<%1', CalcDate('<-2Y>', Today));\n+ exit(DateComprRegister.Count());\n+ end;\n+}\ndiff --git a/src/PermissionSetListOverview.Page.al b/src/PermissionSetListOverview.Page.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PermissionSetListOverview.Page.al\n@@ -0,0 +1,84 @@\n+page 50220 \"Permission Set List Overview\"\n+{\n+ PageType = List;\n+ ApplicationArea = All;\n+ UsageCategory = Administration;\n+ SourceTable = \"Aggregate Permission Set\";\n+ Caption = 'Permission Set List Overview';\n+ Editable = false;\n+\n+ layout\n+ {\n+ area(Content)\n+ {\n+ repeater(Permissions)\n+ {\n+ field(\"Role ID\"; Rec.\"Role ID\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the identifier of the permission set.';\n+ }\n+\n+ field(Name; Rec.Name)\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the display name of the permission set.';\n+ }\n+\n+ field(Scope; Rec.Scope)\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies whether the permission set is defined by the system or by a tenant.';\n+ }\n+\n+ field(\"App Name\"; Rec.\"App Name\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the name of the extension that defines the permission set.';\n+ }\n+\n+ field(\"Permission Count\"; PermissionCount)\n+ {\n+ Caption = 'Permission Count';\n+ ApplicationArea = All;\n+ Editable = false;\n+ ToolTip = 'Specifies the number of permissions that belong to the permission set.';\n+ }\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(Processing)\n+ {\n+ action(RefreshCounts)\n+ {\n+ Caption = 'Refresh Permission Counts';\n+ ApplicationArea = All;\n+ ToolTip = 'Recalculates the permission count shown for each permission set.';\n+\n+ trigger OnAction()\n+ begin\n+ CurrPage.Update();\n+ end;\n+ }\n+ }\n+ }\n+\n+ var\n+ PermissionCount: Integer;\n+\n+ trigger OnAfterGetRecord()\n+ var\n+ Permission: Record Permission;\n+ begin\n+ Permission.SetRange(\"Role ID\", Rec.\"Role ID\");\n+ PermissionCount := Permission.Count();\n+ end;\n+\n+ trigger OnOpenPage()\n+ begin\n+ Rec.SetFilter(Scope, '%1|%2', Rec.Scope::System, Rec.Scope::Tenant);\n+ end;\n+}\ndiff --git a/src/SalesInvoiceFilter.Codeunit.al b/src/SalesInvoiceFilter.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SalesInvoiceFilter.Codeunit.al\n@@ -0,0 +1,26 @@\n+codeunit 50222 \"Sales Invoice Filter\"\n+{\n+ procedure ListLinesByDescription(Description: Text[100])\n+ var\n+ SalesInvoiceLine: Record \"Sales Invoice Line\";\n+ begin\n+ SalesInvoiceLine.SetRange(Description, Description);\n+ if SalesInvoiceLine.FindSet() then\n+ repeat\n+ Message('%1 %2', SalesInvoiceLine.\"Document No.\", SalesInvoiceLine.\"Line No.\");\n+ until SalesInvoiceLine.Next() = 0;\n+ end;\n+\n+ procedure SumQuantityByDocument(DocumentNo: Code[20]): Decimal\n+ var\n+ SalesInvoiceLine: Record \"Sales Invoice Line\";\n+ Total: Decimal;\n+ begin\n+ SalesInvoiceLine.SetRange(\"Document No.\", DocumentNo);\n+ if SalesInvoiceLine.FindSet() then\n+ repeat\n+ Total += SalesInvoiceLine.Quantity;\n+ until SalesInvoiceLine.Next() = 0;\n+ exit(Total);\n+ end;\n+}\n", "expected_comments": [{"file": "src/SalesInvoiceFilter.Codeunit.al", "line_start": 7, "line_end": 7, "body": "SetRange on Sales Invoice Line.Description with no SetCurrentKey and no key including Description. Sales Invoice Line is large (up to 3M rows) and the query will table-scan. — Either add `SetCurrentKey` to a key whose leading field matches the filter, or introduce a new key on the source table that covers Description, before filtering.", "severity": "high", "domain": "performance"}, {"file": "src/SalesInvoiceFilter.Codeunit.al", "line_start": 20, "line_end": 20, "body": "FindSet over Sales Invoice Line (3M rows, ~80 fields) loads every field for every row but the loop only reads `Quantity`. — Add `SalesInvoiceLine.SetLoadFields(Quantity);` before SetRange so SQL returns only the Quantity column (plus key fields). Even better, replace the loop with `SalesInvoiceLine.CalcSums(Quantity)` since Quantity is a SumIndexField on this table.", "severity": "medium", "domain": "performance"}, {"file": "src/PermissionSetListOverview.Page.al", "line_start": 77, "line_end": 77, "body": "PermissionCount is computed with Permission.Count() inside OnAfterGetRecord, which fires once per row rendered on this List page (and again while scrolling), so every visible row issues its own SQL COUNT against the Permission table (a per-row N+1 that scales with the number of permission sets shown). — Expose the value as a FlowField on the source table (FieldClass = FlowField, CalcFormula = count(Permission where(\"Role ID\" = field(\"Role ID\")))) so the aggregate is computed by the query engine instead of recomputing it per row.", "severity": "high", "domain": "performance"}], "category": "code-review", "description": "False positive performance findings: index_false_positive (29 false positives). Agent flagged these but reviewers rejected them. Enriched with 2 true-positive findings in addition to the false-positive bait.", "expect_findings": true, "source": "vsoadmin"} @@ -78,10 +78,10 @@ {"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-007", "base_commit": "a29ca6a99e6cd2bd46242f769b660ef1ec45b063", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "29.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocumentDirection.Enum.al b/src/Apps/W1/EDocument/App/src/Document/EDocumentDirection.Enum.al\nindex 1bef559920..00d0a3278d 100644\n--- a/src/Apps/W1/EDocument/App/src/Document/EDocumentDirection.Enum.al\n+++ b/src/Apps/W1/EDocument/App/src/Document/EDocumentDirection.Enum.al\n@@ -6,6 +6,6 @@ namespace Microsoft.eServices.EDocument;\n \n enum 6102 \"E-Document Direction\"\n {\n- value(0; \"Outgoing\") { Caption = 'Outgoing'; }\n- value(1; \"Incoming\") { Caption = 'Incoming'; }\n+ value(1; \"Outgoing\") { Caption = 'Outgoing'; }\n+ value(2; \"Incoming\") { Caption = 'Incoming'; }\n }\n", "expected_comments": [{"file": "src/Apps/W1/EDocument/App/src/Document/EDocumentDirection.Enum.al", "line_start": 9, "line_end": 9, "body": "The enum member \"Outgoing\" was renumbered from 0 to 1 (and \"Incoming\" from 1 to 2), so records that stored the old ordinals will resolve to a different member after upgrade; keep the original ordinals or add upgrade code to remap stored values.", "severity": "high", "domain": "upgrade"}], "category": "code-review", "description": "True positive upgrade findings: enum_conversion (real E-Document Direction enum member renumber on BCApps 29.0)", "expect_findings": true, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-008", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "diff --git a/src/OIOUBLInitialize.Codeunit.al b/src/OIOUBLInitialize.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/OIOUBLInitialize.Codeunit.al\n@@ -0,0 +1,78 @@\n+codeunit 13631 \"OIOUBL-Initialize\"\n+{\n+ Subtype = Install;\n+\n+ trigger OnInstallAppPerCompany()\n+ var\n+ AppInfo: ModuleInfo;\n+ begin\n+ NavApp.GetCurrentModuleInfo(AppInfo);\n+\n+ if AppInfo.DataVersion = Version.Create(0, 0, 0, 0) then\n+ SetupOIOUBLDefaults()\n+ else\n+ HandleOIOUBLUpgrade(AppInfo.DataVersion);\n+ end;\n+\n+ trigger OnInstallAppPerDatabase()\n+ begin\n+ SetupOIOUBLReportSelections();\n+ end;\n+\n+ local procedure HandleOIOUBLUpgrade(AppVersion: Version)\n+ begin\n+ if AppVersion < Version.Create(25, 0, 0, 0) then\n+ UpgradeToV25();\n+ end;\n+\n+ local procedure SetupOIOUBLDefaults()\n+ var\n+ CompanyInformation: Record \"Company Information\";\n+ OIOUBLProfile: Record \"OIOUBL-Profile\";\n+ begin\n+ if not OIOUBLProfile.Get() then begin\n+ OIOUBLProfile.Init();\n+ OIOUBLProfile.\"OIOUBL Code\" := 'DEFAULT';\n+ OIOUBLProfile.\"OIOUBL Path\" := 'OIOUBL';\n+ OIOUBLProfile.\"Check Company\" := true;\n+ OIOUBLProfile.\"Check Customer\" := true;\n+ OIOUBLProfile.\"Check Item\" := true;\n+ OIOUBLProfile.Insert();\n+ end;\n+\n+ if CompanyInformation.Get() then\n+ if CompanyInformation.\"Country/Region Code\" = 'DK' then begin\n+ CompanyInformation.\"OIOUBL-Profile Code\" := 'DEFAULT';\n+ CompanyInformation.Modify();\n+ end;\n+ end;\n+\n+ local procedure SetupOIOUBLReportSelections()\n+ var\n+ ReportSelections: Record \"Report Selections\";\n+ OIOUBLManagement: Codeunit \"OIOUBL-Management\";\n+ begin\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"S.Invoice\", Report::\"OIOUBL-Sales Invoice\");\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"S.Cr.Memo\", Report::\"OIOUBL-Sales Cr. Memo\");\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"Reminder\", Report::\"OIOUBL-Reminder\");\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"Fin.Charge\", Report::\"OIOUBL-Fin. Charge Memo\");\n+ end;\n+\n+ local procedure UpgradeToV25()\n+ var\n+ OIOUBLProfile: Record \"OIOUBL-Profile\";\n+ GLSetup: Record \"General Ledger Setup\";\n+ begin\n+ if OIOUBLProfile.Get() then begin\n+ OIOUBLProfile.\"Check Item Reference\" := true;\n+ OIOUBLProfile.\"Validate Line Discount\" := true;\n+ OIOUBLProfile.Modify();\n+ end;\n+\n+ if GLSetup.Get() then\n+ if GLSetup.\"Country/Region Code\" = 'DK' then begin\n+ GLSetup.\"OIOUBL Enabled\" := true;\n+ GLSetup.Modify();\n+ end;\n+ end;\n+}\ndiff --git a/src/WHTPurchTaxCrMemoHdr.Table.al b/src/WHTPurchTaxCrMemoHdr.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/WHTPurchTaxCrMemoHdr.Table.al\n@@ -0,0 +1,195 @@\n+table 28047 \"WHT Purch. Tax Cr. Memo Hdr.\"\n+{\n+ Caption = 'WHT Purch. Tax Cr. Memo Hdr.';\n+ DataClassification = CustomerContent;\n+ ObsoleteState = Removed;\n+ ObsoleteReason = 'Replaced with standard Purchase Credit Memo with WHT extensions';\n+ ObsoleteTag = '26.0';\n+\n+ fields\n+ {\n+ field(1; \"No.\"; Code[20])\n+ {\n+ Caption = 'No.';\n+ }\n+\n+ field(2; \"Buy-from Vendor No.\"; Code[20])\n+ {\n+ Caption = 'Buy-from Vendor No.';\n+ TableRelation = Vendor;\n+ }\n+\n+ field(3; \"Buy-from Vendor Name\"; Text[100])\n+ {\n+ Caption = 'Buy-from Vendor Name';\n+ }\n+\n+ field(4; \"Buy-from Address\"; Text[100])\n+ {\n+ Caption = 'Buy-from Address';\n+ }\n+\n+ field(5; \"Buy-from City\"; Text[30])\n+ {\n+ Caption = 'Buy-from City';\n+ }\n+\n+ field(6; \"Buy-from Contact\"; Text[100])\n+ {\n+ Caption = 'Buy-from Contact';\n+ }\n+\n+ field(7; \"Posting Date\"; Date)\n+ {\n+ Caption = 'Posting Date';\n+ }\n+\n+ field(8; \"Document Date\"; Date)\n+ {\n+ Caption = 'Document Date';\n+ }\n+\n+ field(9; \"Due Date\"; Date)\n+ {\n+ Caption = 'Due Date';\n+ }\n+\n+ field(10; \"Payment Discount %\"; Decimal)\n+ {\n+ Caption = 'Payment Discount %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(11; \"Payment Terms Code\"; Code[10])\n+ {\n+ Caption = 'Payment Terms Code';\n+ TableRelation = \"Payment Terms\";\n+ }\n+\n+ field(12; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(13; \"Currency Factor\"; Decimal)\n+ {\n+ Caption = 'Currency Factor';\n+ DecimalPlaces = 0 : 15;\n+ }\n+\n+ field(14; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(15; \"Amount Including VAT\"; Decimal)\n+ {\n+ Caption = 'Amount Including VAT';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(16; \"WHT Business Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Business Posting Group';\n+ TableRelation = \"WHT Business Posting Group\";\n+ }\n+\n+ field(17; \"WHT Product Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Product Posting Group';\n+ TableRelation = \"WHT Product Posting Group\";\n+ }\n+\n+ field(18; \"WHT Amount\"; Decimal)\n+ {\n+ Caption = 'WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(19; \"WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(20; \"WHT %\"; Decimal)\n+ {\n+ Caption = 'WHT %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(21; \"WHT Certificate No.\"; Code[20])\n+ {\n+ Caption = 'WHT Certificate No.';\n+ }\n+\n+ field(22; \"Vendor Cr. Memo No.\"; Code[35])\n+ {\n+ Caption = 'Vendor Cr. Memo No.';\n+ }\n+\n+ field(23; \"Gen. Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Bus. Posting Group';\n+ TableRelation = \"Gen. Business Posting Group\";\n+ }\n+\n+ field(24; \"VAT Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'VAT Bus. Posting Group';\n+ TableRelation = \"VAT Business Posting Group\";\n+ }\n+\n+ field(25; \"Reason Code\"; Code[10])\n+ {\n+ Caption = 'Reason Code';\n+ TableRelation = \"Reason Code\";\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"No.\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Buy-from Vendor No.\", \"Posting Date\")\n+ {\n+ }\n+\n+ key(Key3; \"WHT Business Posting Group\", \"WHT Product Posting Group\")\n+ {\n+ }\n+ }\n+\n+ trigger OnDelete()\n+ var\n+ WHTEntry: Record \"WHT Entry\";\n+ WHTCertificate: Record \"WHT Certificate\";\n+ begin\n+ WHTEntry.SetRange(\"Document No.\", \"No.\");\n+ WHTEntry.DeleteAll();\n+\n+ if \"WHT Certificate No.\" <> '' then begin\n+ WHTCertificate.SetRange(\"Certificate No.\", \"WHT Certificate No.\");\n+ WHTCertificate.DeleteAll();\n+ end;\n+ end;\n+\n+ procedure CalcWHTAmount()\n+ begin\n+ if \"WHT %\" <> 0 then begin\n+ \"WHT Amount\" := Round(Amount * \"WHT %\" / 100, 0.01);\n+ if \"Currency Factor\" <> 0 then\n+ \"WHT Amount (LCY)\" := Round(\"WHT Amount\" / \"Currency Factor\", 0.01)\n+ else\n+ \"WHT Amount (LCY)\" := \"WHT Amount\";\n+ end else begin\n+ \"WHT Amount\" := 0;\n+ \"WHT Amount (LCY)\" := 0;\n+ end;\n+ end;\n+}\n", "expected_comments": [{"file": "src/OIOUBLInitialize.Codeunit.al", "line_start": 24, "line_end": 24, "body": "Version check pattern instead of upgrade tags - not idempotent.", "severity": "medium", "domain": "upgrade"}, {"file": "src/OIOUBLInitialize.Codeunit.al", "line_start": 61, "line_end": 61, "body": "UpgradeToV25 procedure lacks upgrade tag to prevent re-execution.", "severity": "medium", "domain": "upgrade"}, {"file": "src/WHTPurchTaxCrMemoHdr.Table.al", "line_start": 5, "line_end": 5, "body": "Table marked ObsoleteState = Removed without corresponding upgrade code for data migration.", "severity": "high", "domain": "upgrade"}, {"file": "src/WHTPurchTaxCrMemoHdr.Table.al", "line_start": 168, "line_end": 168, "body": "OnDelete trigger on removed table can cascade-delete related WHT records during cleanup or migration.", "severity": "medium", "domain": "upgrade"}], "category": "code-review", "description": "True positive upgrade findings: obsolete_usage (trimmed to reliably detected findings)", "expect_findings": true, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-009", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "diff --git a/src/ContactSyncFolder.Table.al b/src/ContactSyncFolder.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/ContactSyncFolder.Table.al\n@@ -0,0 +1,234 @@\n+/// \n+/// Contact Sync Folder Table (5368)\n+/// Stores folder information for contact synchronization with external systems\n+/// \n+table 5368 \"Contact Sync Folder\"\n+{\n+ Caption = 'Contact Sync Folder';\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Folder ID\"; Text[250])\n+ {\n+ Caption = 'Folder ID';\n+ NotBlank = true;\n+ }\n+\n+ field(2; Name; Text[100])\n+ {\n+ Caption = 'Name';\n+ }\n+\n+ field(3; \"Last Sync DateTime\"; DateTime)\n+ {\n+ Caption = 'Last Sync DateTime';\n+ Editable = false;\n+ }\n+\n+ field(4; \"Parent Id\"; Text[250])\n+ {\n+ Caption = 'Parent Id';\n+ }\n+\n+ field(5; \"Folder Type\"; Option)\n+ {\n+ Caption = 'Folder Type';\n+ OptionCaption = 'Root,Contacts,Companies,People,Distribution Lists';\n+ OptionMembers = Root,Contacts,Companies,People,\"Distribution Lists\";\n+ }\n+\n+ field(6; \"Sync Enabled\"; Boolean)\n+ {\n+ Caption = 'Sync Enabled';\n+ InitValue = true;\n+ }\n+\n+ field(7; \"Sync Direction\"; Option)\n+ {\n+ Caption = 'Sync Direction';\n+ OptionCaption = 'Bidirectional,To Exchange,From Exchange';\n+ OptionMembers = Bidirectional,\"To Exchange\",\"From Exchange\";\n+ InitValue = Bidirectional;\n+ }\n+\n+ field(8; \"Exchange Service\"; Code[20])\n+ {\n+ Caption = 'Exchange Service';\n+ TableRelation = \"Exchange Service Connection\";\n+ }\n+\n+ field(9; \"Total Items\"; Integer)\n+ {\n+ Caption = 'Total Items';\n+ Editable = false;\n+ }\n+\n+ field(10; \"Synced Items\"; Integer)\n+ {\n+ Caption = 'Synced Items';\n+ Editable = false;\n+ }\n+\n+ field(11; \"Pending Items\"; Integer)\n+ {\n+ Caption = 'Pending Items';\n+ Editable = false;\n+ CalcFormula = Count(\"Contact Sync Entry\" WHERE(\"Folder ID\" = FIELD(\"Folder ID\"), \"Sync Status\" = CONST(Pending)));\n+ FieldClass = FlowField;\n+ }\n+\n+ field(12; \"Error Items\"; Integer)\n+ {\n+ Caption = 'Error Items';\n+ Editable = false;\n+ CalcFormula = Count(\"Contact Sync Entry\" WHERE(\"Folder ID\" = FIELD(\"Folder ID\"), \"Sync Status\" = CONST(Error)));\n+ FieldClass = FlowField;\n+ }\n+\n+ field(13; \"Auto Sync Interval\"; Duration)\n+ {\n+ Caption = 'Auto Sync Interval';\n+ }\n+\n+ field(14; \"Next Sync DateTime\"; DateTime)\n+ {\n+ Caption = 'Next Sync DateTime';\n+ }\n+\n+ field(15; \"Conflict Resolution\"; Option)\n+ {\n+ Caption = 'Conflict Resolution';\n+ OptionCaption = 'Exchange Wins,Business Central Wins,Manual Resolution';\n+ OptionMembers = \"Exchange Wins\",\"Business Central Wins\",\"Manual Resolution\";\n+ InitValue = \"Manual Resolution\";\n+ }\n+\n+ field(16; \"Created By\"; Code[50])\n+ {\n+ Caption = 'Created By';\n+ Editable = false;\n+ TableRelation = User.\"User Name\";\n+ }\n+\n+ field(17; \"Created DateTime\"; DateTime)\n+ {\n+ Caption = 'Created DateTime';\n+ Editable = false;\n+ }\n+\n+ field(18; \"Modified By\"; Code[50])\n+ {\n+ Caption = 'Modified By';\n+ Editable = false;\n+ TableRelation = User.\"User Name\";\n+ }\n+\n+ field(19; \"Modified DateTime\"; DateTime)\n+ {\n+ Caption = 'Modified DateTime';\n+ Editable = false;\n+ }\n+\n+ field(20; Blocked; Boolean)\n+ {\n+ Caption = 'Blocked';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Folder ID\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Exchange Service\", \"Sync Enabled\")\n+ {\n+ }\n+\n+ key(Key3; \"Parent Id\", \"Folder Type\")\n+ {\n+ }\n+\n+ key(Key4; \"Next Sync DateTime\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Folder ID\", Name, \"Folder Type\", \"Sync Enabled\")\n+ {\n+ }\n+\n+ fieldgroup(Brick; \"Folder ID\", Name, \"Total Items\", \"Last Sync DateTime\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Created By\" := UserId;\n+ \"Created DateTime\" := CurrentDateTime;\n+ \"Modified By\" := UserId;\n+ \"Modified DateTime\" := CurrentDateTime;\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Modified By\" := UserId;\n+ \"Modified DateTime\" := CurrentDateTime;\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ ContactSyncEntry: Record \"Contact Sync Entry\";\n+ begin\n+ ContactSyncEntry.SetRange(\"Folder ID\", \"Folder ID\");\n+ ContactSyncEntry.DeleteAll();\n+ end;\n+\n+ procedure SyncContacts()\n+ var\n+ ContactSyncMgt: Codeunit \"Contact Sync. Management\";\n+ begin\n+ TestField(\"Sync Enabled\", true);\n+ TestField(Blocked, false);\n+\n+ ContactSyncMgt.SyncFolder(Rec);\n+ end;\n+\n+ procedure ScheduleNextSync()\n+ begin\n+ if \"Auto Sync Interval\" > 0 then\n+ \"Next Sync DateTime\" := CurrentDateTime + \"Auto Sync Interval\";\n+ end;\n+\n+ procedure UpdateSyncStatistics()\n+ var\n+ ContactSyncEntry: Record \"Contact Sync Entry\";\n+ begin\n+ ContactSyncEntry.SetRange(\"Folder ID\", \"Folder ID\");\n+ \"Total Items\" := ContactSyncEntry.Count();\n+\n+ ContactSyncEntry.SetRange(\"Sync Status\", ContactSyncEntry.\"Sync Status\"::Synced);\n+ \"Synced Items\" := ContactSyncEntry.Count();\n+\n+ \"Last Sync DateTime\" := CurrentDateTime;\n+ Modify();\n+ end;\n+\n+ procedure GetChildFolders(var ChildFolders: Record \"Contact Sync Folder\")\n+ begin\n+ ChildFolders.SetRange(\"Parent Id\", \"Folder ID\");\n+ ChildFolders.SetRange(\"Sync Enabled\", true);\n+ ChildFolders.SetRange(Blocked, false);\n+ end;\n+\n+ procedure HasPendingSync(): Boolean\n+ begin\n+ CalcFields(\"Pending Items\");\n+ exit(\"Pending Items\" > 0);\n+ end;\n+}\ndiff --git a/src/Currency.Table.al b/src/Currency.Table.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/Currency.Table.al\n@@ -0,0 +1,255 @@\n+/// \n+/// Currency Table (4)\n+/// Master table for currency codes and settings\n+/// \n+table 4 Currency\n+{\n+ Caption = 'Currency';\n+ DataCaptionFields = \"Code\", Description;\n+ DataClassification = CustomerContent;\n+ LookupPageID = Currencies;\n+\n+ fields\n+ {\n+ field(1; \"Code\"; Code[10])\n+ {\n+ Caption = 'Code';\n+ NotBlank = true;\n+ }\n+\n+ field(2; \"Last Date Modified\"; Date)\n+ {\n+ Caption = 'Last Date Modified';\n+ Editable = false;\n+ }\n+\n+ field(3; \"Last Date Adjusted\"; Date)\n+ {\n+ Caption = 'Last Date Adjusted';\n+ Editable = false;\n+ }\n+\n+ field(4; \"Exchange Rate Amount\"; Decimal)\n+ {\n+ Caption = 'Exchange Rate Amount';\n+ DecimalPlaces = 1 : 6;\n+ InitValue = 1;\n+ MinValue = 0;\n+ NotBlank = true;\n+ }\n+\n+ field(5; \"Relational Exch. Rate Amount\"; Decimal)\n+ {\n+ Caption = 'Relational Exch. Rate Amount';\n+ DecimalPlaces = 1 : 6;\n+ InitValue = 1;\n+ MinValue = 0;\n+ }\n+\n+ field(6; \"Unrealized Gains Acc.\"; Code[20])\n+ {\n+ Caption = 'Unrealized Gains Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(7; \"Unrealized Losses Acc.\"; Code[20])\n+ {\n+ Caption = 'Unrealized Losses Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(8; \"Realized Gains Acc.\"; Code[20])\n+ {\n+ Caption = 'Realized Gains Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(9; \"Realized Losses Acc.\"; Code[20])\n+ {\n+ Caption = 'Realized Losses Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(10; \"Amount Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Amount Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 0.01;\n+ }\n+\n+ field(11; \"Unit-Amount Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Unit-Amount Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 0.00001;\n+ }\n+\n+ field(12; Description; Text[60])\n+ {\n+ Caption = 'Description';\n+ }\n+\n+ field(13; \"Invoice Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Invoice Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 1;\n+ }\n+\n+ field(14; \"Invoice Rounding Type\"; Option)\n+ {\n+ Caption = 'Invoice Rounding Type';\n+ OptionCaption = 'Nearest,Up,Down';\n+ OptionMembers = Nearest,Up,Down;\n+ }\n+\n+ field(15; \"Amount Decimal Places\"; Text[5])\n+ {\n+ Caption = 'Amount Decimal Places';\n+ }\n+\n+ field(16; \"Unit-Amount Decimal Places\"; Text[5])\n+ {\n+ Caption = 'Unit-Amount Decimal Places';\n+ }\n+\n+ field(17; \"Appln. Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Appln. Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 0.01;\n+ }\n+\n+ field(18; \"Conv. LCY Rndg. Debit Acc.\"; Code[20])\n+ {\n+ Caption = 'Conv. LCY Rndg. Debit Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(19; \"Conv. LCY Rndg. Credit Acc.\"; Code[20])\n+ {\n+ Caption = 'Conv. LCY Rndg. Credit Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(20; \"Max. VAT Difference Allowed\"; Decimal)\n+ {\n+ Caption = 'Max. VAT Difference Allowed';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(21; \"VAT Rounding Type\"; Option)\n+ {\n+ Caption = 'VAT Rounding Type';\n+ OptionCaption = 'Nearest,Up,Down';\n+ OptionMembers = Nearest,Up,Down;\n+ }\n+\n+ field(22; \"Payment Tolerance %\"; Decimal)\n+ {\n+ Caption = 'Payment Tolerance %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(23; \"Max. Payment Tolerance Amount\"; Decimal)\n+ {\n+ Caption = 'Max. Payment Tolerance Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(24; Symbol; Text[10])\n+ {\n+ Caption = 'Symbol';\n+ }\n+\n+ field(746; \"Currency Symbol Position\"; Enum \"Currency Symbol Position\")\n+ {\n+ Caption = 'Currency Symbol Position';\n+ InitValue = \"Default\";\n+ }\n+\n+ field(747; \"ISO Code\"; Code[3])\n+ {\n+ Caption = 'ISO Code';\n+ }\n+\n+ field(748; \"ISO Numeric Code\"; Code[3])\n+ {\n+ Caption = 'ISO Numeric Code';\n+ }\n+\n+ field(749; \"Digital Currency\"; Boolean)\n+ {\n+ Caption = 'Digital Currency';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Code\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Code\", Description, Symbol, \"ISO Code\")\n+ {\n+ }\n+\n+ fieldgroup(Brick; \"Code\", Description, Symbol)\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ SetDefaultValues();\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ end;\n+\n+ local procedure SetDefaultValues()\n+ begin\n+ if \"Exchange Rate Amount\" = 0 then\n+ \"Exchange Rate Amount\" := 1;\n+\n+ if \"Relational Exch. Rate Amount\" = 0 then\n+ \"Relational Exch. Rate Amount\" := 1;\n+\n+ if \"Amount Rounding Precision\" = 0 then\n+ \"Amount Rounding Precision\" := 0.01;\n+\n+ if \"Unit-Amount Rounding Precision\" = 0 then\n+ \"Unit-Amount Rounding Precision\" := 0.00001;\n+\n+ if \"Invoice Rounding Precision\" = 0 then\n+ \"Invoice Rounding Precision\" := 1;\n+\n+ if \"Appln. Rounding Precision\" = 0 then\n+ \"Appln. Rounding Precision\" := 0.01;\n+ end;\n+\n+ procedure InitRoundingPrecision()\n+ begin\n+ \"Amount Rounding Precision\" := 0.01;\n+ \"Unit-Amount Rounding Precision\" := 0.00001;\n+ \"Appln. Rounding Precision\" := 0.01;\n+ \"Invoice Rounding Precision\" := 1;\n+ \"Invoice Rounding Type\" := \"Invoice Rounding Type\"::Nearest;\n+ \"VAT Rounding Type\" := \"VAT Rounding Type\"::Nearest;\n+ end;\n+\n+ procedure GetCurrencySymbol(): Text[10]\n+ begin\n+ if Symbol <> '' then\n+ exit(Symbol);\n+\n+ exit(Code);\n+ end;\n+}\ndiff --git a/src/UpgradeExpenseAgentSetup.Codeunit.al b/src/UpgradeExpenseAgentSetup.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/UpgradeExpenseAgentSetup.Codeunit.al\n@@ -0,0 +1,119 @@\n+/// \n+/// Upgrade Expense Agent Setup Codeunit (69135)\n+/// Handles upgrade procedures for Expense Agent setup with direct implementation\n+/// \n+codeunit 69135 \"Upgrade Expense Agent Setup\"\n+{\n+ Subtype = Upgrade;\n+\n+ trigger OnUpgradePerDatabase()\n+ var\n+ InstallExpenseAgentSetup: Codeunit \"Install Expense Agent Setup\";\n+ begin\n+ InstallExpenseAgentSetup.RegisterCapability();\n+ end;\n+\n+ trigger OnUpgradePerCompany()\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ UpgradeTag: Codeunit \"Upgrade Tag\";\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ if UpgradeTag.HasUpgradeTag(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag()) then\n+ exit;\n+\n+ UpgradeExpenseAgentSetup();\n+\n+ UpgradeTag.SetUpgradeTag(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerCompanyUpgradeTags', '', false, false)]\n+ local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])\n+ var\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ PerCompanyUpgradeTags.Add(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerDatabaseUpgradeTags', '', false, false)]\n+ local procedure RegisterPerDatabaseTags(var PerDatabaseUpgradeTags: List of [Code[250]])\n+ var\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ PerDatabaseUpgradeTags.Add(ExpenseAgentUpgradeTags.GetExpenseCapabilityUpgradeTag());\n+ end;\n+\n+ local procedure UpgradeExpenseAgentSetup()\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ begin\n+ if not ExpenseAgentSetup.Get() then begin\n+ ExpenseAgentSetup.Init();\n+ ExpenseAgentSetup.Insert();\n+ end;\n+\n+ // Set default values for new fields\n+ ExpenseAgentSetup.\"Enable AI Processing\" := true;\n+ ExpenseAgentSetup.\"Max File Size (MB)\" := 10;\n+ ExpenseAgentSetup.\"Supported File Types\" := 'PDF,JPG,PNG,JPEG';\n+ ExpenseAgentSetup.\"Auto-Submit Threshold\" := 100;\n+ ExpenseAgentSetup.\"Receipt Required for Amount\" := 50;\n+ ExpenseAgentSetup.\"Mileage Rate per KM\" := 0.45;\n+ ExpenseAgentSetup.\"Supervisor Notification Days\" := 7;\n+ ExpenseAgentSetup.\"Expense Approval Timeout (Days)\" := 14;\n+ ExpenseAgentSetup.Modify();\n+ end;\n+\n+ procedure UpgradeExpenseCategories()\n+ var\n+ ExpenseCategory: Record \"Expense Category\";\n+ GLAccount: Record \"G/L Account\";\n+ begin\n+ ExpenseCategory.SetRange(\"G/L Account No.\", '');\n+ if ExpenseCategory.FindSet() then\n+ repeat\n+ // Set default G/L accounts for expense categories without them\n+ case ExpenseCategory.Type of\n+ ExpenseCategory.Type::Travel:\n+ if GLAccount.Get('6110') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Meals:\n+ if GLAccount.Get('6120') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Accommodation:\n+ if GLAccount.Get('6130') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Transportation:\n+ if GLAccount.Get('6140') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Entertainment:\n+ if GLAccount.Get('6150') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ end;\n+ ExpenseCategory.Modify();\n+ until ExpenseCategory.Next() = 0;\n+ end;\n+\n+ procedure UpgradeExpensePaymentMethods()\n+ var\n+ PaymentMethod: Record \"Payment Method\";\n+ ExpensePaymentMethod: Record \"Expense Payment Method\";\n+ begin\n+ // Migrate from Payment Method to Expense Payment Method\n+ PaymentMethod.SetRange(\"Expense Report Type\", true);\n+ if PaymentMethod.FindSet() then\n+ repeat\n+ if not ExpensePaymentMethod.Get(PaymentMethod.Code) then begin\n+ ExpensePaymentMethod.Init();\n+ ExpensePaymentMethod.Code := PaymentMethod.Code;\n+ ExpensePaymentMethod.Description := PaymentMethod.Description;\n+ ExpensePaymentMethod.\"Reimbursable\" := not PaymentMethod.\"Corporate Card\";\n+ ExpensePaymentMethod.\"Corporate Card\" := PaymentMethod.\"Corporate Card\";\n+ ExpensePaymentMethod.\"Requires Receipt\" := PaymentMethod.\"Receipt Required\";\n+ ExpensePaymentMethod.\"Balancing Account Type\" := PaymentMethod.\"Bal. Account Type\";\n+ ExpensePaymentMethod.\"Balancing Account No.\" := PaymentMethod.\"Bal. Account No.\";\n+ ExpensePaymentMethod.Insert();\n+ end;\n+ until PaymentMethod.Next() = 0;\n+ end;\n+}\n", "expected_comments": [{"file": "src/Currency.Table.al", "line_start": 168, "line_end": 168, "body": "New field with InitValue = Default without upgrade code.", "severity": "medium", "domain": "upgrade"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 9, "line_end": 9, "body": "OnUpgradePerDatabase trigger lacks upgrade tag protection and runs per-database logic on every upgrade.", "severity": "high", "domain": "upgrade"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 16, "line_end": 16, "body": "OnUpgradePerCompany trigger contains inline implementation instead of delegating to a named procedure.", "severity": "medium", "domain": "upgrade"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 56, "line_end": 56, "body": "UpgradeExpenseAgentSetup() unconditionally overwrites existing setup values during upgrade.", "severity": "high", "domain": "upgrade"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 67, "line_end": 67, "body": "Public upgrade procedure UpgradeExpenseCategories without upgrade tag guard.", "severity": "medium", "domain": "upgrade"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 97, "line_end": 97, "body": "Public upgrade procedure UpgradeExpensePaymentMethods without upgrade tag guard.", "severity": "medium", "domain": "upgrade"}, {"file": "src/ContactSyncFolder.Table.al", "line_start": 44, "line_end": 44, "body": "InitValue = true on Sync Enabled without upgrade code for existing records.", "severity": "medium", "domain": "upgrade"}, {"file": "src/ContactSyncFolder.Table.al", "line_start": 104, "line_end": 104, "body": "Conflict Resolution InitValue on existing table without upgrade code for existing records.", "severity": "medium", "domain": "upgrade"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 93, "line_end": 93, "body": "Modify() called unconditionally even when no case branch matched.", "severity": "low", "domain": "upgrade"}], "category": "code-review", "description": "True positive upgrade findings: other_upgrade (trimmed to reliably detected setup and InitValue upgrade risks)", "expect_findings": true, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-01", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/PartnerConfigKeyManager.Codeunit.al b/src/PartnerConfigKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PartnerConfigKeyManager.Codeunit.al\n@@ -0,0 +1,9 @@\n+codeunit 50100 \"Partner Config Key Manager\"\n+{\n+ Access = Internal;\n+\n+ internal procedure StoreApiKey(KeyName: Text; ApiKey: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, ApiKey, DataScope::Module);\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: secure isolated-storage key handling with no issues to flag (derived from security-008 with the plain-Text secret parameter resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-02", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SecureKeyManager.Codeunit.al b/src/SecureKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureKeyManager.Codeunit.al\n@@ -0,0 +1,26 @@\n+codeunit 50101 \"Secure Key Manager\"\n+{\n+ Access = Internal;\n+\n+ var\n+ KeyNotFoundErr: Label 'The requested key was not found. Configure it before use.';\n+\n+ [NonDebuggable]\n+ procedure StoreEncryptedKey(KeyName: Code[20]; KeyValue: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, KeyValue, DataScope::Module);\n+ end;\n+\n+ [NonDebuggable]\n+ procedure RetrieveKey(KeyName: Code[20]) Result: SecretText\n+ begin\n+ if not IsolatedStorage.Contains(KeyName, DataScope::Module) then\n+ Error(KeyNotFoundErr);\n+ IsolatedStorage.Get(KeyName, DataScope::Module, Result);\n+ end;\n+\n+ procedure HasKey(KeyName: Code[20]): Boolean\n+ begin\n+ exit(IsolatedStorage.Contains(KeyName, DataScope::Module));\n+ end;\n+}\ndiff --git a/src/PlainTokenHeaderClient.Codeunit.al b/src/PlainTokenHeaderClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PlainTokenHeaderClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50302 \"Plain Token Header Client\"\n+{\n+ procedure SendRequest(AccessToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', SecretStrSubstNo('Bearer %1', AccessToken));\n+ HttpClient.Get(this.GetOrdersEndpoint(), HttpResponseMessage);\n+ end;\n+\n+ local procedure GetOrdersEndpoint(): Text\n+ begin\n+ exit('https://api.contoso.com/orders');\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: outbound HTTP client that builds the authorization header as SecretText (derived from security-002 with the plain-Text bearer token resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-03", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/SafeErrorHandler.Codeunit.al b/src/SafeErrorHandler.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SafeErrorHandler.Codeunit.al\n@@ -0,0 +1,41 @@\n+codeunit 50102 \"Safe Error Handler\"\n+{\n+ Access = Internal;\n+\n+ var\n+ InvalidRequestTxt: Label 'Invalid request. Please check your input.';\n+ AuthFailedTxt: Label 'Authentication failed. Please verify your credentials.';\n+ ForbiddenTxt: Label 'You do not have permission for this operation.';\n+ NotFoundTxt: Label 'The requested resource was not found.';\n+ UnexpectedTxt: Label 'An unexpected error occurred. Contact your administrator.';\n+ PostFailedErr: Label 'Could not post document %1.', Comment = '%1 = document number';\n+\n+ procedure GetApiResponseMessage(StatusCode: Integer): Text\n+ begin\n+ case StatusCode of\n+ 200, 201:\n+ exit('');\n+ 400:\n+ exit(InvalidRequestTxt);\n+ 401:\n+ exit(AuthFailedTxt);\n+ 403:\n+ exit(ForbiddenTxt);\n+ 404:\n+ exit(NotFoundTxt);\n+ else\n+ exit(UnexpectedTxt);\n+ end;\n+ end;\n+\n+ procedure PostDocument(DocNo: Code[20])\n+ begin\n+ if not TryPost(DocNo) then\n+ Error(PostFailedErr, DocNo);\n+ end;\n+\n+ [TryFunction]\n+ local procedure TryPost(DocNo: Code[20])\n+ begin\n+ end;\n+}\ndiff --git a/src/UnwrappedSecretClient.Codeunit.al b/src/UnwrappedSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/UnwrappedSecretClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50303 \"Unwrapped Secret Client\"\n+{\n+ procedure SendRequest(SessionToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', this.BuildHeader(SessionToken));\n+ HttpClient.Get('https://api.contoso.com/data', HttpResponseMessage);\n+ end;\n+\n+ local procedure BuildHeader(SessionToken: SecretText): SecretText\n+ begin\n+ exit(SecretStrSubstNo('Bearer %1', SessionToken));\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: authorization header built without unwrapping the secret (derived from security-003 with the SecretText.Unwrap() call resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} -{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-04", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security"}, "patch": "diff --git a/src/AppConstants.Codeunit.al b/src/AppConstants.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/AppConstants.Codeunit.al\n@@ -0,0 +1,30 @@\n+codeunit 50103 \"App Constants\"\n+{\n+ Access = Internal;\n+\n+ var\n+ ApiVersionTok: Label 'v2.0', Locked = true;\n+ DefaultCurrencyTok: Label 'USD', Locked = true;\n+ DateFormatTok: Label 'yyyy-MM-dd', Locked = true;\n+ AppIdTok: Label 'BC-INVENTORY-APP', Locked = true;\n+\n+ procedure GetApiVersion(): Text\n+ begin\n+ exit(ApiVersionTok);\n+ end;\n+\n+ procedure GetDefaultCurrency(): Code[10]\n+ begin\n+ exit(DefaultCurrencyTok);\n+ end;\n+\n+ procedure GetDateFormat(): Text\n+ begin\n+ exit(DateFormatTok);\n+ end;\n+\n+ procedure GetAppId(): Text\n+ begin\n+ exit(AppIdTok);\n+ end;\n+}\ndiff --git a/src/QueryStringSecretClient.Codeunit.al b/src/QueryStringSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/QueryStringSecretClient.Codeunit.al\n@@ -0,0 +1,13 @@\n+codeunit 50304 \"Query String Secret Client\"\n+{\n+ procedure FetchAccount(ApiKey: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', SecretStrSubstNo('Bearer %1', ApiKey));\n+ HttpClient.Get('https://api.contoso.com/accounts', HttpResponseMessage);\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: API key passed via the Authorization header instead of the URL query string (derived from security-004). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-01", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security", "articles": ["security/secrettext-for-credentials"]}, "patch": "diff --git a/src/PartnerConfigKeyManager.Codeunit.al b/src/PartnerConfigKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PartnerConfigKeyManager.Codeunit.al\n@@ -0,0 +1,9 @@\n+codeunit 50100 \"Partner Config Key Manager\"\n+{\n+ Access = Internal;\n+\n+ internal procedure StoreApiKey(KeyName: Text; ApiKey: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, ApiKey, DataScope::Module);\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: secure isolated-storage key handling with no issues to flag (derived from security-008 with the plain-Text secret parameter resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-02", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security", "articles": ["security/secretstrsubstno-for-composing-secrets"]}, "patch": "diff --git a/src/SecureKeyManager.Codeunit.al b/src/SecureKeyManager.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SecureKeyManager.Codeunit.al\n@@ -0,0 +1,26 @@\n+codeunit 50101 \"Secure Key Manager\"\n+{\n+ Access = Internal;\n+\n+ var\n+ KeyNotFoundErr: Label 'The requested key was not found. Configure it before use.';\n+\n+ [NonDebuggable]\n+ procedure StoreEncryptedKey(KeyName: Code[20]; KeyValue: SecretText)\n+ begin\n+ IsolatedStorage.SetEncrypted(KeyName, KeyValue, DataScope::Module);\n+ end;\n+\n+ [NonDebuggable]\n+ procedure RetrieveKey(KeyName: Code[20]) Result: SecretText\n+ begin\n+ if not IsolatedStorage.Contains(KeyName, DataScope::Module) then\n+ Error(KeyNotFoundErr);\n+ IsolatedStorage.Get(KeyName, DataScope::Module, Result);\n+ end;\n+\n+ procedure HasKey(KeyName: Code[20]): Boolean\n+ begin\n+ exit(IsolatedStorage.Contains(KeyName, DataScope::Module));\n+ end;\n+}\ndiff --git a/src/PlainTokenHeaderClient.Codeunit.al b/src/PlainTokenHeaderClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/PlainTokenHeaderClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50302 \"Plain Token Header Client\"\n+{\n+ procedure SendRequest(AccessToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', SecretStrSubstNo('Bearer %1', AccessToken));\n+ HttpClient.Get(this.GetOrdersEndpoint(), HttpResponseMessage);\n+ end;\n+\n+ local procedure GetOrdersEndpoint(): Text\n+ begin\n+ exit('https://api.contoso.com/orders');\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: outbound HTTP client that builds the authorization header as SecretText (derived from security-002 with the plain-Text bearer token resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-03", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security", "articles": ["security/nondebuggable-required-when-unwrapping-secrettext"]}, "patch": "diff --git a/src/SafeErrorHandler.Codeunit.al b/src/SafeErrorHandler.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/SafeErrorHandler.Codeunit.al\n@@ -0,0 +1,41 @@\n+codeunit 50102 \"Safe Error Handler\"\n+{\n+ Access = Internal;\n+\n+ var\n+ InvalidRequestTxt: Label 'Invalid request. Please check your input.';\n+ AuthFailedTxt: Label 'Authentication failed. Please verify your credentials.';\n+ ForbiddenTxt: Label 'You do not have permission for this operation.';\n+ NotFoundTxt: Label 'The requested resource was not found.';\n+ UnexpectedTxt: Label 'An unexpected error occurred. Contact your administrator.';\n+ PostFailedErr: Label 'Could not post document %1.', Comment = '%1 = document number';\n+\n+ procedure GetApiResponseMessage(StatusCode: Integer): Text\n+ begin\n+ case StatusCode of\n+ 200, 201:\n+ exit('');\n+ 400:\n+ exit(InvalidRequestTxt);\n+ 401:\n+ exit(AuthFailedTxt);\n+ 403:\n+ exit(ForbiddenTxt);\n+ 404:\n+ exit(NotFoundTxt);\n+ else\n+ exit(UnexpectedTxt);\n+ end;\n+ end;\n+\n+ procedure PostDocument(DocNo: Code[20])\n+ begin\n+ if not TryPost(DocNo) then\n+ Error(PostFailedErr, DocNo);\n+ end;\n+\n+ [TryFunction]\n+ local procedure TryPost(DocNo: Code[20])\n+ begin\n+ end;\n+}\ndiff --git a/src/UnwrappedSecretClient.Codeunit.al b/src/UnwrappedSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/UnwrappedSecretClient.Codeunit.al\n@@ -0,0 +1,18 @@\n+codeunit 50303 \"Unwrapped Secret Client\"\n+{\n+ procedure SendRequest(SessionToken: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', this.BuildHeader(SessionToken));\n+ HttpClient.Get('https://api.contoso.com/data', HttpResponseMessage);\n+ end;\n+\n+ local procedure BuildHeader(SessionToken: SecretText): SecretText\n+ begin\n+ exit(SecretStrSubstNo('Bearer %1', SessionToken));\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: authorization header built without unwrapping the secret (derived from security-003 with the SecretText.Unwrap() call resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} +{"repo": "microsoft/BCApps", "instance_id": "synthetic__security-clean-04", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "security", "articles": ["security/secrettext-with-httpclient"]}, "patch": "diff --git a/src/AppConstants.Codeunit.al b/src/AppConstants.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/AppConstants.Codeunit.al\n@@ -0,0 +1,30 @@\n+codeunit 50103 \"App Constants\"\n+{\n+ Access = Internal;\n+\n+ var\n+ ApiVersionTok: Label 'v2.0', Locked = true;\n+ DefaultCurrencyTok: Label 'USD', Locked = true;\n+ DateFormatTok: Label 'yyyy-MM-dd', Locked = true;\n+ AppIdTok: Label 'BC-INVENTORY-APP', Locked = true;\n+\n+ procedure GetApiVersion(): Text\n+ begin\n+ exit(ApiVersionTok);\n+ end;\n+\n+ procedure GetDefaultCurrency(): Code[10]\n+ begin\n+ exit(DefaultCurrencyTok);\n+ end;\n+\n+ procedure GetDateFormat(): Text\n+ begin\n+ exit(DateFormatTok);\n+ end;\n+\n+ procedure GetAppId(): Text\n+ begin\n+ exit(AppIdTok);\n+ end;\n+}\ndiff --git a/src/QueryStringSecretClient.Codeunit.al b/src/QueryStringSecretClient.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/QueryStringSecretClient.Codeunit.al\n@@ -0,0 +1,13 @@\n+codeunit 50304 \"Query String Secret Client\"\n+{\n+ procedure FetchAccount(ApiKey: SecretText)\n+ var\n+ HttpClient: HttpClient;\n+ HttpHeaders: HttpHeaders;\n+ HttpResponseMessage: HttpResponseMessage;\n+ begin\n+ HttpHeaders := HttpClient.DefaultRequestHeaders();\n+ HttpHeaders.Add('Authorization', SecretStrSubstNo('Bearer %1', ApiKey));\n+ HttpClient.Get('https://api.contoso.com/accounts', HttpResponseMessage);\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: API key passed via the Authorization header instead of the URL query string (derived from security-004). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-clean-01", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "diff --git a/src/BatchModifier.Codeunit.al b/src/BatchModifier.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/BatchModifier.Codeunit.al\n@@ -0,0 +1,22 @@\n+codeunit 50231 \"Batch Modifier FP\"\n+{\n+ procedure UpdateSetupDefaults(CompanyCode: Code[10])\n+ var\n+ FASetup: Record \"FA Setup\";\n+ begin\n+ // CORRECT: FA Setup is a singleton — direct Get + Modify is ideal\n+ if FASetup.Get() then begin\n+ FASetup.\"Default Depr. Book\" := CompanyCode;\n+ FASetup.Modify(false);\n+ end;\n+ end;\n+\n+ procedure CleanupObsoleteReasonCodes()\n+ var\n+ ReasonCode: Record \"Reason Code\";\n+ begin\n+ // CORRECT: DeleteAll is the proper bulk delete pattern\n+ ReasonCode.SetRange(Description, '');\n+ ReasonCode.DeleteAll(false);\n+ end;\n+}\ndiff --git a/src/CustomerReader.Codeunit.al b/src/CustomerReader.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/CustomerReader.Codeunit.al\n@@ -0,0 +1,12 @@\n+codeunit 50232 \"Customer Reader\"\n+{\n+ procedure GetCustomerCreditLimit(CustomerNo: Code[20]): Decimal\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.SetLoadFields(\"Credit Limit (LCY)\");\n+ if Customer.Get(CustomerNo) then\n+ exit(Customer.\"Credit Limit (LCY)\");\n+ exit(0);\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: read-only customer getter without a stray LockTable (derived from performance-004). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-clean-02", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "diff --git a/src/TransactionProcessor.Codeunit.al b/src/TransactionProcessor.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/TransactionProcessor.Codeunit.al\n@@ -0,0 +1,21 @@\n+codeunit 50403 \"Transaction Processor\"\n+{\n+ procedure GetPaymentDiscount(TermsCode: Code[10]): Decimal\n+ var\n+ PaymentTerms: Record \"Payment Terms\";\n+ begin\n+ if PaymentTerms.Get(TermsCode) then\n+ exit(PaymentTerms.\"Discount %\");\n+ exit(0);\n+ end;\n+\n+ procedure GetReasonCodeDescription(ReasonCode: Code[10]): Text[100]\n+ var\n+ ReasonCodeRec: Record \"Reason Code\";\n+ begin\n+ ReasonCodeRec.ReadIsolation := IsolationLevel::ReadCommitted;\n+ if ReasonCodeRec.Get(ReasonCode) then\n+ exit(ReasonCodeRec.Description);\n+ exit('');\n+ end;\n+}\ndiff --git a/src/CustomerReportReader.Codeunit.al b/src/CustomerReportReader.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/CustomerReportReader.Codeunit.al\n@@ -0,0 +1,16 @@\n+codeunit 50404 \"Customer Report Reader\"\n+{\n+ procedure ExportActiveCustomerNumbers(): Text\n+ var\n+ Customer: Record Customer;\n+ Result: TextBuilder;\n+ begin\n+ Customer.SetRange(Blocked, Customer.Blocked::\" \");\n+ if Customer.FindSet() then\n+ repeat\n+ Result.Append(Customer.\"No.\");\n+ Result.Append(';');\n+ until Customer.Next() = 0;\n+ exit(Result.ToText());\n+ end;\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: read-only Customer iteration using FindSet() without an update lock (derived from performance-007). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} {"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-clean-03", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "diff --git a/src/NameValueBufferAPI.Page.al b/src/NameValueBufferAPI.Page.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/NameValueBufferAPI.Page.al\n@@ -0,0 +1,63 @@\n+page 50271 \"Name Value Buffer API\"\n+{\n+ PageType = API;\n+ APIGroup = 'configuration';\n+ APIVersion = 'v1.0';\n+ EntityName = 'nameValueBuffer';\n+ EntitySetName = 'nameValueBuffers';\n+ SourceTable = \"Name/Value Buffer\";\n+ SourceTableTemporary = true;\n+ DelayedInsert = true;\n+\n+ layout\n+ {\n+ area(Content)\n+ {\n+ repeater(Buffers)\n+ {\n+ field(id; Rec.ID)\n+ {\n+ Caption = 'ID';\n+ }\n+\n+ field(name; Rec.Name)\n+ {\n+ Caption = 'Name';\n+ }\n+\n+ field(value; Rec.Value)\n+ {\n+ Caption = 'Value';\n+ }\n+\n+ field(valueLong; Rec.\"Value BLOB\")\n+ {\n+ Caption = 'Value Long';\n+ }\n+ }\n+ }\n+ }\n+\n+ trigger OnInsertRecord(BelowxRec: Boolean): Boolean\n+ var\n+ NextID: Integer;\n+ begin\n+ if Rec.FindLast() then\n+ NextID := Rec.ID + 1\n+ else\n+ NextID := 1;\n+\n+ Rec.ID := NextID;\n+ exit(true);\n+ end;\n+\n+ trigger OnModifyRecord(): Boolean\n+ begin\n+ if Rec.Name = '' then\n+ Error(NameEmptyErr);\n+ exit(true);\n+ end;\n+\n+ var\n+ NameEmptyErr: Label 'Name cannot be empty';\n+}\ndiff --git a/src/RecordSetAggregator.Codeunit.al b/src/RecordSetAggregator.Codeunit.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/RecordSetAggregator.Codeunit.al\n@@ -0,0 +1,33 @@\n+codeunit 50270 \"Record Set Aggregator\"\n+{\n+ Access = Internal;\n+\n+ procedure CalculateOutstandingTotal(var TempSalesLine: Record \"Sales Line\" temporary): Decimal\n+ var\n+ Total: Decimal;\n+ begin\n+ if TempSalesLine.FindSet() then\n+ repeat\n+ TempSalesLine.CalcFields(\"Outstanding Amount\");\n+ Total += TempSalesLine.\"Outstanding Amount\";\n+ until TempSalesLine.Next() = 0;\n+ exit(Total);\n+ end;\n+\n+ procedure FindMaxUnitPrice(var TempItem: Record Item temporary): Decimal\n+ var\n+ MaxUnitPrice: Decimal;\n+ begin\n+ if TempItem.FindSet() then\n+ repeat\n+ if TempItem.\"Unit Price\" > MaxUnitPrice then\n+ MaxUnitPrice := TempItem.\"Unit Price\";\n+ until TempItem.Next() = 0;\n+ exit(MaxUnitPrice);\n+ end;\n+\n+ procedure CountAnalysisEntries(var TempAnalysisReportChartSetup: Record \"Analysis Report Chart Setup\" temporary): Integer\n+ begin\n+ exit(TempAnalysisReportChartSetup.Count());\n+ end;\n+}\ndiff --git a/src/OutboxEmailAPI.Page.al b/src/OutboxEmailAPI.Page.al\nnew file mode 100644\n--- /dev/null\n+++ b/src/OutboxEmailAPI.Page.al\n@@ -0,0 +1,35 @@\n+page 50272 \"Outbox Email API\"\n+{\n+ PageType = API;\n+ APIGroup = 'email';\n+ APIVersion = 'v1.0';\n+ EntityName = 'outboxEmail';\n+ EntitySetName = 'outboxEmails';\n+ SourceTable = \"Email Outbox\";\n+ SourceTableTemporary = true;\n+ DelayedInsert = true;\n+\n+ layout\n+ {\n+ area(Content)\n+ {\n+ repeater(Outbox)\n+ {\n+ field(id; Rec.Id)\n+ {\n+ Caption = 'Id';\n+ }\n+\n+ field(description; Rec.Description)\n+ {\n+ Caption = 'Description';\n+ }\n+\n+ field(status; Rec.Status)\n+ {\n+ Caption = 'Status';\n+ }\n+ }\n+ }\n+ }\n+}\n", "expected_comments": [], "category": "code-review", "description": "Negative example: API page backed by a temporary source table (derived from performance-009 with the missing SourceTableTemporary resolved). Exercises false-positive rate.", "expect_findings": false, "source": "vsoadmin"} diff --git a/src/bcbench/commands/dataset.py b/src/bcbench/commands/dataset.py index 35d63f878..087948b9a 100644 --- a/src/bcbench/commands/dataset.py +++ b/src/bcbench/commands/dataset.py @@ -182,6 +182,92 @@ def version( write_step_outputs({github_output: entry.environment_setup_version}) +@dataset_app.command("coverage") +def coverage( + category: EvaluationCategoryOption = EvaluationCategory.CODE_REVIEW, + bcquality_root: Annotated[ + str | None, + typer.Option(help="Path to a BCQuality checkout; enables zero-coverage detection. Falls back to $BCQUALITY_ROOT."), + ] = None, + show_zero: Annotated[bool, typer.Option(help="List every zero-coverage article (can be long)")] = False, + github_output: Annotated[str | None, typer.Option(help="Write the coverage report as JSON to GITHUB_OUTPUT with this key name")] = None, +) -> None: + """Report per-article BCQuality coverage of the code-review dataset.""" + from rich.console import Console + from rich.table import Table + + from bcbench.dataset import ( + CodeReviewEntry, + build_coverage_report, + enumerate_inventory, + resolve_bcquality_root, + ) + + if category is not EvaluationCategory.CODE_REVIEW: + raise typer.BadParameter("coverage is only supported for the code-review category") + + entries: list[CodeReviewEntry] = CodeReviewEntry.load(category.dataset_path) + + resolved_root = resolve_bcquality_root(bcquality_root) + inventory: set[str] | None = None + if resolved_root is not None: + inventory = enumerate_inventory(resolved_root) + + report = build_coverage_report(entries, inventory) + console = Console() + + if not report.inventory_available: + console.print("[yellow]No BCQuality checkout provided (--bcquality-root / $BCQUALITY_ROOT); reporting declared articles only, zero-coverage undetermined.[/yellow]") + + domains = sorted({c.domain for c in report.covered} | {_domain_of(a) for a in report.zero_coverage}) + covered_by_domain: dict[str, int] = {} + entries_by_domain: dict[str, int] = {} + for cov in report.covered: + covered_by_domain[cov.domain] = covered_by_domain.get(cov.domain, 0) + 1 + entries_by_domain[cov.domain] = entries_by_domain.get(cov.domain, 0) + cov.count + zero_by_domain: dict[str, int] = {} + for article in report.zero_coverage: + domain = _domain_of(article) + zero_by_domain[domain] = zero_by_domain.get(domain, 0) + 1 + + table = Table(title="Per-article coverage (code-review)", title_justify="left", title_style="bold cyan") + table.add_column("Domain", style="cyan") + table.add_column("Covered", justify="right") + if report.inventory_available: + table.add_column("Inventory", justify="right") + table.add_column("Zero-cov", justify="right", style="red") + table.add_column("Gold entries", justify="right") + for domain in domains: + covered_count = covered_by_domain.get(domain, 0) + row = [domain, str(covered_count)] + if report.inventory_available: + inv = covered_count + zero_by_domain.get(domain, 0) + row += [str(inv), str(zero_by_domain.get(domain, 0))] + row.append(str(entries_by_domain.get(domain, 0))) + table.add_row(*row) + console.print(table) + + summary = f"[bold]{len(report.covered)}[/bold] articles covered across [bold]{report.annotated_entries}[/bold]/{report.total_entries} annotated entries" + if report.inventory_available: + summary += f"; [red]{len(report.zero_coverage)}[/red] of {report.inventory_size} articles have zero coverage" + console.print(summary) + + if report.unknown_articles: + console.print(f"[yellow]Unknown article slugs (not in inventory): {', '.join(report.unknown_articles)}[/yellow]") + + if show_zero and report.zero_coverage: + console.print("\n[bold red]Zero-coverage articles:[/bold red]") + for article in report.zero_coverage: + console.print(f" - {article}") + + if github_output: + write_step_outputs({github_output: report.model_dump_json()}) + + +def _domain_of(article: str) -> str: + return article.split("/", 1)[0] + + def _modified_instance_ids_from_diff(diff_output: str) -> list[str]: instance_ids = [] diff --git a/src/bcbench/dataset/__init__.py b/src/bcbench/dataset/__init__.py index 5e8227969..5f8e19c02 100644 --- a/src/bcbench/dataset/__init__.py +++ b/src/bcbench/dataset/__init__.py @@ -1,16 +1,30 @@ """Dataset module for querying, validating and analyze dataset entries.""" from bcbench.dataset.codereview import CodeReviewEntry, ReviewComment, Severity +from bcbench.dataset.coverage import ( + ArticleCoverage, + CoverageReport, + build_coverage_report, + collect_declared_articles, + enumerate_inventory, + resolve_bcquality_root, +) from bcbench.dataset.dataset_entry import BaseDatasetEntry, BugFixEntry, NL2ALEntry, RepoGroundedEntry, TestEntry, TestGenEntry __all__ = [ + "ArticleCoverage", "BaseDatasetEntry", "BugFixEntry", "CodeReviewEntry", + "CoverageReport", "NL2ALEntry", "RepoGroundedEntry", "ReviewComment", "Severity", "TestEntry", "TestGenEntry", + "build_coverage_report", + "collect_declared_articles", + "enumerate_inventory", + "resolve_bcquality_root", ] diff --git a/src/bcbench/dataset/codereview.py b/src/bcbench/dataset/codereview.py index 2048fff41..d5002bc99 100644 --- a/src/bcbench/dataset/codereview.py +++ b/src/bcbench/dataset/codereview.py @@ -58,6 +58,10 @@ class ReviewComment(BaseModel): domain: str | None = None body: Annotated[str, Field(min_length=1)] severity: Severity | None = None + # BCQuality knowledge article this finding derives from, as `/` + # (e.g. `security/hardcoded-secret`). Optional and backward-compatible; drives + # per-article coverage tracking. Older entries leave it unset (counted as unannotated). + article: Annotated[str, Field(pattern=r"^[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$")] | None = None @field_validator("severity", mode="before") @classmethod @@ -87,3 +91,14 @@ def get_task(self) -> str: def get_expected_output(self) -> str: return "\n".join(str(c) for c in self.expected_comments) + + def declared_articles(self) -> set[str]: + """BCQuality articles this entry is annotated against. + + Union of every expected comment's `article` and the entry-level + `metadata.articles` (which carries the association for false-positive-guard + entries whose `expected_comments` is empty). + """ + articles = {c.article for c in self.expected_comments if c.article} + articles.update(self.metadata.articles) + return articles diff --git a/src/bcbench/dataset/coverage.py b/src/bcbench/dataset/coverage.py new file mode 100644 index 000000000..c32ad9c42 --- /dev/null +++ b/src/bcbench/dataset/coverage.py @@ -0,0 +1,138 @@ +"""Per-article coverage tracking for the code-review dataset. + +Gold entries are annotated with the BCQuality knowledge article(s) they exercise +(`/`). This module aggregates those annotations and, when a BCQuality +checkout is available, compares them against the full article inventory to surface +which articles have zero gold coverage. +""" + +from __future__ import annotations + +import os +from collections.abc import Iterable, Sequence +from pathlib import Path + +from pydantic import BaseModel, ConfigDict, Field + +from bcbench.dataset.codereview import CodeReviewEntry + +# Relative to a BCQuality checkout root; knowledge articles live here as +# `/.md`, so the article id is the relative path minus the suffix. +_KNOWLEDGE_SUBDIR = Path("microsoft") / "knowledge" +_BCQUALITY_ROOT_ENV = "BCQUALITY_ROOT" + + +class ArticleCoverage(BaseModel): + """One article and the gold entries that exercise it.""" + + model_config = ConfigDict(frozen=True) + + article: str + domain: str + entry_ids: list[str] = Field(default_factory=list) + + @property + def count(self) -> int: + return len(self.entry_ids) + + +class CoverageReport(BaseModel): + """Per-article coverage of the code-review dataset.""" + + model_config = ConfigDict(frozen=True) + + covered: list[ArticleCoverage] = Field(default_factory=list) + zero_coverage: list[str] = Field(default_factory=list) + unknown_articles: list[str] = Field(default_factory=list) + unannotated_entry_ids: list[str] = Field(default_factory=list) + total_entries: int = 0 + inventory_available: bool = False + + @property + def inventory_size(self) -> int: + if not self.inventory_available: + return 0 + return len(self.covered) + len(self.zero_coverage) + + @property + def annotated_entries(self) -> int: + return self.total_entries - len(self.unannotated_entry_ids) + + +def _domain_of(article: str) -> str: + return article.split("/", 1)[0] + + +def collect_declared_articles(entries: Sequence[CodeReviewEntry]) -> dict[str, list[str]]: + """Map each declared article to the sorted, de-duplicated entry ids that declare it.""" + article_to_entries: dict[str, set[str]] = {} + for entry in entries: + for article in entry.declared_articles(): + article_to_entries.setdefault(article, set()).add(entry.instance_id) + return {article: sorted(ids) for article, ids in article_to_entries.items()} + + +def enumerate_inventory(bcquality_root: Path) -> set[str]: + """Enumerate `/` article ids from a BCQuality checkout. + + Only `.md` files under `microsoft/knowledge/` count; sibling `.good.al` / `.bad.al` + sample files and the generated `knowledge-index.json` are ignored. + """ + knowledge_dir = bcquality_root / _KNOWLEDGE_SUBDIR + if not knowledge_dir.is_dir(): + raise FileNotFoundError(f"BCQuality knowledge directory not found: {knowledge_dir}") + + inventory: set[str] = set() + for path in knowledge_dir.rglob("*.md"): + relative = path.relative_to(knowledge_dir).with_suffix("") + inventory.add(relative.as_posix()) + return inventory + + +def resolve_bcquality_root(explicit: Path | str | None = None) -> Path | None: + """Resolve a BCQuality checkout root from an explicit value or `BCQUALITY_ROOT`. + + Returns None when neither is set, so callers can degrade to declared-only coverage. + """ + candidate = explicit if explicit is not None else os.environ.get(_BCQUALITY_ROOT_ENV) + if not candidate: + return None + return Path(candidate).expanduser() + + +def build_coverage_report( + entries: Sequence[CodeReviewEntry], + inventory: Iterable[str] | None = None, +) -> CoverageReport: + """Compute per-article coverage. + + When `inventory` is provided, articles in the inventory with no declaring entry are + reported as `zero_coverage`, and declared articles absent from the inventory (typos / + stale slugs) are reported as `unknown_articles`. Without an inventory, only declared + articles are reported (`covered`), and zero-coverage cannot be determined. + """ + declared = collect_declared_articles(entries) + inventory_set = set(inventory) if inventory is not None else None + + covered: list[ArticleCoverage] = [] + unknown: list[str] = [] + for article in sorted(declared): + if inventory_set is not None and article not in inventory_set: + unknown.append(article) + continue + covered.append(ArticleCoverage(article=article, domain=_domain_of(article), entry_ids=declared[article])) + + zero_coverage: list[str] = [] + if inventory_set is not None: + zero_coverage = sorted(inventory_set - set(declared)) + + unannotated = sorted(e.instance_id for e in entries if not e.declared_articles()) + + return CoverageReport( + covered=covered, + zero_coverage=zero_coverage, + unknown_articles=sorted(unknown), + unannotated_entry_ids=unannotated, + total_entries=len(entries), + inventory_available=inventory_set is not None, + ) diff --git a/src/bcbench/dataset/dataset_entry.py b/src/bcbench/dataset/dataset_entry.py index 637be599e..39c70df8e 100644 --- a/src/bcbench/dataset/dataset_entry.py +++ b/src/bcbench/dataset/dataset_entry.py @@ -30,6 +30,10 @@ class EntryMetadata(BaseModel): area: str | None = None image_count: Annotated[int, Field(ge=0)] | None = None persona: str | None = None + # BCQuality knowledge articles this entry exercises as `/`. Primarily + # for false-positive-guard entries (expected_comments=[]) that test an article by + # omission and thus have no per-comment `article` to carry the association. + articles: list[Annotated[str, Field(pattern=r"^[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$")]] = Field(default_factory=list) class BaseDatasetEntry(BaseModel): diff --git a/tests/test_codereview_coverage.py b/tests/test_codereview_coverage.py new file mode 100644 index 000000000..448942528 --- /dev/null +++ b/tests/test_codereview_coverage.py @@ -0,0 +1,116 @@ +"""Tests for per-article BCQuality coverage of the code-review dataset.""" + +from pathlib import Path + +from bcbench.dataset import ( + CodeReviewEntry, + ReviewComment, + build_coverage_report, + collect_declared_articles, + enumerate_inventory, + resolve_bcquality_root, +) +from bcbench.dataset.dataset_entry import EntryMetadata + +_BASE_COMMIT = "70fd0246a0a4dbc72cb183ca719106722c03be4d" + + +def _entry(instance_id: str, *, comments: list[ReviewComment] | None = None, articles: list[str] | None = None) -> CodeReviewEntry: + metadata = EntryMetadata(area="security", articles=articles or []) + return CodeReviewEntry( + instance_id=instance_id, + created_at="2026-01-01", + environment_setup_version="27.0", + base_commit=_BASE_COMMIT, + patch="diff --git a/src/A.al b/src/A.al\n+x", + expected_comments=comments or [], + metadata=metadata, + ) + + +def _comment(body: str, *, article: str | None = None) -> ReviewComment: + return ReviewComment(file="src/A.al", line_start=1, body=body, article=article) + + +class TestDeclaredArticles: + def test_declared_from_comment_and_metadata(self): + entry = _entry( + "synthetic__security-001", + comments=[_comment("finding", article="security/secrettext-for-credentials")], + articles=["security/permission-set-avoid-wildcard-grants"], + ) + assert entry.declared_articles() == { + "security/secrettext-for-credentials", + "security/permission-set-avoid-wildcard-grants", + } + + def test_unannotated_entry_declares_nothing(self): + entry = _entry("synthetic__security-002", comments=[_comment("finding")]) + assert entry.declared_articles() == set() + + def test_collect_maps_article_to_sorted_entry_ids(self): + shared = "security/validate-user-configurable-urls" + entries = [ + _entry("synthetic__security-015", comments=[_comment("ssrf", article=shared)]), + _entry("synthetic__security-011", comments=[_comment("ssrf", article=shared)]), + ] + declared = collect_declared_articles(entries) + assert declared[shared] == ["synthetic__security-011", "synthetic__security-015"] + + +class TestCoverageReport: + def test_without_inventory_reports_declared_only(self): + entries = [ + _entry("synthetic__security-001", comments=[_comment("x", article="security/secrettext-for-credentials")]), + _entry("synthetic__security-002", comments=[_comment("y")]), + ] + report = build_coverage_report(entries, inventory=None) + assert report.inventory_available is False + assert report.zero_coverage == [] + assert [c.article for c in report.covered] == ["security/secrettext-for-credentials"] + assert report.unannotated_entry_ids == ["synthetic__security-002"] + assert report.annotated_entries == 1 + + def test_with_inventory_flags_zero_and_unknown(self): + inventory = { + "security/secrettext-for-credentials", + "security/permission-set-avoid-wildcard-grants", + } + entries = [ + _entry("synthetic__security-001", comments=[_comment("x", article="security/secrettext-for-credentials")]), + _entry("synthetic__security-009", comments=[_comment("z", article="security/made-up-slug")]), + ] + report = build_coverage_report(entries, inventory=inventory) + assert report.inventory_available is True + assert [c.article for c in report.covered] == ["security/secrettext-for-credentials"] + assert report.zero_coverage == ["security/permission-set-avoid-wildcard-grants"] + assert report.unknown_articles == ["security/made-up-slug"] + assert report.inventory_size == 2 + + def test_covered_entry_ids_deduplicated_across_entries(self): + article = "security/inherent-permissions-minimal-grant" + entries = [ + _entry("synthetic__security-013", comments=[_comment("a", article=article), _comment("b", article=article)]), + ] + report = build_coverage_report(entries, inventory={article}) + assert len(report.covered) == 1 + assert report.covered[0].entry_ids == ["synthetic__security-013"] + assert report.covered[0].count == 1 + + +class TestInventoryEnumeration: + def test_enumerate_ignores_al_samples_and_index(self, tmp_path: Path): + knowledge = tmp_path / "microsoft" / "knowledge" / "security" + knowledge.mkdir(parents=True) + (knowledge / "secrettext-for-credentials.md").write_text("x", encoding="utf-8") + (knowledge / "secrettext-for-credentials.good.al").write_text("x", encoding="utf-8") + (tmp_path / "microsoft" / "knowledge" / "knowledge-index.json").write_text("{}", encoding="utf-8") + assert enumerate_inventory(tmp_path) == {"security/secrettext-for-credentials"} + + def test_resolve_prefers_explicit_over_env(self, tmp_path: Path, monkeypatch): + monkeypatch.setenv("BCQUALITY_ROOT", str(tmp_path / "from-env")) + assert resolve_bcquality_root(str(tmp_path / "explicit")) == tmp_path / "explicit" + + def test_resolve_returns_none_without_source(self, monkeypatch): + monkeypatch.delenv("BCQUALITY_ROOT", raising=False) + assert resolve_bcquality_root(None) is None