Complete AL review knowledge readiness

Fill telemetry and Query coverage, strengthen thin review domains, correct audited content defects, and add deterministic cheap-model evaluation and reference-integrity safeguards.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9825b012-e653-496a-9310-c1f4b6f8ac27
This commit is contained in:
Jesper Schulz-Wedde 2026-07-15 07:19:15 +02:00
parent 809af9708e
commit e81632b4be
103 changed files with 2350 additions and 210 deletions

View file

@ -1,28 +0,0 @@
table 50100 "Customer Feedback"
{
fields
{
field(1; "Feedback No."; Code[20])
{
// No DataClassification declared. Defaults to ToBeClassified.
}
field(2; "Contact Name"; Text[100])
{
DataClassification = ToBeClassified;
}
field(3; "Email"; Text[80])
{
// Personal data classified as CustomerContent understates privacy impact.
DataClassification = CustomerContent;
}
field(4; "Feedback Text"; Text[2048])
{
DataClassification = ToBeClassified;
}
}
keys
{
key(PK; "Feedback No.") { Clustered = true; }
}
}

View file

@ -1,36 +0,0 @@
table 50100 "Customer Feedback"
{
fields
{
field(1; "Feedback No."; Code[20])
{
DataClassification = SystemMetadata;
}
field(2; "Contact Name"; Text[100])
{
DataClassification = EndUserIdentifiableInformation;
}
field(3; "Email"; Text[80])
{
DataClassification = EndUserIdentifiableInformation;
}
field(4; "Product Code"; Code[20])
{
DataClassification = CustomerContent;
}
field(5; "Feedback Text"; Text[2048])
{
// When uncertain between CustomerContent and EUII, prefer the stronger protection.
DataClassification = EndUserIdentifiableInformation;
}
field(6; "Submitted DateTime"; DateTime)
{
DataClassification = SystemMetadata;
}
}
keys
{
key(PK; "Feedback No.") { Clustered = true; }
}
}

View file

@ -1,26 +0,0 @@
---
bc-version: [all]
domain: security
keywords: [dataclassification, gdpr, privacy, euii, compliance]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Classify every field with DataClassification
## Description
Every field on every AL table and table extension must have a resolved `DataClassification` value, either declared directly on the field or inherited from a table-level default. The value drives GDPR tooling, data-subject requests, retention policies, and audit reporting — all of which rely on the field metadata to know what data to include, anonymize, or delete. A field with no field-level property and no table-level default resolves to `ToBeClassified`, which is a compliance gap, not a neutral state.
## Best Practice
Choose the narrowest value that accurately describes the field's content: `EndUserIdentifiableInformation` for data that directly identifies a person, `EndUserPseudonymousIdentifiers` for indirect identifiers, `CustomerContent` for business operational data, `SystemMetadata` for system-generated housekeeping, `AccountData` for tenant/billing, `OrganizationIdentifiableInformation` for organization-level identifiers. Use a table-level default for homogeneous tables, and override individual fields whose content differs from that default. When uncertain between two values, pick the stronger protection.
See sample: `classify-every-field-with-dataclassification.good.al`.
## Anti Pattern
Leaving `DataClassification = ToBeClassified` on a field, omitting classification when the table has no default, or relying on a table-level default that understates a field's actual content. Code in this state fails compliance audits and breaks the subject-access-request and retention tooling that depends on the property being set correctly.
See sample: `classify-every-field-with-dataclassification.bad.al`.

View file

@ -0,0 +1,15 @@
codeunit 50471 "Unprotected Setup Action"
{
Access = Internal;
trigger OnRun()
begin
// Internal does not prevent another extension from invoking this OnRun
// through Codeunit.Run.
UpdateSensitiveSetup();
end;
local procedure UpdateSensitiveSetup()
begin
end;
}

View file

@ -0,0 +1,39 @@
table 50468 "Sensitive Setup"
{
DataClassification = CustomerContent;
fields
{
field(1; "Primary Key"; Code[10]) { }
}
}
codeunit 50469 "Setup Authorization"
{
procedure CanManageSetup(): Boolean
var
SensitiveSetup: Record "Sensitive Setup";
begin
exit(SensitiveSetup.WritePermission());
end;
}
codeunit 50470 "Protected Setup Action"
{
Access = Internal;
trigger OnRun()
begin
if not SetupAuthorization.CanManageSetup() then
Error(NotAuthorizedErr);
UpdateSensitiveSetup();
end;
local procedure UpdateSensitiveSetup()
begin
end;
var
SetupAuthorization: Codeunit "Setup Authorization";
NotAuthorizedErr: Label 'You are not authorized to manage this setup.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: security
keywords: [access, internal, internalsvisibleto, recordref, codeunit-run, security-boundary, authorization]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Access Internal is API hygiene, not an authorization boundary
## Description
`Access = Internal` controls compile-time symbol visibility. It does not prevent runtime access through mechanisms such as `RecordRef`, `TransferFields`, or `Codeunit.Run`, and `internalsVisibleTo` deliberately grants compile-time access to named companion apps. Microsoft explicitly documents that access modifiers cannot be used as a security boundary.
## Best Practice
Use `internal` to keep implementation details out of the supported API, but enforce sensitive operations with permissions, entitlements, and explicit authorization checks appropriate to the operation. Treat `internalsVisibleTo` as a same-publisher development/testability relationship, not as a trust grant for secrets or elevated data access.
See sample: `internal-access-is-not-a-security-boundary.good.al`.
## Anti Pattern
Placing privileged work in an internal codeunit and claiming that other extensions cannot invoke it, or exposing an app to a different publisher through `internalsVisibleTo` because `internal` is assumed to protect the underlying operation. The access modifier narrows supported callers; it does not authenticate runtime callers.
See sample: `internal-access-is-not-a-security-boundary.bad.al`.

View file

@ -17,6 +17,10 @@ API keys, OAuth tokens, client secrets, and connection strings must not be store
Persist every credential in `IsolatedStorage`, write it at the point of capture, and read it only when needed. Prefer `SetEncrypted` when the value fits its documented length limit. On BC24 and later, carry the value through the `SecretText` overloads; on earlier releases, keep any required `Text` handling inside a `[NonDebuggable]` boundary. Choose the `DataScope` that matches the credential's lifetime. See `isolatedstorage-datascope-module-vs-company`, `isolatedstorage-setencrypted-for-sensitive-values`, and `secrettext-for-credentials` for those separate concerns.
See sample: `secrets-isolated-storage.good.al`.
## Anti Pattern
A "Setup" or "Connection" table carrying a `Text` field named `API Key`, `Password`, or `Client Secret`. The value is now readable by any object with table permission, ships in RapidStart packages and Excel exports, and appears in record snapshots — a credential disclosure that no amount of encryption-in-transit elsewhere makes up for. Reviewer signal: a secret-shaped field declared on a table instead of an `IsolatedStorage` call.
See sample: `secrets-isolated-storage.bad.al`.

View file

@ -19,4 +19,4 @@ Declare credential-carrying parameters and variables as `SecretText` from the ca
## Anti Pattern
Holding a credential in a `Text` variable (`BearerToken: Text`), concatenating it into a header, then passing it to `HttpClient`. The token is visible in the debugger and in any error that prints the variable, and the compiler offers no help because the type was wrong from the start. Reviewers should flag any local or parameter named like a secret (`ApiKey`, `Token`, `Password`, `ClientSecret`) whose type is `Text` or `Code`. See sample: `secrettext-for-credentials.bad.al`.
Holding a credential in a `Text` variable (`BearerToken: Text`) makes it visible in the debugger and in any error that prints the variable, and the compiler offers no help because the type was wrong from the start. Reviewers should flag any local or parameter named like a secret (`ApiKey`, `Token`, `Password`, `ClientSecret`) whose type is `Text` or `Code`. When the same value is visibly sent through an HTTP URI, header, or body, `secrettext-with-httpclient.md` is the more specific primary rule. See sample: `secrettext-for-credentials.bad.al`.

View file

@ -19,4 +19,4 @@ Compose a secret URI with `SecretStrSubstNo`, call `Request.SetSecretRequestUri(
## Anti Pattern
Holding a credential in `Text`, interpolating it with `StrSubstNo` or concatenation, and passing that plain text to `HttpClient.Get` or `HttpHeaders.Add`. The secret-aware request and header APIs remove the need to materialize the value as `Text`. See sample: `secrettext-with-httpclient.bad.al`.
Holding a credential in `Text`, interpolating it with `StrSubstNo` or concatenation, and passing that plain text to `HttpClient.Get` or `HttpHeaders.Add`. The secret-aware request and header APIs remove the need to materialize the value as `Text`. This HTTP-sink rule supersedes the generic `secrettext-for-credentials.md` rule at the same location. See sample: `secrettext-with-httpclient.bad.al`.