├── BusinessCentral.Sentinel ├── AppSourceCop.json ├── logo.png ├── src │ ├── Telemetry │ │ ├── TelemetryFeatures.Enum.al │ │ ├── TelemetryHelper.Codeunit.al │ │ └── SentinelTelemetryLogger.Codeunit.al │ ├── Area.Enum.al │ ├── Setup │ │ ├── TelemetryLogging.Enum.al │ │ ├── SentinelSetup.Page.al │ │ └── SentinelSetup.Table.al │ ├── ReRunAllAlerts.Codeunit.al │ ├── Severity.Enum.al │ ├── SentinelRuleSet.Page.al │ ├── IgnoredAlerts.Table.al │ ├── Alert.Codeunit.al │ ├── SentinelRuleSet.Table.al │ ├── IAuditAlert.Interface.al │ ├── AlertCode.Enum.al │ ├── Rules │ │ ├── EvaluationCompanyInProd.Codeunit.al │ │ ├── AlertDevScopeExt.Codeunit.al │ │ ├── AnalysisNotScheduled.Codeunit.al │ │ ├── DemoDataExtInProd.Codeunit.al │ │ ├── UserWithSuper.Codeunit.al │ │ ├── AlertPteDownloadCode.Codeunit.al │ │ ├── NonPostNoSeriesGaps.Codeunit.al │ │ └── UnusedExtensionInstalled.Codeunit.al │ ├── AlertCard.Page.al │ ├── Alert.Table.al │ └── AlertList.Page.al ├── .vscode │ └── launch.json ├── SentinelAdmin.PermissionSet.al └── app.json ├── .github ├── Test Current.settings.json ├── Test Next Major.settings.json ├── Test Next Minor.settings.json ├── AL-Go-Settings.json └── workflows │ ├── Troubleshooting.yaml │ ├── DeployReferenceDocumentation.yaml │ ├── AddExistingAppOrTestApp.yaml │ ├── IncrementVersionNumber.yaml │ ├── CreateApp.yaml │ ├── CreateTestApp.yaml │ ├── PublishToAppSource.yaml │ ├── CreatePerformanceTestApp.yaml │ ├── Current.yaml │ ├── NextMajor.yaml │ ├── NextMinor.yaml │ ├── PullRequestHandler.yaml │ ├── UpdateGitHubGoSystemFiles.yaml │ ├── CreateOnlineDevelopmentEnvironment.yaml │ ├── PublishToEnvironment.yaml │ ├── _BuildALGoProject.yaml │ ├── CICD.yaml │ └── CreateRelease.yaml ├── .gitignore ├── custom.ruleset.json ├── .AL-Go ├── settings.json ├── cloudDevEnv.ps1 └── localDevEnv.ps1 ├── al.code-workspace ├── LICENSE ├── README.md └── CHANGELOG.md /BusinessCentral.Sentinel/AppSourceCop.json: -------------------------------------------------------------------------------- 1 | { 2 | "mandatoryAffixes": [ 3 | "SESTM" 4 | ] 5 | } -------------------------------------------------------------------------------- /.github/Test Current.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "artifact": "////latest", 3 | "cacheImageName": "", 4 | "versioningStrategy": 15 5 | } 6 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanMaron/BusinessCentral.Sentinel/HEAD/BusinessCentral.Sentinel/logo.png -------------------------------------------------------------------------------- /.github/Test Next Major.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "artifact": "////nextmajor", 3 | "cacheImageName": "", 4 | "versioningStrategy": 15 5 | } 6 | -------------------------------------------------------------------------------- /.github/Test Next Minor.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "artifact": "////nextminor", 3 | "cacheImageName": "", 4 | "versioningStrategy": 15 5 | } 6 | -------------------------------------------------------------------------------- /.github/AL-Go-Settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "AppSource App", 3 | "templateUrl": "https://github.com/microsoft/AL-Go-AppSource@main", 4 | "templateSha": "0a3467404bc45069db7856c4c7f5cfd6b80a3eee" 5 | } 6 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Telemetry/TelemetryFeatures.Enum.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | enum 71180278 "TelemetryFeaturesSESTM" 4 | { 5 | Access = Internal; 6 | Extensible = false; 7 | 8 | value(1; "SESTM0000001") 9 | { 10 | Caption = 'Alert Created', Locked = true; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.app 2 | *.flf 3 | *.bclicense 4 | *.g.xlf 5 | .DS_Store 6 | Thumbs.db 7 | TestResults*.xml 8 | bcptTestResults*.json 9 | BuildOutput.txt 10 | rad.json 11 | .output/ 12 | .dependencies/ 13 | .buildartifacts/ 14 | .alpackages/ 15 | .packages/ 16 | .alcache/ 17 | .altemplates/ 18 | .altestrunner/ 19 | .snapshots/ 20 | cache_* 21 | ~$* 22 | -------------------------------------------------------------------------------- /custom.ruleset.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Custom", 3 | "description": "Description", 4 | "includedRuleSets": [ 5 | { 6 | "action": "Default", 7 | "path": "https://raw.githubusercontent.com/StefanMaron/RulesetFiles/main/appsource.rulset.json" 8 | } 9 | ], 10 | "rules": [ 11 | { 12 | "id": "AA0248", 13 | "action": "Warning", 14 | "justification": "Add this where possible" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Area.Enum.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | enum 71180277 AreaSESTM 4 | { 5 | Access = Internal; 6 | Extensible = false; 7 | 8 | value(1; Technical) 9 | { 10 | Caption = 'Technical'; 11 | } 12 | value(2; Database) 13 | { 14 | Caption = 'Database'; 15 | } 16 | value(3; Performance) 17 | { 18 | Caption = 'Performance'; 19 | } 20 | value(4; Permissions) 21 | { 22 | Caption = 'Permissions'; 23 | } 24 | } -------------------------------------------------------------------------------- /.AL-Go/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "country": "w1", 3 | "appSourceCopMandatoryAffixes": [ 4 | "SESTM" 5 | ], 6 | "enableAppSourceCop": true, 7 | "enableExternalRulesets": true, 8 | "rulesetFile": "custom.ruleset.json", 9 | "appFolders": [], 10 | "testFolders": [], 11 | "bcptTestFolders": [], 12 | "deliverToAppSource": { 13 | "productId": "c1566843-a454-4143-8828-ae9118f11efa", 14 | "continuousDelivery": false, 15 | "mainAppFolder": "BusinessCentral.Sentinel" 16 | }, 17 | "repoVersion": "1.9" 18 | } 19 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Setup/TelemetryLogging.Enum.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | enum 71180279 TelemetryLogging 4 | { 5 | Access = Internal; 6 | Extensible = true; 7 | 8 | value(0; " ") 9 | { 10 | Caption = ' ', Locked = true; 11 | } 12 | value(1; Daily) 13 | { 14 | Caption = 'Daily'; 15 | } 16 | value(2; OnRuleLogging) 17 | { 18 | Caption = 'On Rule Logging'; 19 | } 20 | value(3; Off) 21 | { 22 | Caption = 'Off'; 23 | } 24 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/ReRunAllAlerts.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | using System.Reflection; 3 | using System.DateTime; 4 | using STM.BusinessCentral.Sentinel; 5 | using System.Threading; 6 | 7 | codeunit 71180286 ReRunAllAlerts 8 | { 9 | Access = Internal; 10 | Permissions = 11 | tabledata AlertSESTM = R, 12 | tabledata "Job Queue Entry" = RI; 13 | TableNo = "Job Queue Entry"; 14 | 15 | trigger OnRun() 16 | var 17 | Alert: Record AlertSESTM; 18 | begin 19 | Alert.FullRerun(); 20 | end; 21 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Setup/SentinelSetup.Page.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | page 71180278 SentinelSetup 4 | { 5 | ApplicationArea = All; 6 | Caption = 'Sentinel Setup'; 7 | DeleteAllowed = false; 8 | Extensible = false; 9 | PageType = Card; 10 | SourceTable = SentinelSetup; 11 | UsageCategory = Administration; 12 | 13 | layout 14 | { 15 | area(Content) 16 | { 17 | group(General) 18 | { 19 | Caption = 'General'; 20 | 21 | field(TelemetryLogging; Rec.TelemetryLogging) { } 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Severity.Enum.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | enum 71180276 SeveritySESTM 4 | { 5 | Access = Internal; 6 | Extensible = false; 7 | 8 | value(0; " ") 9 | { 10 | Caption = ' ', Locked = true; 11 | } 12 | value(1; Info) 13 | { 14 | Caption = 'Info'; 15 | } 16 | value(2; Warning) 17 | { 18 | Caption = 'Warning'; 19 | } 20 | value(3; Error) 21 | { 22 | Caption = 'Error'; 23 | } 24 | value(4; Critical) 25 | { 26 | Caption = 'Critical'; 27 | } 28 | value(5; Disabled) 29 | { 30 | Caption = 'Disabled'; 31 | } 32 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/SentinelRuleSet.Page.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | page 71180277 SentinelRuleSetSESTM 4 | { 5 | ApplicationArea = All; 6 | Caption = 'Sentinel Rule Set'; 7 | Extensible = false; 8 | PageType = List; 9 | SourceTable = SentinelRuleSetSESTM; 10 | UsageCategory = Administration; 11 | 12 | layout 13 | { 14 | area(Content) 15 | { 16 | repeater(Main) 17 | { 18 | field(AlertCode; Rec.AlertCode) { } 19 | field(Severity; Rec.Severity) { } 20 | field(TelemetryLogging; Rec.TelemetryLogging) { } 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/IgnoredAlerts.Table.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | table 71180276 IgnoredAlertsSESTM 4 | { 5 | Access = Internal; 6 | Caption = 'Ignored Alerts'; 7 | DataClassification = SystemMetadata; 8 | Extensible = false; 9 | 10 | fields 11 | { 12 | field(1; AlertCode; Enum AlertCodeSESTM) 13 | { 14 | Caption = 'Alert Code'; 15 | } 16 | field(2; UniqueIdentifier; Text[100]) 17 | { 18 | Caption = 'Unique Identifier'; 19 | } 20 | } 21 | 22 | keys 23 | { 24 | key(Key1; AlertCode, UniqueIdentifier) 25 | { 26 | Clustered = true; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "StMaDev", 6 | "type": "al", 7 | "request": "launch", 8 | "environmentType": "Sandbox", 9 | "environmentName": "StMaDev", 10 | "tenant": "14f62f77-ea18-443b-b103-09fd5deacbee", 11 | "breakOnError": "All", 12 | "breakOnRecordWrite": "None", 13 | "startupObjectId": 71180275, 14 | "launchBrowser": true, 15 | "enableSqlInformationDebugger": true, 16 | "enableLongRunningSqlStatements": true, 17 | "longRunningSqlStatementsThreshold": 500, 18 | "numberOfSqlStatements": 10, 19 | "schemaUpdateMode": "ForceSync" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /al.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "BusinessCentral.Sentinel" 5 | }, 6 | { 7 | "path": ".AL-Go" 8 | }, 9 | { 10 | "path": "." 11 | } 12 | ], 13 | "settings": { 14 | "CRS.ObjectNameSuffix": "SESTM", 15 | "CRS.RemoveSuffixFromFilename": true, 16 | "al-test-runner.enableCodeCoverage": true, 17 | "al.codeAnalyzers": [ 18 | "${CodeCop}", 19 | "${UICop}", 20 | "${PerTenantExtensionCop}", 21 | "${AppSourceCop}", 22 | "${analyzerFolder}BusinessCentral.LinterCop.dll" 23 | ], 24 | "al.enableCodeAnalysis": true, 25 | "al.backgroundCodeAnalysis": "Project", 26 | "al.enableExternalRulesets": true, 27 | "al.ruleSetPath": "../custom.ruleset.json", 28 | "cSpell.words": [ 29 | "Ceridian", 30 | "Contoso", 31 | "SESTM", 32 | "Shpfy" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Alert.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | codeunit 71180275 AlertSESTM implements IAuditAlertSESTM 4 | { 5 | Access = Internal; 6 | 7 | procedure RunAlert() 8 | begin 9 | 10 | end; 11 | 12 | procedure CreateAlerts() 13 | begin 14 | 15 | end; 16 | 17 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 18 | begin 19 | 20 | end; 21 | 22 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 23 | begin 24 | 25 | end; 26 | 27 | procedure AutoFix(var Alert: Record AlertSESTM) 28 | begin 29 | 30 | end; 31 | 32 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 33 | begin 34 | 35 | end; 36 | 37 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 38 | begin 39 | 40 | end; 41 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Telemetry/TelemetryHelper.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using System.Environment; 4 | using System.Telemetry; 5 | 6 | codeunit 71180284 "TelemetryHelperSESTM" 7 | { 8 | Access = Internal; 9 | 10 | procedure LogUsage(Alert: Enum AlertCodeSESTM; Description: Text; CustomDimensions: Dictionary of [Text, Text]) 11 | var 12 | FeatureTelemetry: Codeunit "Feature Telemetry"; 13 | begin 14 | if not this.IsSaaS() then 15 | exit; 16 | 17 | FeatureTelemetry.LogUsage( 18 | Alert.Names().Get(Alert.AsInteger()), 19 | Format(Alert), 20 | Description, 21 | CustomDimensions 22 | ); 23 | end; 24 | 25 | local procedure IsSaaS(): Boolean 26 | var 27 | EnvironmentInformation: Codeunit "Environment Information"; 28 | begin 29 | exit(EnvironmentInformation.IsSaaS()); 30 | end; 31 | } -------------------------------------------------------------------------------- /.github/workflows/Troubleshooting.yaml: -------------------------------------------------------------------------------- 1 | name: 'Troubleshooting' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | displayNameOfSecrets: 7 | description: Display the name (not the value) of secrets available to the repository 8 | type: boolean 9 | default: false 10 | 11 | permissions: 12 | actions: read 13 | contents: read 14 | 15 | defaults: 16 | run: 17 | shell: pwsh 18 | 19 | env: 20 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 21 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 22 | 23 | jobs: 24 | Troubleshooting: 25 | runs-on: [ ubuntu-latest ] 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | with: 30 | lfs: true 31 | 32 | - name: Troubleshooting 33 | uses: microsoft/AL-Go-Actions/Troubleshooting@v7.0 34 | with: 35 | shell: pwsh 36 | gitHubSecrets: ${{ toJson(secrets) }} 37 | displayNameOfSecrets: ${{ github.event.inputs.displayNameOfSecrets }} 38 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/SentinelRuleSet.Table.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | table 71180277 SentinelRuleSetSESTM 4 | { 5 | Access = Internal; 6 | Caption = 'Sentinel Rule Set'; 7 | DataClassification = CustomerContent; 8 | DrillDownPageId = SentinelRuleSetSESTM; 9 | LookupPageId = SentinelRuleSetSESTM; 10 | 11 | fields 12 | { 13 | field(1; AlertCode; Enum "AlertCodeSESTM") 14 | { 15 | Caption = 'Code'; 16 | NotBlank = true; 17 | ToolTip = 'The code representing the type of alert.'; 18 | } 19 | field(3; Severity; Enum SeveritySESTM) 20 | { 21 | Caption = 'Severity'; 22 | ToolTip = 'The severity level of the alert.'; 23 | } 24 | field(4; TelemetryLogging; Enum TelemetryLogging) 25 | { 26 | Caption = 'Telemetry Logging'; 27 | ToolTip = 'Specifies how sentinel emits telemetry data.'; 28 | } 29 | } 30 | 31 | keys 32 | { 33 | key(Key1; AlertCode) 34 | { 35 | Clustered = true; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stefan Maroń 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/SentinelAdmin.PermissionSet.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | 5 | permissionset 71180275 SentinelAdminSESTM 6 | { 7 | Access = Internal; 8 | Assignable = true; 9 | Caption = 'Sentinel Admin', MaxLength = 30; 10 | Permissions = table AlertSESTM = X, 11 | tabledata AlertSESTM = RIMD, 12 | table IgnoredAlertsSESTM = X, 13 | tabledata IgnoredAlertsSESTM = RIMD, 14 | table SentinelRuleSetSESTM = X, 15 | tabledata SentinelRuleSetSESTM = RIMD, 16 | table SentinelSetup = X, 17 | tabledata SentinelSetup = RIMD, 18 | codeunit AlertDevScopeExtSESTM = X, 19 | codeunit AlertPteDownloadCodeSESTM = X, 20 | codeunit AlertSESTM = X, 21 | codeunit AnalysisNotScheduledSESTM = X, 22 | codeunit DemoDataExtInProdSESTM = X, 23 | codeunit EvaluationCompanyInProdSESTM = X, 24 | codeunit NonPostNoSeriesGapsSESTM = X, 25 | codeunit ReRunAllAlerts = X, 26 | codeunit SentinelTelemetryLoggerSESTM = X, 27 | codeunit TelemetryHelperSESTM = X, 28 | codeunit UnusedExtensionInstalledSESTM = X, 29 | codeunit UserWithSuperSESTM = X, 30 | page AlertCard = X, 31 | page AlertListSESTM = X, 32 | page SentinelRuleSetSESTM = X, 33 | page SentinelSetup = X; 34 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Telemetry/SentinelTelemetryLogger.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | using System.Telemetry; 5 | 6 | codeunit 71180285 "SentinelTelemetryLoggerSESTM" implements "Telemetry Logger" 7 | { 8 | Access = Internal; 9 | Permissions = 10 | tabledata AlertSESTM = R; 11 | 12 | procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text]) 13 | begin 14 | Session.LogMessage(EventId, Message, Verbosity, DataClassification, TelemetryScope::All, CustomDimensions); 15 | end; 16 | 17 | // For the functionality to behave as expected, there must be exactly one implementation of the "Telemetry Logger" interface registered per app publisher 18 | [EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Loggers", 'OnRegisterTelemetryLogger', '', true, true)] 19 | local procedure OnRegisterTelemetryLogger(var Sender: Codeunit "Telemetry Loggers") 20 | var 21 | SentinelTelemetryLogger: Codeunit SentinelTelemetryLoggerSESTM; 22 | begin 23 | Sender.Register(SentinelTelemetryLogger); 24 | end; 25 | 26 | [EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Management", OnSendDailyTelemetry, '', false, false)] 27 | local procedure LogSentinelTelemetry_OnSendDailyTelemetry() 28 | var 29 | Alert: Record AlertSESTM; 30 | Setup: Record SentinelSetup; 31 | begin 32 | if Alert.FindSet() then 33 | repeat 34 | if Setup.GetTelemetryLoggingSetting(Alert.AlertCode) = Setup.TelemetryLogging::Daily then 35 | Alert.LogUsage(); 36 | until Alert.Next() = 0; 37 | end; 38 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Setup/SentinelSetup.Table.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | table 71180278 SentinelSetup 4 | { 5 | Access = Internal; 6 | Caption = 'Sentinel Setup'; 7 | DataClassification = SystemMetadata; 8 | InherentPermissions = R; 9 | Permissions = 10 | tabledata SentinelRuleSetSESTM = R, 11 | tabledata SentinelSetup = RI; 12 | 13 | fields 14 | { 15 | field(1; PrimaryKey; Code[10]) 16 | { 17 | Caption = 'Primary Key'; 18 | NotBlank = false; 19 | } 20 | field(2; TelemetryLogging; Enum TelemetryLogging) 21 | { 22 | Caption = 'Telemetry Logging'; 23 | InitValue = Daily; 24 | ToolTip = 'Specifies how sentinel emits telemetry data.'; 25 | ValuesAllowed = Daily, OnRuleLogging, Off; 26 | } 27 | } 28 | 29 | keys 30 | { 31 | key(Key1; PrimaryKey) 32 | { 33 | Clustered = true; 34 | } 35 | } 36 | 37 | internal procedure SaveGet() 38 | begin 39 | if not Rec.Get() then begin 40 | Rec.Init(); 41 | Rec.Insert(true); 42 | end; 43 | end; 44 | 45 | internal procedure GetTelemetryLoggingSetting(AlertCode: Enum AlertCodeSESTM): Enum TelemetryLogging 46 | var 47 | SentinelRuleSet: Record SentinelRuleSetSESTM; 48 | begin 49 | SentinelRuleSet.ReadIsolation(IsolationLevel::ReadCommitted); 50 | SentinelRuleSet.SetLoadFields(TelemetryLogging); 51 | if SentinelRuleSet.Get(AlertCode) and (SentinelRuleSet.TelemetryLogging <> SentinelRuleSet.TelemetryLogging::" ") then 52 | exit(SentinelRuleSet.TelemetryLogging); 53 | 54 | Rec.SetLoadFields(TelemetryLogging); 55 | Rec.ReadIsolation(IsolationLevel::ReadCommitted); 56 | Rec.SaveGet(); 57 | exit(Rec.TelemetryLogging); 58 | end; 59 | 60 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/IAuditAlert.Interface.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | interface IAuditAlertSESTM 4 | { 5 | Access = Internal; 6 | 7 | /// 8 | /// This procedure should be used to create alerts. It should determine any alerts for a rule and create the records in the alert table. 9 | /// Use `Alert.New()` to create a new alert. 10 | /// 11 | procedure CreateAlerts() 12 | 13 | /// 14 | /// This should a very detailed description of the alert. It can also use `Hyperlink` to open a documentation page in a browser. 15 | /// 16 | /// 17 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 18 | 19 | /// 20 | /// This procedure should offer the user to open a page within BC where the user can get more information about the alert 21 | /// or where the user can take actions to resolve the alert. 22 | /// 23 | /// 24 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 25 | 26 | /// 27 | /// AutoFix is a procedure that will be called when the user wants to fix the alert automatically. 28 | /// It should just fix the issue described in the alert. 29 | /// Before executing the autofix, the procedure should inform the user about the changes that will be made and ask for confirmation. 30 | /// If no autofix is available, the procedure should inform the user about it. 31 | /// 32 | /// 33 | procedure AutoFix(var Alert: Record AlertSESTM) 34 | 35 | /// 36 | /// Add custom telemetry dimensions to the alert. Severity, Area and Ignore are already added by default. 37 | /// 38 | /// 39 | /// 40 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 41 | 42 | /// 43 | /// Get the telemetry description for the alert. 44 | /// 45 | /// 46 | /// 47 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 48 | 49 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1aba0d21-e0f6-45c2-8d46-b7a4f155d66a", 3 | "name": "BusinessCentral.Sentinel", 4 | "publisher": "Stefan Maron Consulting", 5 | "version": "1.9.0.0", 6 | "brief": "BusinessCentral.Sentinel is an intelligent monitoring and advisory tool designed for Microsoft Dynamics 365 Business Central users. It provides real-time insights, proactive alerts, and actionable recommendations to enhance operational efficiency, reduce risks, and optimize decision-making. This app acts as a vigilant 'sentinel' for your business, ensuring key processes run smoothly and critical issues are addressed promptly. From financial anomalies to inventory mismatches, BusinessCentral.Sentinel empowers users with the oversight they need to stay ahead in a dynamic business environment.", 7 | "description": "BusinessCentral.Sentinel is a robust application designed to monitor, analyze, and guide business operations within Microsoft Dynamics 365 Business Central. It provides real-time alerts, actionable recommendations, and customizable rules to ensure you are always informed about critical business aspects such as overdue payments, low stock levels, or unbalanced financial entries. The tool features intuitive visual dashboards displaying key performance indicators and insights at a glance. Built specifically for Dynamics 365 Business Central, it integrates seamlessly into your workflows, offering managers, finance teams, and operational staff reliable oversight and decision-making support. BusinessCentral.Sentinel empowers businesses to proactively manage risks, optimize efficiency, and make informed decisions faster.", 8 | "contextSensitiveHelpUrl": "https://github.com/StefanMaron/BusinessCentral.Sentinel/issues", 9 | "privacyStatement": "https://stefanmaron.com/privacystatement/", 10 | "EULA": "https://stefanmaron.com/eula/", 11 | "help": "https://github.com/StefanMaron/BusinessCentral.Sentinel/issues", 12 | "url": "https://github.com/StefanMaron/BusinessCentral.Sentinel", 13 | "logo": "logo.png", 14 | "dependencies": [], 15 | "screenshots": [], 16 | "platform": "1.0.0.0", 17 | "application": "25.0.0.0", 18 | "idRanges": [ 19 | { 20 | "from": 71180275, 21 | "to": 71180574 22 | } 23 | ], 24 | "features": [ 25 | "NoImplicitWith", 26 | "TranslationFile" 27 | ], 28 | "runtime": "14.0", 29 | "resourceExposurePolicy": { 30 | "allowDebugging": true, 31 | "allowDownloadingSource": true, 32 | "includeSourceInSymbolFile": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/AlertCode.Enum.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | enum 71180275 "AlertCodeSESTM" implements IAuditAlertSESTM 4 | { 5 | Access = Internal; 6 | DefaultImplementation = IAuditAlertSESTM = AlertSESTM; 7 | Extensible = false; 8 | UnknownValueImplementation = IAuditAlertSESTM = AlertSESTM; 9 | 10 | value(0; " ") 11 | { 12 | Caption = ' ', Locked = true; 13 | } 14 | /// 15 | /// Warns if Per Tenant Extension do not allow Download Code 16 | /// 17 | value(1; "SE-000001") 18 | { 19 | Caption = 'SE-000001'; 20 | Implementation = IAuditAlertSESTM = AlertPteDownloadCodeSESTM; 21 | } 22 | /// 23 | /// Warns if Extension in DEV Scope are installed 24 | /// 25 | value(2; "SE-000002") 26 | { 27 | Caption = 'SE-000002'; 28 | Implementation = IAuditAlertSESTM = AlertDevScopeExtSESTM; 29 | } 30 | /// 31 | /// Evaluation Company detected 32 | /// 33 | value(3; "SE-000003") 34 | { 35 | Caption = 'SE-000003'; 36 | Implementation = IAuditAlertSESTM = EvaluationCompanyInProdSESTM; 37 | } 38 | /// 39 | /// Demo Data Extensions should get uninstalled 40 | /// 41 | value(4; "SE-000004") 42 | { 43 | Caption = 'SE-000004'; 44 | Implementation = IAuditAlertSESTM = DemoDataExtInProdSESTM; 45 | } 46 | /// 47 | /// Inform about users with Super permissions 48 | /// 49 | value(5; "SE-000005") 50 | { 51 | Caption = 'SE-000005'; 52 | Implementation = IAuditAlertSESTM = UserWithSuperSESTM; 53 | } 54 | /// 55 | /// Consider configuring non-posting number series to allow gaps to increase performance 56 | /// 57 | value(6; "SE-000006") 58 | { 59 | Caption = 'SE-000006'; 60 | Implementation = IAuditAlertSESTM = NonPostNoSeriesGapsSESTM; 61 | } 62 | /// 63 | /// Warns if and Extension is installed and but unused 64 | /// 65 | value(7; "SE-000007") 66 | { 67 | Caption = 'SE-000007'; 68 | Implementation = IAuditAlertSESTM = UnusedExtensionInstalledSESTM; 69 | } 70 | /// 71 | /// Informs that the Alert analysis is not scheduled 72 | /// 73 | value(8; "SE-000008") 74 | { 75 | Caption = 'SE-000008'; 76 | Implementation = IAuditAlertSESTM = AnalysisNotScheduledSESTM; 77 | } 78 | } -------------------------------------------------------------------------------- /.github/workflows/DeployReferenceDocumentation.yaml: -------------------------------------------------------------------------------- 1 | name: ' Deploy Reference Documentation' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | id-token: write 10 | pages: write 11 | 12 | defaults: 13 | run: 14 | shell: pwsh 15 | 16 | env: 17 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 18 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 19 | 20 | jobs: 21 | DeployALDoc: 22 | runs-on: [ ubuntu-latest ] 23 | name: Deploy Reference Documentation 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 30 | 31 | - name: Initialize the workflow 32 | id: init 33 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 34 | with: 35 | shell: pwsh 36 | 37 | - name: Read settings 38 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 39 | with: 40 | shell: pwsh 41 | 42 | - name: Determine Deployment Environments 43 | id: DetermineDeploymentEnvironments 44 | uses: microsoft/AL-Go-Actions/DetermineDeploymentEnvironments@v7.0 45 | env: 46 | GITHUB_TOKEN: ${{ github.token }} 47 | with: 48 | shell: pwsh 49 | getEnvironments: 'github-pages' 50 | type: 'Publish' 51 | 52 | - name: Setup Pages 53 | if: steps.DetermineDeploymentEnvironments.outputs.deployALDocArtifact == 1 54 | uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 55 | 56 | - name: Build Reference Documentation 57 | uses: microsoft/AL-Go-Actions/BuildReferenceDocumentation@v7.0 58 | with: 59 | shell: pwsh 60 | artifacts: 'latest' 61 | 62 | - name: Upload pages artifact 63 | uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 64 | with: 65 | path: ".aldoc/_site/" 66 | 67 | - name: Deploy to GitHub Pages 68 | if: steps.DetermineDeploymentEnvironments.outputs.deployALDocArtifact == 1 69 | id: deployment 70 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 71 | 72 | - name: Finalize the workflow 73 | if: always() 74 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 75 | env: 76 | GITHUB_TOKEN: ${{ github.token }} 77 | with: 78 | shell: pwsh 79 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 80 | currentJobContext: ${{ toJson(job) }} 81 | -------------------------------------------------------------------------------- /.github/workflows/AddExistingAppOrTestApp.yaml: -------------------------------------------------------------------------------- 1 | name: 'Add existing app or test app' 2 | 3 | run-name: "Add existing app or test app in [${{ github.ref_name }}]" 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | project: 9 | description: Project name if the repository is setup for multiple projects 10 | required: false 11 | default: '.' 12 | url: 13 | description: Direct Download Url of .app or .zip file 14 | required: true 15 | directCommit: 16 | description: Direct Commit? 17 | type: boolean 18 | default: false 19 | useGhTokenWorkflow: 20 | description: Use GhTokenWorkflow for PR/Commit? 21 | type: boolean 22 | default: false 23 | 24 | permissions: 25 | actions: read 26 | contents: write 27 | id-token: write 28 | pull-requests: write 29 | 30 | defaults: 31 | run: 32 | shell: pwsh 33 | 34 | env: 35 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 36 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 37 | 38 | jobs: 39 | AddExistingAppOrTestApp: 40 | needs: [ ] 41 | runs-on: [ ubuntu-latest ] 42 | steps: 43 | - name: Dump Workflow Information 44 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 45 | with: 46 | shell: pwsh 47 | 48 | - name: Checkout 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 50 | 51 | - name: Initialize the workflow 52 | id: init 53 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 54 | with: 55 | shell: pwsh 56 | 57 | - name: Read settings 58 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 59 | with: 60 | shell: pwsh 61 | 62 | - name: Read secrets 63 | id: ReadSecrets 64 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 65 | with: 66 | shell: pwsh 67 | gitHubSecrets: ${{ toJson(secrets) }} 68 | getSecrets: 'TokenForPush' 69 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 70 | 71 | - name: Add existing app 72 | uses: microsoft/AL-Go-Actions/AddExistingApp@v7.0 73 | with: 74 | shell: pwsh 75 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 76 | project: ${{ github.event.inputs.project }} 77 | url: ${{ github.event.inputs.url }} 78 | directCommit: ${{ github.event.inputs.directCommit }} 79 | 80 | - name: Finalize the workflow 81 | if: always() 82 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 83 | env: 84 | GITHUB_TOKEN: ${{ github.token }} 85 | with: 86 | shell: pwsh 87 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 88 | currentJobContext: ${{ toJson(job) }} 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BusinessCentral.Sentinel 2 | 3 | BusinessCentral.Sentinel is an intelligent monitoring and advisory tool designed for Microsoft Dynamics 365 Business Central users. It focuses on the analysis of technical configuration and may include functional configuration in the future. This app provides real-time insights, proactive alerts, and actionable recommendations to enhance operational efficiency, reduce risks, and optimize decision-making. BusinessCentral.Sentinel acts as a vigilant 'sentinel' for your business, ensuring key processes run smoothly and critical issues are addressed promptly. 4 | 5 | Get it on AppSource: 6 | 7 | [BusinessCentral.Sentinel](https://appsource.microsoft.com/en-us/product/dynamics-365-business-central/PUBID.stefanmaronconsulting1646304351282%7CAID.sentinel%7CPAPPID.1aba0d21-e0f6-45c2-8d46-b7a4f155d66a?tab=Overview) 8 | 9 | ## Features 10 | 11 | - **Real-Time Alerts**: Get notified instantly about critical business events. 12 | - **Actionable Recommendations**: Receive suggestions to improve business processes. 13 | - **Customizable Rules**: Tailor the monitoring rules to fit your specific needs. 14 | - **Intuitive Dashboards**: Visualize key performance indicators and insights at a glance. 15 | - **Seamless Integration**: Built specifically for Dynamics 365 Business Central, ensuring smooth workflow integration. 16 | 17 | ## Rules 18 | 19 | | ID | Short Description | Area | Severity | 20 | |----------|-----------------------------------------------------------------------------------|------------|----------| 21 | | SE-000001| Warns if Per Tenant Extension do not allow Download Code| Technical| Warning| 22 | | SE-000002| Warns if Extension in DEV Scope are installed | Technical| Warning| 23 | | SE-000003| Evaluation Company detected in Production | Technical | Info | 24 | | SE-000004| Demo Data Extensions should get uninstalled from production | Technical | Info | 25 | | SE-000005| Inform about users with Super permissions | Permissions| Info | 26 | | SE-000006| Consider configuring non-posting number series to allow gaps to increase performance | Performance| Warning| 27 | | SE-000007| Extension installed but unused.| Performance| Warning| 28 | | SE-000008| Scheduled Analysis is not configured | Technical| Info| 29 | 30 | ## Documentation 31 | 32 | For detailed documentation, visit our [GitHub Wiki](https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki). 33 | 34 | ## Open Source 35 | 36 | This project is open source and we welcome contributions from the community. Feel free to fork the repository, make improvements, and submit pull requests. Your contributions help make this project better for everyone. 37 | 38 | ## Contributing 39 | 40 | We welcome contributions to BusinessCentral.Sentinel! 41 | 42 | ## License 43 | 44 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 45 | 46 | ## Support 47 | 48 | If you encounter any issues or have questions, please open an issue on our [GitHub Issues](https://github.com/StefanMaron/BusinessCentral.Sentinel/issues) page. 49 | 50 | ## Acknowledgements 51 | 52 | This project is based on the AL-Go AppSource App Template. Learn more about AL-Go [here](https://aka.ms/AL-Go). 53 | 54 | [![Use this template](https://github.com/microsoft/AL-Go/assets/10775043/ca1ecc85-2fd3-4ab5-a866-bd2e7e80259d)](https://github.com/new?template_name=AL-Go-AppSource&template_owner=microsoft) 55 | -------------------------------------------------------------------------------- /.github/workflows/IncrementVersionNumber.yaml: -------------------------------------------------------------------------------- 1 | name: ' Increment Version Number' 2 | 3 | run-name: "Increment Version Number in [${{ github.ref_name }}]" 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | projects: 9 | description: Comma-separated list of project name patterns if the repository is setup for multiple projects (default is * for all projects) 10 | required: false 11 | default: '*' 12 | versionNumber: 13 | description: New Version Number in main branch. Use Major.Minor (optionally add .Build for versioningstrategy 3) for absolute change, or +1, +0.1 (or +0.0.1 for versioningstrategy 3) incremental change. 14 | required: false 15 | default: '' 16 | skipUpdatingDependencies: 17 | description: Skip updating dependency version numbers in all apps. 18 | type: boolean 19 | default: false 20 | directCommit: 21 | description: Direct Commit? 22 | type: boolean 23 | default: false 24 | useGhTokenWorkflow: 25 | description: Use GhTokenWorkflow for PR/Commit? 26 | type: boolean 27 | default: false 28 | 29 | defaults: 30 | run: 31 | shell: pwsh 32 | 33 | env: 34 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 35 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 36 | 37 | jobs: 38 | IncrementVersionNumber: 39 | needs: [ ] 40 | runs-on: [ ubuntu-latest ] 41 | permissions: 42 | actions: read 43 | contents: write 44 | id-token: write 45 | pull-requests: write 46 | steps: 47 | - name: Dump Workflow Information 48 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 49 | with: 50 | shell: pwsh 51 | 52 | - name: Checkout 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 54 | 55 | - name: Initialize the workflow 56 | id: init 57 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 58 | with: 59 | shell: pwsh 60 | 61 | - name: Read settings 62 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 63 | with: 64 | shell: pwsh 65 | 66 | - name: Read secrets 67 | id: ReadSecrets 68 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 69 | with: 70 | shell: pwsh 71 | gitHubSecrets: ${{ toJson(secrets) }} 72 | getSecrets: 'TokenForPush' 73 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 74 | 75 | - name: Increment Version Number 76 | uses: microsoft/AL-Go-Actions/IncrementVersionNumber@v7.0 77 | with: 78 | shell: pwsh 79 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 80 | projects: ${{ github.event.inputs.projects }} 81 | versionNumber: ${{ github.event.inputs.versionNumber }} 82 | skipUpdatingDependencies: ${{ github.event.inputs.skipUpdatingDependencies }} 83 | directCommit: ${{ github.event.inputs.directCommit }} 84 | 85 | - name: Finalize the workflow 86 | if: always() 87 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 88 | env: 89 | GITHUB_TOKEN: ${{ github.token }} 90 | with: 91 | shell: pwsh 92 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 93 | currentJobContext: ${{ toJson(job) }} 94 | -------------------------------------------------------------------------------- /.github/workflows/CreateApp.yaml: -------------------------------------------------------------------------------- 1 | name: 'Create a new app' 2 | 3 | run-name: "Create a new app in [${{ github.ref_name }}]" 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | project: 9 | description: Project name if the repository is setup for multiple projects 10 | required: false 11 | default: '.' 12 | name: 13 | description: Name 14 | required: true 15 | publisher: 16 | description: Publisher 17 | required: true 18 | idrange: 19 | description: ID range (from..to) 20 | required: true 21 | sampleCode: 22 | description: Include Sample code? 23 | type: boolean 24 | default: true 25 | directCommit: 26 | description: Direct Commit? 27 | type: boolean 28 | default: false 29 | useGhTokenWorkflow: 30 | description: Use GhTokenWorkflow for PR/Commit? 31 | type: boolean 32 | default: false 33 | 34 | permissions: 35 | actions: read 36 | contents: write 37 | id-token: write 38 | pull-requests: write 39 | 40 | defaults: 41 | run: 42 | shell: pwsh 43 | 44 | env: 45 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 46 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 47 | 48 | jobs: 49 | CreateApp: 50 | needs: [ ] 51 | runs-on: [ ubuntu-latest ] 52 | steps: 53 | - name: Dump Workflow Information 54 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 55 | with: 56 | shell: pwsh 57 | 58 | - name: Checkout 59 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 60 | 61 | - name: Initialize the workflow 62 | id: init 63 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 64 | with: 65 | shell: pwsh 66 | 67 | - name: Read settings 68 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 69 | with: 70 | shell: pwsh 71 | get: type 72 | 73 | - name: Read secrets 74 | id: ReadSecrets 75 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 76 | with: 77 | shell: pwsh 78 | gitHubSecrets: ${{ toJson(secrets) }} 79 | getSecrets: 'TokenForPush' 80 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 81 | 82 | - name: Creating a new app 83 | uses: microsoft/AL-Go-Actions/CreateApp@v7.0 84 | with: 85 | shell: pwsh 86 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 87 | project: ${{ github.event.inputs.project }} 88 | type: ${{ env.type }} 89 | name: ${{ github.event.inputs.name }} 90 | publisher: ${{ github.event.inputs.publisher }} 91 | idrange: ${{ github.event.inputs.idrange }} 92 | sampleCode: ${{ github.event.inputs.sampleCode }} 93 | directCommit: ${{ github.event.inputs.directCommit }} 94 | 95 | - name: Finalize the workflow 96 | if: always() 97 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 98 | env: 99 | GITHUB_TOKEN: ${{ github.token }} 100 | with: 101 | shell: pwsh 102 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 103 | currentJobContext: ${{ toJson(job) }} 104 | -------------------------------------------------------------------------------- /.github/workflows/CreateTestApp.yaml: -------------------------------------------------------------------------------- 1 | name: 'Create a new test app' 2 | 3 | run-name: "Create a new test app in [${{ github.ref_name }}]" 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | project: 9 | description: Project name if the repository is setup for multiple projects 10 | required: false 11 | default: '.' 12 | name: 13 | description: Name 14 | required: true 15 | default: '.Test' 16 | publisher: 17 | description: Publisher 18 | required: true 19 | idrange: 20 | description: ID range 21 | required: true 22 | default: '50000..99999' 23 | sampleCode: 24 | description: Include Sample code? 25 | type: boolean 26 | default: true 27 | directCommit: 28 | description: Direct Commit? 29 | type: boolean 30 | default: false 31 | useGhTokenWorkflow: 32 | description: Use GhTokenWorkflow for PR/Commit? 33 | type: boolean 34 | default: false 35 | 36 | permissions: 37 | actions: read 38 | contents: write 39 | id-token: write 40 | pull-requests: write 41 | 42 | defaults: 43 | run: 44 | shell: pwsh 45 | 46 | env: 47 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 48 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 49 | 50 | jobs: 51 | CreateTestApp: 52 | needs: [ ] 53 | runs-on: [ ubuntu-latest ] 54 | steps: 55 | - name: Dump Workflow Information 56 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 57 | with: 58 | shell: pwsh 59 | 60 | - name: Checkout 61 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 62 | 63 | - name: Initialize the workflow 64 | id: init 65 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 66 | with: 67 | shell: pwsh 68 | 69 | - name: Read settings 70 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 71 | with: 72 | shell: pwsh 73 | 74 | - name: Read secrets 75 | id: ReadSecrets 76 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 77 | with: 78 | shell: pwsh 79 | gitHubSecrets: ${{ toJson(secrets) }} 80 | getSecrets: 'TokenForPush' 81 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 82 | 83 | - name: Creating a new test app 84 | uses: microsoft/AL-Go-Actions/CreateApp@v7.0 85 | with: 86 | shell: pwsh 87 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 88 | project: ${{ github.event.inputs.project }} 89 | type: 'Test App' 90 | name: ${{ github.event.inputs.name }} 91 | publisher: ${{ github.event.inputs.publisher }} 92 | idrange: ${{ github.event.inputs.idrange }} 93 | sampleCode: ${{ github.event.inputs.sampleCode }} 94 | directCommit: ${{ github.event.inputs.directCommit }} 95 | 96 | - name: Finalize the workflow 97 | if: always() 98 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 99 | env: 100 | GITHUB_TOKEN: ${{ github.token }} 101 | with: 102 | shell: pwsh 103 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 104 | currentJobContext: ${{ toJson(job) }} 105 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/EvaluationCompanyInProd.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using Microsoft.Foundation.Company; 4 | using STM.BusinessCentral.Sentinel; 5 | using System.Environment; 6 | using System.Environment.Configuration; 7 | 8 | codeunit 71180278 EvaluationCompanyInProdSESTM implements IAuditAlertSESTM 9 | { 10 | Access = Internal; 11 | Permissions = 12 | tabledata AlertSESTM = RI, 13 | tabledata Company = R; 14 | 15 | procedure CreateAlerts() 16 | var 17 | Alert: Record AlertSESTM; 18 | Company: Record Company; 19 | EnvironmentInformation: Codeunit "Environment Information"; 20 | CallToActionLbl: Label 'Delete the Company called %1', Comment = '%1 = Company Name'; 21 | LongDescLbl: Label 'An evaluation company has been detected in the environment. If you do not need it anymore, you should consider deleting it. It takes up additional space and it is copied to sandboxes if you copy from production.'; 22 | ShortDescLbl: Label 'Evaluation Company detected: %1', Comment = '%1 = Company Name'; 23 | begin 24 | Company.SetRange("Evaluation Company", true); 25 | Company.ReadIsolation(IsolationLevel::ReadUncommitted); 26 | Company.SetLoadFields("Name", SystemId); 27 | if Company.FindSet() then 28 | repeat 29 | Alert.New( 30 | "AlertCodeSESTM"::"SE-000003", 31 | StrSubstNo(ShortDescLbl, Company.Name), 32 | EnvironmentInformation.IsProduction() ? SeveritySESTM::Warning : SeveritySESTM::Info, 33 | AreaSESTM::Technical, 34 | LongDescLbl, 35 | StrSubstNo(CallToActionLbl, Company.Name), 36 | Company.SystemId 37 | ); 38 | until Company.Next() = 0; 39 | 40 | 41 | end; 42 | 43 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 44 | var 45 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000003', Locked = true; 46 | begin 47 | Hyperlink(WikiLinkTok); 48 | end; 49 | 50 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 51 | var 52 | Company: Record Company; 53 | OpenPageQst: Label 'Do you want to open the page to manage companies?'; 54 | begin 55 | if Confirm(OpenPageQst) then begin 56 | Company.SetRange(SystemId, Alert.UniqueIdentifier); 57 | Page.Run(Page::Companies, Company) 58 | end; 59 | end; 60 | 61 | procedure AutoFix(var Alert: Record AlertSESTM) 62 | var 63 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000003)'; 64 | begin 65 | // The base BC logic that happens on delete company is on the page, and can not be reused 66 | // Therefore, the user has to use the ShowRelatedInformation to open the page and delete from there. 67 | Message(NoAutofixAvailableLbl); 68 | end; 69 | 70 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 71 | begin 72 | CustomDimensions.Add('AlertCompanyName', Alert.ShortDescription); 73 | CustomDimensions.Add('AlertCompanySystemId', Alert.UniqueIdentifier); 74 | end; 75 | 76 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 77 | begin 78 | exit(Alert.LongDescription); 79 | end; 80 | } -------------------------------------------------------------------------------- /.github/workflows/PublishToAppSource.yaml: -------------------------------------------------------------------------------- 1 | name: ' Publish To AppSource' 2 | run-name: 'Publish To AppSource - Version ${{ inputs.appVersion }}, Projects ${{ inputs.projects }}' 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | appVersion: 8 | description: App version to deliver to AppSource (current, prerelease, draft, latest or version number) 9 | required: false 10 | default: 'current' 11 | projects: 12 | description: Projects to publish to AppSource if the repository is multi-project. Default is *, which will publish all projects to AppSource. 13 | required: false 14 | default: '*' 15 | GoLive: 16 | description: Promote AppSource App to go live if it passes technical validation? 17 | type: boolean 18 | default: false 19 | 20 | permissions: 21 | actions: read 22 | contents: read 23 | id-token: write 24 | 25 | defaults: 26 | run: 27 | shell: pwsh 28 | 29 | env: 30 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 31 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 32 | 33 | jobs: 34 | Initialization: 35 | needs: [ ] 36 | runs-on: [ ubuntu-latest ] 37 | outputs: 38 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 39 | steps: 40 | - name: Dump Workflow Information 41 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 42 | with: 43 | shell: pwsh 44 | 45 | - name: Checkout 46 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 47 | 48 | - name: Initialize the workflow 49 | id: init 50 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 51 | with: 52 | shell: pwsh 53 | 54 | Deliver: 55 | needs: [ Initialization ] 56 | runs-on: [ ubuntu-latest ] 57 | name: Deliver to AppSource 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 61 | 62 | - name: Read settings 63 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 64 | with: 65 | shell: pwsh 66 | 67 | - name: Read secrets 68 | id: ReadSecrets 69 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 70 | with: 71 | shell: pwsh 72 | gitHubSecrets: ${{ toJson(secrets) }} 73 | getSecrets: 'appSourceContext' 74 | 75 | - name: Deliver 76 | uses: microsoft/AL-Go-Actions/Deliver@v7.0 77 | env: 78 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 79 | with: 80 | shell: pwsh 81 | type: 'Release' 82 | projects: ${{ github.event.inputs.projects }} 83 | deliveryTarget: 'AppSource' 84 | artifacts: ${{ github.event.inputs.appVersion }} 85 | goLive: ${{ github.event.inputs.goLive }} 86 | 87 | PostProcess: 88 | needs: [ Initialization, Deliver ] 89 | if: always() 90 | runs-on: [ ubuntu-latest ] 91 | steps: 92 | - name: Checkout 93 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 94 | 95 | - name: Finalize the workflow 96 | id: PostProcess 97 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 98 | env: 99 | GITHUB_TOKEN: ${{ github.token }} 100 | with: 101 | shell: pwsh 102 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 103 | currentJobContext: ${{ toJson(job) }} 104 | -------------------------------------------------------------------------------- /.github/workflows/CreatePerformanceTestApp.yaml: -------------------------------------------------------------------------------- 1 | name: 'Create a new performance test app' 2 | 3 | run-name: "Create a new performance test app in [${{ github.ref_name }}]" 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | project: 9 | description: Project name if the repository is setup for multiple projects 10 | required: false 11 | default: '.' 12 | name: 13 | description: Name 14 | required: true 15 | default: '.PerformanceTest' 16 | publisher: 17 | description: Publisher 18 | required: true 19 | idrange: 20 | description: ID range 21 | required: true 22 | default: '50000..99999' 23 | sampleCode: 24 | description: Include Sample code? 25 | type: boolean 26 | default: true 27 | sampleSuite: 28 | description: Include Sample BCPT Suite? 29 | type: boolean 30 | default: true 31 | directCommit: 32 | description: Direct Commit? 33 | type: boolean 34 | default: false 35 | useGhTokenWorkflow: 36 | description: Use GhTokenWorkflow for PR/Commit? 37 | type: boolean 38 | default: false 39 | 40 | permissions: 41 | actions: read 42 | contents: write 43 | id-token: write 44 | pull-requests: write 45 | 46 | defaults: 47 | run: 48 | shell: pwsh 49 | 50 | env: 51 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 52 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 53 | 54 | jobs: 55 | CreatePerformanceTestApp: 56 | needs: [ ] 57 | runs-on: [ ubuntu-latest ] 58 | steps: 59 | - name: Dump Workflow Information 60 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 61 | with: 62 | shell: pwsh 63 | 64 | - name: Checkout 65 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 66 | 67 | - name: Initialize the workflow 68 | id: init 69 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 70 | with: 71 | shell: pwsh 72 | 73 | - name: Read settings 74 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 75 | with: 76 | shell: pwsh 77 | 78 | - name: Read secrets 79 | id: ReadSecrets 80 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 81 | with: 82 | shell: pwsh 83 | gitHubSecrets: ${{ toJson(secrets) }} 84 | getSecrets: 'TokenForPush' 85 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 86 | 87 | - name: Creating a new test app 88 | uses: microsoft/AL-Go-Actions/CreateApp@v7.0 89 | with: 90 | shell: pwsh 91 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 92 | project: ${{ github.event.inputs.project }} 93 | type: 'Performance Test App' 94 | name: ${{ github.event.inputs.name }} 95 | publisher: ${{ github.event.inputs.publisher }} 96 | idrange: ${{ github.event.inputs.idrange }} 97 | sampleCode: ${{ github.event.inputs.sampleCode }} 98 | sampleSuite: ${{ github.event.inputs.sampleSuite }} 99 | directCommit: ${{ github.event.inputs.directCommit }} 100 | 101 | - name: Finalize the workflow 102 | if: always() 103 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 104 | env: 105 | GITHUB_TOKEN: ${{ github.token }} 106 | with: 107 | shell: pwsh 108 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 109 | currentJobContext: ${{ toJson(job) }} 110 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/AlertDevScopeExt.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | using System.Apps; 5 | using System.Utilities; 6 | 7 | codeunit 71180277 AlertDevScopeExtSESTM implements IAuditAlertSESTM 8 | { 9 | Access = Internal; 10 | Permissions = 11 | tabledata AlertSESTM = RI, 12 | tabledata "NAV App Installed App" = R; 13 | 14 | procedure CreateAlerts() 15 | var 16 | Alert: Record AlertSESTM; 17 | Extensions: Record "NAV App Installed App"; 18 | ActionRecommendationLbl: Label 'Talk to the developer that developed the extension and ask them to publish the extension in PTE scope instead.'; 19 | LongDescLbl: Label 'Extension in DEV Scope will get uninstalled when the environment is upgraded to a newer version. This can be Minor updates (monthly) or Major updates (bi-yearly). Publishing them in PTE scope instead will prevent this and make sure the business processes are not interrupted.'; 20 | ShortDescLbl: Label 'Extension in DEV Scope found: Name: "%1" AppId: "%2"', Comment = '%1 = Extension Name, %2 = App ID'; 21 | begin 22 | Extensions.SetRange("Published As", Extensions."Published As"::Dev); 23 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 24 | Extensions.SetLoadFields("App ID", Name); 25 | if Extensions.FindSet() then 26 | repeat 27 | Alert.New( 28 | AlertCodeSESTM::"SE-000002", 29 | StrSubstNo(ShortDescLbl, Extensions."Name", DelChr(Extensions."App ID", '=', '{}')), 30 | SeveritySESTM::Warning, 31 | AreaSESTM::Technical, 32 | LongDescLbl, 33 | ActionRecommendationLbl, 34 | Extensions."App ID" 35 | ); 36 | until Extensions.Next() = 0; 37 | end; 38 | 39 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 40 | var 41 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000002', Locked = true; 42 | begin 43 | Hyperlink(WikiLinkTok); 44 | end; 45 | 46 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 47 | var 48 | OpenPageQst: Label 'Do you want to open the page to manage the extension?'; 49 | begin 50 | if Confirm(OpenPageQst) then 51 | Page.Run(Page::"Extension Management"); 52 | end; 53 | 54 | procedure AutoFix(var Alert: Record AlertSESTM) 55 | var 56 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000002)'; 57 | begin 58 | Message(NoAutofixAvailableLbl); 59 | end; 60 | 61 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 62 | var 63 | Extensions: Record "NAV App Installed App"; 64 | begin 65 | Extensions.SetLoadFields("Name", "App ID", Publisher, "Version Major", "Version Minor", "Version Build", "Version Revision"); 66 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 67 | if not Extensions.Get(Alert.UniqueIdentifier) then 68 | exit; 69 | 70 | CustomDimensions.Add('AlertExtensionName', Extensions.Name); 71 | CustomDimensions.Add('AlertAppID', Extensions."App ID"); 72 | CustomDimensions.Add('AlertPublisher', Extensions.Publisher); 73 | CustomDimensions.Add('AlertAppVersion', StrSubstNo('%1.%2.%3.%4', Extensions."Version Major", Extensions."Version Minor", Extensions."Version Build", Extensions."Version Revision")); 74 | end; 75 | 76 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 77 | begin 78 | exit(Alert.ShortDescription); 79 | end; 80 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/AnalysisNotScheduled.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using System.Threading; 4 | 5 | codeunit 71180287 AnalysisNotScheduledSESTM implements IAuditAlertSESTM 6 | { 7 | Access = Internal; 8 | Permissions = 9 | tabledata "Job Queue Entry" = RI; 10 | 11 | procedure CreateAlerts() 12 | var 13 | Alert: Record AlertSESTM; 14 | JobQueueEntry: Record "Job Queue Entry"; 15 | ActionRecommendationLbl: Label 'Create a Job Queue Entry to run the Scheduled Alert Analysis.'; 16 | LongDescLbl: Label 'Scheduled Alert Analysis is not scheduled to run. This means that the alerts are not being evaluated and the system is not being monitored for potential issues.'; 17 | ShotDescLbl: Label 'Scheduled Alert Analysis is not scheduled to run.'; 18 | begin 19 | JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); 20 | JobQueueEntry.SetRange("Object ID to Run", Codeunit::"ReRunAllAlerts"); 21 | if JobQueueEntry.IsEmpty() then 22 | Alert.New( 23 | AlertCodeSESTM::"SE-000008", 24 | ShotDescLbl, 25 | SeveritySESTM::Info, 26 | AreaSESTM::Technical, 27 | LongDescLbl, 28 | ActionRecommendationLbl, 29 | '' 30 | ); 31 | end; 32 | 33 | local procedure CreateJobQueueEntry() 34 | var 35 | JobQueueEntry: Record "Job Queue Entry"; 36 | begin 37 | JobQueueEntry.Init(); 38 | JobQueueEntry.Validate("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); 39 | JobQueueEntry.Validate("Object ID to Run", Codeunit::"ReRunAllAlerts"); 40 | Evaluate(JobQueueEntry."Next Run Date Formula", '<1D>'); 41 | JobQueueEntry.Validate("Next Run Date Formula"); 42 | JobQueueEntry.Validate("Status", JobQueueEntry."Status"::Ready); 43 | JobQueueEntry.Validate("Recurring Job", true); 44 | JobQueueEntry.Validate("Run on Mondays", true); 45 | JobQueueEntry.Validate("Run on Tuesdays", true); 46 | JobQueueEntry.Validate("Run on Wednesdays", true); 47 | JobQueueEntry.Validate("Run on Thursdays", true); 48 | JobQueueEntry.Validate("Run on Fridays", true); 49 | JobQueueEntry.Validate("Run on Saturdays", true); 50 | JobQueueEntry.Validate("Run on Sundays", true); 51 | 52 | // This Validate will insert the Job Queue Entry 53 | JobQueueEntry.Validate("Earliest Start Date/Time", CreateDateTime(Today() + 1, 0T)); 54 | JobQueueEntry.Insert(true); 55 | end; 56 | 57 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 58 | var 59 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000008', Locked = true; 60 | begin 61 | Hyperlink(WikiLinkTok); 62 | end; 63 | 64 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 65 | var 66 | JobQueueEntriesPage: Page "Job Queue Entries"; 67 | begin 68 | JobQueueEntriesPage.Run(); 69 | end; 70 | 71 | procedure AutoFix(var Alert: Record AlertSESTM) 72 | var 73 | ConfirmJobQueueCreateQst: Label 'Do you want to create a Job Queue Entry to run the Scheduled Alert Analysis?'; 74 | begin 75 | if Confirm(ConfirmJobQueueCreateQst) then 76 | this.CreateJobQueueEntry(); 77 | end; 78 | 79 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 80 | begin 81 | // No custom telemetry dimensions to add 82 | end; 83 | 84 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 85 | begin 86 | exit(Alert.ShortDescription); 87 | end; 88 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/DemoDataExtInProd.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | using System.Apps; 5 | using System.Environment; 6 | 7 | codeunit 71180279 DemoDataExtInProdSESTM implements IAuditAlertSESTM 8 | { 9 | Access = Internal; 10 | Permissions = 11 | tabledata AlertSESTM = RI, 12 | tabledata Company = R; 13 | 14 | procedure CreateAlerts() 15 | var 16 | Extensions: Record "NAV App Installed App"; 17 | ContosoCoffeeDemoDatasetAppIdTok: Label '5a0b41e9-7a42-4123-d521-2265186cfb31', Locked = true; 18 | ContosoCoffeeDemoDatasetUSAppIdTok: Label '3a3f33b1-7b42-4123-a521-2265186cfb31', Locked = true; 19 | SustainabilityContosoCoffeeDemoDatasetAppIdTok: Label 'a0673989-48a4-48a0-9517-499c9f4037d3', Locked = true; 20 | begin 21 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 22 | Extensions.SetLoadFields("App ID", Name); 23 | 24 | this.CreateAlert(Extensions, ContosoCoffeeDemoDatasetAppIdTok); 25 | this.CreateAlert(Extensions, ContosoCoffeeDemoDatasetUSAppIdTok); 26 | this.CreateAlert(Extensions, SustainabilityContosoCoffeeDemoDatasetAppIdTok); 27 | end; 28 | 29 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 30 | var 31 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000004', Locked = true; 32 | begin 33 | Hyperlink(WikiLinkTok); 34 | end; 35 | 36 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 37 | var 38 | OpenPageQst: Label 'Do you want to open the page to manage the extension?'; 39 | begin 40 | if Confirm(OpenPageQst) then 41 | Page.Run(Page::"Extension Management"); 42 | end; 43 | 44 | procedure AutoFix(var Alert: Record AlertSESTM) 45 | var 46 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000004)'; 47 | begin 48 | Message(NoAutofixAvailableLbl); 49 | end; 50 | 51 | local procedure CreateAlert(var Extensions: Record "NAV App Installed App"; AppId: Text) 52 | var 53 | Alert: Record AlertSESTM; 54 | EnvironmentInformation: Codeunit "Environment Information"; 55 | ActionRecommendationLbl: Label 'Uninstall the "%1" Extension', Comment = '%1 = Extension Name'; 56 | LongDescLbl: Label 'Extension for generation of demo data can mess up your data. If you do not need it to generate demo data anymore, you should consider uninstalling it.'; 57 | ShotDescLbl: Label 'Demo Data Extension Found: %1', Comment = '%1 = Extension Name'; 58 | begin 59 | Extensions.SetRange("App ID", AppId); 60 | if Extensions.FindFirst() then 61 | Alert.New( 62 | "AlertCodeSESTM"::"SE-000004", 63 | StrSubstNo(ShotDescLbl, Extensions."Name"), 64 | EnvironmentInformation.IsProduction() ? SeveritySESTM::Warning : SeveritySESTM::Info, 65 | AreaSESTM::Technical, 66 | LongDescLbl, 67 | StrSubstNo(ActionRecommendationLbl, Extensions."Name"), 68 | Extensions."App ID" 69 | ); 70 | end; 71 | 72 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 73 | var 74 | Extensions: Record "NAV App Installed App"; 75 | begin 76 | Extensions.SetLoadFields("Name", "App ID", Publisher, "Version Major", "Version Minor", "Version Build", "Version Revision"); 77 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 78 | if not Extensions.Get(Alert.UniqueIdentifier) then 79 | exit; 80 | 81 | CustomDimensions.Add('AlertExtensionName', Extensions.Name); 82 | CustomDimensions.Add('AlertAppID', Extensions."App ID"); 83 | CustomDimensions.Add('AlertPublisher', Extensions.Publisher); 84 | CustomDimensions.Add('AlertAppVersion', StrSubstNo('%1.%2.%3.%4', Extensions."Version Major", Extensions."Version Minor", Extensions."Version Build", Extensions."Version Revision")); 85 | end; 86 | 87 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 88 | begin 89 | exit(Alert.ShortDescription); 90 | end; 91 | } -------------------------------------------------------------------------------- /.AL-Go/cloudDevEnv.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Script for creating cloud development environment 3 | # Please do not modify this script as it will be auto-updated from the AL-Go Template 4 | # Recommended approach is to use as is or add a script (freddyk-devenv.ps1), which calls this script with the user specific parameters 5 | # 6 | Param( 7 | [string] $environmentName = "", 8 | [bool] $reuseExistingEnvironment, 9 | [switch] $fromVSCode, 10 | [switch] $clean 11 | ) 12 | 13 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 14 | 15 | function DownloadHelperFile { 16 | param( 17 | [string] $url, 18 | [string] $folder 19 | ) 20 | 21 | $prevProgressPreference = $ProgressPreference; $ProgressPreference = 'SilentlyContinue' 22 | $name = [System.IO.Path]::GetFileName($url) 23 | Write-Host "Downloading $name from $url" 24 | $path = Join-Path $folder $name 25 | Invoke-WebRequest -UseBasicParsing -uri $url -OutFile $path 26 | $ProgressPreference = $prevProgressPreference 27 | return $path 28 | } 29 | 30 | try { 31 | Clear-Host 32 | Write-Host 33 | Write-Host -ForegroundColor Yellow @' 34 | _____ _ _ _____ ______ 35 | / ____| | | | | __ \ | ____| 36 | | | | | ___ _ _ __| | | | | | _____ __ |__ _ ____ __ 37 | | | | |/ _ \| | | |/ _` | | | | |/ _ \ \ / / __| | '_ \ \ / / 38 | | |____| | (_) | |_| | (_| | | |__| | __/\ V /| |____| | | \ V / 39 | \_____|_|\___/ \__,_|\__,_| |_____/ \___| \_/ |______|_| |_|\_/ 40 | 41 | '@ 42 | 43 | $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" 44 | New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null 45 | $GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/Github-Helper.psm1' -folder $tmpFolder 46 | $ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/AL-Go-Helper.ps1' -folder $tmpFolder 47 | DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/Packages.json' -folder $tmpFolder | Out-Null 48 | 49 | Import-Module $GitHubHelperPath 50 | . $ALGoHelperPath -local 51 | 52 | $baseFolder = GetBaseFolder -folder $PSScriptRoot 53 | $project = GetProject -baseFolder $baseFolder -projectALGoFolder $PSScriptRoot 54 | 55 | Write-Host @' 56 | 57 | This script will create a cloud based development environment (Business Central SaaS Sandbox) for your project. 58 | All apps and test apps will be compiled and published to the environment in the development scope. 59 | The script will also modify launch.json to have a "Cloud Sandbox ()" configuration point to your environment. 60 | 61 | '@ 62 | 63 | if (Test-Path (Join-Path $PSScriptRoot "NewBcContainer.ps1")) { 64 | Write-Host -ForegroundColor Red "WARNING: The project has a NewBcContainer override defined. Typically, this means that you cannot run a cloud development environment" 65 | } 66 | 67 | Write-Host 68 | 69 | if (-not $environmentName) { 70 | $environmentName = Enter-Value ` 71 | -title "Environment name" ` 72 | -question "Please enter the name of the environment to create" ` 73 | -default "$($env:USERNAME)-sandbox" ` 74 | -trimCharacters @('"',"'",' ') 75 | } 76 | 77 | if ($PSBoundParameters.Keys -notcontains 'reuseExistingEnvironment') { 78 | $reuseExistingEnvironment = (Select-Value ` 79 | -title "What if the environment already exists?" ` 80 | -options @{ "Yes" = "Reuse existing environment"; "No" = "Recreate environment" } ` 81 | -question "Select behavior" ` 82 | -default "No") -eq "Yes" 83 | } 84 | 85 | CreateDevEnv ` 86 | -kind cloud ` 87 | -caller local ` 88 | -environmentName $environmentName ` 89 | -reuseExistingEnvironment:$reuseExistingEnvironment ` 90 | -baseFolder $baseFolder ` 91 | -project $project ` 92 | -clean:$clean 93 | } 94 | catch { 95 | Write-Host -ForegroundColor Red "Error: $($_.Exception.Message)`nStacktrace: $($_.scriptStackTrace)" 96 | } 97 | finally { 98 | if ($fromVSCode) { 99 | Read-Host "Press ENTER to close this window" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/UserWithSuper.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | using System.Security.AccessControl; 5 | using System.Security.User; 6 | 7 | codeunit 71180280 UserWithSuperSESTM implements IAuditAlertSESTM 8 | { 9 | Access = Internal; 10 | Permissions = 11 | tabledata "Access Control" = R, 12 | tabledata AlertSESTM = RI; 13 | 14 | procedure CreateAlerts() 15 | var 16 | AccessControl: Record "Access Control"; 17 | Alert: Record AlertSESTM; 18 | User: Record User; 19 | ActionRecommendationLbl: Label 'Reduce the permissions of the user %1 in the company %2 if possible.', Comment = '%1 = User Name, %2 = Company Name'; 20 | LongDescLbl: Label 'The user %1 has SUPER permissions in the company %2. This is a security risk and should be reviewed.', Comment = '%1 = User Name, %2 = Company Name'; 21 | ShortDescLbl: Label 'User "%1: has SUPER permissions in Company %2', Comment = '%1 = User Name, %2 = Company Name'; 22 | begin 23 | User.SetFilter("License Type", '<>External User&<>Application&<>AAD Group'); 24 | User.ReadIsolation(IsolationLevel::ReadUncommitted); 25 | User.SetLoadFields("User Security ID", "User Name"); 26 | if User.FindSet() then 27 | repeat 28 | AccessControl.SetRange("User Security ID", User."User Security ID"); 29 | AccessControl.SetRange("Role ID", 'SUPER'); 30 | AccessControl.ReadIsolation(IsolationLevel::ReadUncommitted); 31 | AccessControl.SetLoadFields("User Security ID", "Role ID", "Company Name"); 32 | if AccessControl.FindSet() then 33 | repeat 34 | if AccessControl."Company Name" = '' then 35 | AccessControl."Company Name" := ''; 36 | 37 | Alert.New( 38 | "AlertCodeSESTM"::"SE-000005", 39 | StrSubstNo(ShortDescLbl, User."User Name", AccessControl."Company Name"), 40 | SeveritySESTM::Info, 41 | AreaSESTM::Permissions, 42 | StrSubstNo(LongDescLbl, User."User Name", AccessControl."Company Name"), 43 | StrSubstNo(ActionRecommendationLbl, User."User Name", AccessControl."Company Name"), 44 | this.CreateUniqueIdentifier(AccessControl) 45 | ); 46 | until AccessControl.Next() = 0; 47 | until User.Next() = 0; 48 | end; 49 | 50 | local procedure CreateUniqueIdentifier(var AccessControl: Record "Access Control"): Text[100] 51 | begin 52 | exit(CopyStr(AccessControl."User Security ID" + '/' + AccessControl."Role ID" + '/' + AccessControl."Company Name", 1, 100)); 53 | end; 54 | 55 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 56 | var 57 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000005', Locked = true; 58 | begin 59 | Hyperlink(WikiLinkTok); 60 | end; 61 | 62 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 63 | var 64 | User: Record User; 65 | OpenPageQst: Label 'Do you want to open the page to manage the user?'; 66 | begin 67 | if not Confirm(OpenPageQst) then 68 | exit; 69 | 70 | User.SetRange("User Security ID", Alert."UniqueIdentifier".Split('/').Get(1)); 71 | Page.Run(Page::"User Card", User); 72 | end; 73 | 74 | procedure AutoFix(var Alert: Record AlertSESTM) 75 | var 76 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000005)'; 77 | begin 78 | Message(NoAutofixAvailableLbl); 79 | end; 80 | 81 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 82 | begin 83 | CustomDimensions.Add('AlertUserSecurityID', Alert."UniqueIdentifier".Split('/').Get(1)); 84 | CustomDimensions.Add('AlertCompanyName', Alert."UniqueIdentifier".Split('/').Get(3)); 85 | end; 86 | 87 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 88 | var 89 | ShortDescLbl: Label 'User with SUPER permissions found', Locked = true; 90 | begin 91 | exit(ShortDescLbl); 92 | end; 93 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/AlertPteDownloadCode.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | using System.Apps; 5 | using System.Utilities; 6 | 7 | codeunit 71180276 AlertPteDownloadCodeSESTM implements IAuditAlertSESTM 8 | { 9 | Access = Internal; 10 | Permissions = 11 | tabledata AlertSESTM = RI, 12 | tabledata "NAV App Installed App" = R; 13 | 14 | procedure CreateAlerts() 15 | var 16 | Alert: Record AlertSESTM; 17 | Extensions: Record "NAV App Installed App"; 18 | ActionRecommendationLbl: Label 'Talk to the third party that developed the extension and ask for a copy of the code or to enable the download code option.'; 19 | LongDescLbl: Label 'The Per Tenant Extension does not allow Download Code, if the code was developed for you by a third party, you might want to make sure to have access to the code in case you need to make changes in the future and the third party is not available anymore. If you have access to the source, for example, because you developed the extension yourself or you have been granted access though another way, like GitHub, you can ignore this alert.'; 20 | ShortDescLbl: Label 'Download Code not allowed for PTE: Name: "%1" AppId: "%2"', Comment = '%1 = Extension Name, %2 = App ID'; 21 | begin 22 | Extensions.SetRange("Published As", Extensions."Published As"::PTE); 23 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 24 | Extensions.SetLoadFields("Package ID", "App ID", Name); 25 | if Extensions.FindSet() then 26 | repeat 27 | if not this.CanDownloadSourceCode(Extensions."Package ID") then 28 | Alert.New( 29 | "AlertCodeSESTM"::"SE-000001", 30 | StrSubstNo(ShortDescLbl, Extensions."Name", DelChr(Extensions."App ID", '=', '{}')), 31 | SeveritySESTM::Warning, 32 | AreaSESTM::Technical, 33 | LongDescLbl, 34 | ActionRecommendationLbl, 35 | Extensions."App ID" 36 | ); 37 | until Extensions.Next() = 0; 38 | end; 39 | 40 | [TryFunction] 41 | local procedure CanDownloadSourceCode(PackageId: Guid) 42 | var 43 | ExtensionManagement: Codeunit "Extension Management"; 44 | ExtensionSourceTempBlob: Codeunit "Temp Blob"; 45 | begin 46 | ExtensionManagement.GetExtensionSource(PackageId, ExtensionSourceTempBlob); 47 | end; 48 | 49 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 50 | var 51 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000001', Locked = true; 52 | begin 53 | Hyperlink(WikiLinkTok); 54 | end; 55 | 56 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 57 | var 58 | OpenPageQst: Label 'Do you want to open the page to manage the extension?'; 59 | begin 60 | if Confirm(OpenPageQst) then 61 | Page.Run(Page::"Extension Management"); 62 | end; 63 | 64 | procedure AutoFix(var Alert: Record AlertSESTM) 65 | var 66 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000001)'; 67 | begin 68 | Message(NoAutofixAvailableLbl); 69 | end; 70 | 71 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 72 | var 73 | Extensions: Record "NAV App Installed App"; 74 | begin 75 | Extensions.SetLoadFields("Name", "App ID", Publisher, "Version Major", "Version Minor", "Version Build", "Version Revision"); 76 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 77 | if not Extensions.Get(Alert.UniqueIdentifier) then 78 | exit; 79 | 80 | CustomDimensions.Add('AlertExtensionName', Extensions.Name); 81 | CustomDimensions.Add('AlertAppID', Extensions."App ID"); 82 | CustomDimensions.Add('AlertPublisher', Extensions.Publisher); 83 | CustomDimensions.Add('AlertAppVersion', StrSubstNo('%1.%2.%3.%4', Extensions."Version Major", Extensions."Version Minor", Extensions."Version Build", Extensions."Version Revision")); 84 | end; 85 | 86 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 87 | begin 88 | exit(Alert.ShortDescription); 89 | end; 90 | } -------------------------------------------------------------------------------- /.github/workflows/Current.yaml: -------------------------------------------------------------------------------- 1 | name: ' Test Current' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | id-token: write 10 | 11 | defaults: 12 | run: 13 | shell: pwsh 14 | 15 | env: 16 | workflowDepth: 1 17 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 18 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 19 | 20 | jobs: 21 | Initialization: 22 | needs: [ ] 23 | runs-on: [ ubuntu-latest ] 24 | outputs: 25 | projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} 26 | projectDependenciesJson: ${{ steps.determineProjectsToBuild.outputs.ProjectDependenciesJson }} 27 | buildOrderJson: ${{ steps.determineProjectsToBuild.outputs.BuildOrderJson }} 28 | workflowDepth: ${{ steps.DetermineWorkflowDepth.outputs.WorkflowDepth }} 29 | artifactsRetentionDays: ${{ steps.DetermineWorkflowDepth.outputs.ArtifactsRetentionDays }} 30 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 31 | steps: 32 | - name: Dump Workflow Information 33 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 34 | with: 35 | shell: pwsh 36 | 37 | - name: Checkout 38 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 39 | with: 40 | lfs: true 41 | 42 | - name: Initialize the workflow 43 | id: init 44 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 45 | with: 46 | shell: pwsh 47 | 48 | - name: Read settings 49 | id: ReadSettings 50 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 51 | with: 52 | shell: pwsh 53 | get: useGitSubmodules,shortLivedArtifactsRetentionDays 54 | 55 | - name: Read submodules token 56 | id: ReadSubmodulesToken 57 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 58 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 59 | with: 60 | shell: pwsh 61 | gitHubSecrets: ${{ toJson(secrets) }} 62 | getSecrets: '-gitSubmodulesToken' 63 | 64 | - name: Checkout Submodules 65 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 66 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | with: 68 | lfs: true 69 | submodules: ${{ env.useGitSubmodules }} 70 | token: '${{ fromJson(steps.ReadSubmodulesToken.outputs.Secrets).gitSubmodulesToken }}' 71 | 72 | - name: Determine Workflow Depth 73 | id: DetermineWorkflowDepth 74 | run: | 75 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "WorkflowDepth=$($env:workflowDepth)" 76 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "ArtifactsRetentionDays=$($env:shortLivedArtifactsRetentionDays)" 77 | 78 | - name: Determine Projects To Build 79 | id: determineProjectsToBuild 80 | uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v7.0 81 | with: 82 | shell: pwsh 83 | maxBuildDepth: ${{ env.workflowDepth }} 84 | 85 | Build: 86 | needs: [ Initialization ] 87 | if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 88 | strategy: 89 | matrix: 90 | include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} 91 | fail-fast: false 92 | name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) 93 | uses: ./.github/workflows/_BuildALGoProject.yaml 94 | secrets: inherit 95 | with: 96 | shell: ${{ matrix.githubRunnerShell }} 97 | runsOn: ${{ matrix.githubRunner }} 98 | project: ${{ matrix.project }} 99 | projectName: ${{ matrix.projectName }} 100 | buildMode: ${{ matrix.buildMode }} 101 | projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} 102 | secrets: 'licenseFileUrl,codeSignCertificateUrl,*codeSignCertificatePassword,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' 103 | artifactsRetentionDays: ${{ fromJson(needs.Initialization.outputs.artifactsRetentionDays) }} 104 | artifactsNameSuffix: 'Current' 105 | 106 | PostProcess: 107 | needs: [ Initialization, Build ] 108 | if: always() 109 | runs-on: [ ubuntu-latest ] 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 113 | 114 | - name: Finalize the workflow 115 | id: PostProcess 116 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 117 | env: 118 | GITHUB_TOKEN: ${{ github.token }} 119 | with: 120 | shell: pwsh 121 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 122 | currentJobContext: ${{ toJson(job) }} 123 | -------------------------------------------------------------------------------- /.github/workflows/NextMajor.yaml: -------------------------------------------------------------------------------- 1 | name: ' Test Next Major' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | id-token: write 10 | 11 | defaults: 12 | run: 13 | shell: pwsh 14 | 15 | env: 16 | workflowDepth: 1 17 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 18 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 19 | 20 | jobs: 21 | Initialization: 22 | needs: [ ] 23 | runs-on: [ ubuntu-latest ] 24 | outputs: 25 | projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} 26 | projectDependenciesJson: ${{ steps.determineProjectsToBuild.outputs.ProjectDependenciesJson }} 27 | buildOrderJson: ${{ steps.determineProjectsToBuild.outputs.BuildOrderJson }} 28 | workflowDepth: ${{ steps.DetermineWorkflowDepth.outputs.WorkflowDepth }} 29 | artifactsRetentionDays: ${{ steps.DetermineWorkflowDepth.outputs.ArtifactsRetentionDays }} 30 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 31 | steps: 32 | - name: Dump Workflow Information 33 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 34 | with: 35 | shell: pwsh 36 | 37 | - name: Checkout 38 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 39 | with: 40 | lfs: true 41 | 42 | - name: Initialize the workflow 43 | id: init 44 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 45 | with: 46 | shell: pwsh 47 | 48 | - name: Read settings 49 | id: ReadSettings 50 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 51 | with: 52 | shell: pwsh 53 | get: useGitSubmodules,shortLivedArtifactsRetentionDays 54 | 55 | - name: Read submodules token 56 | id: ReadSubmodulesToken 57 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 58 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 59 | with: 60 | shell: pwsh 61 | gitHubSecrets: ${{ toJson(secrets) }} 62 | getSecrets: '-gitSubmodulesToken' 63 | 64 | - name: Checkout Submodules 65 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 66 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | with: 68 | lfs: true 69 | submodules: ${{ env.useGitSubmodules }} 70 | token: '${{ fromJson(steps.ReadSubmodulesToken.outputs.Secrets).gitSubmodulesToken }}' 71 | 72 | - name: Determine Workflow Depth 73 | id: DetermineWorkflowDepth 74 | run: | 75 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "WorkflowDepth=$($env:workflowDepth)" 76 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "ArtifactsRetentionDays=$($env:shortLivedArtifactsRetentionDays)" 77 | 78 | - name: Determine Projects To Build 79 | id: determineProjectsToBuild 80 | uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v7.0 81 | with: 82 | shell: pwsh 83 | maxBuildDepth: ${{ env.workflowDepth }} 84 | 85 | Build: 86 | needs: [ Initialization ] 87 | if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 88 | strategy: 89 | matrix: 90 | include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} 91 | fail-fast: false 92 | name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) 93 | uses: ./.github/workflows/_BuildALGoProject.yaml 94 | secrets: inherit 95 | with: 96 | shell: ${{ matrix.githubRunnerShell }} 97 | runsOn: ${{ matrix.githubRunner }} 98 | project: ${{ matrix.project }} 99 | projectName: ${{ matrix.projectName }} 100 | buildMode: ${{ matrix.buildMode }} 101 | projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} 102 | secrets: 'licenseFileUrl,codeSignCertificateUrl,*codeSignCertificatePassword,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' 103 | artifactsRetentionDays: ${{ fromJson(needs.Initialization.outputs.artifactsRetentionDays) }} 104 | artifactsNameSuffix: 'NextMajor' 105 | 106 | PostProcess: 107 | needs: [ Initialization, Build ] 108 | if: always() 109 | runs-on: [ ubuntu-latest ] 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 113 | 114 | - name: Finalize the workflow 115 | id: PostProcess 116 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 117 | env: 118 | GITHUB_TOKEN: ${{ github.token }} 119 | with: 120 | shell: pwsh 121 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 122 | currentJobContext: ${{ toJson(job) }} 123 | -------------------------------------------------------------------------------- /.github/workflows/NextMinor.yaml: -------------------------------------------------------------------------------- 1 | name: ' Test Next Minor' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | id-token: write 10 | 11 | defaults: 12 | run: 13 | shell: pwsh 14 | 15 | env: 16 | workflowDepth: 1 17 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 18 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 19 | 20 | jobs: 21 | Initialization: 22 | needs: [ ] 23 | runs-on: [ ubuntu-latest ] 24 | outputs: 25 | projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} 26 | projectDependenciesJson: ${{ steps.determineProjectsToBuild.outputs.ProjectDependenciesJson }} 27 | buildOrderJson: ${{ steps.determineProjectsToBuild.outputs.BuildOrderJson }} 28 | workflowDepth: ${{ steps.DetermineWorkflowDepth.outputs.WorkflowDepth }} 29 | artifactsRetentionDays: ${{ steps.DetermineWorkflowDepth.outputs.ArtifactsRetentionDays }} 30 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 31 | steps: 32 | - name: Dump Workflow Information 33 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 34 | with: 35 | shell: pwsh 36 | 37 | - name: Checkout 38 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 39 | with: 40 | lfs: true 41 | 42 | - name: Initialize the workflow 43 | id: init 44 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 45 | with: 46 | shell: pwsh 47 | 48 | - name: Read settings 49 | id: ReadSettings 50 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 51 | with: 52 | shell: pwsh 53 | get: useGitSubmodules,shortLivedArtifactsRetentionDays 54 | 55 | - name: Read submodules token 56 | id: ReadSubmodulesToken 57 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 58 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 59 | with: 60 | shell: pwsh 61 | gitHubSecrets: ${{ toJson(secrets) }} 62 | getSecrets: '-gitSubmodulesToken' 63 | 64 | - name: Checkout Submodules 65 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 66 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 67 | with: 68 | lfs: true 69 | submodules: ${{ env.useGitSubmodules }} 70 | token: '${{ fromJson(steps.ReadSubmodulesToken.outputs.Secrets).gitSubmodulesToken }}' 71 | 72 | - name: Determine Workflow Depth 73 | id: DetermineWorkflowDepth 74 | run: | 75 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "WorkflowDepth=$($env:workflowDepth)" 76 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "ArtifactsRetentionDays=$($env:shortLivedArtifactsRetentionDays)" 77 | 78 | - name: Determine Projects To Build 79 | id: determineProjectsToBuild 80 | uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v7.0 81 | with: 82 | shell: pwsh 83 | maxBuildDepth: ${{ env.workflowDepth }} 84 | 85 | Build: 86 | needs: [ Initialization ] 87 | if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 88 | strategy: 89 | matrix: 90 | include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} 91 | fail-fast: false 92 | name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) 93 | uses: ./.github/workflows/_BuildALGoProject.yaml 94 | secrets: inherit 95 | with: 96 | shell: ${{ matrix.githubRunnerShell }} 97 | runsOn: ${{ matrix.githubRunner }} 98 | project: ${{ matrix.project }} 99 | projectName: ${{ matrix.projectName }} 100 | buildMode: ${{ matrix.buildMode }} 101 | projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} 102 | secrets: 'licenseFileUrl,codeSignCertificateUrl,*codeSignCertificatePassword,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' 103 | artifactsRetentionDays: ${{ fromJson(needs.Initialization.outputs.artifactsRetentionDays) }} 104 | artifactsNameSuffix: 'NextMinor' 105 | 106 | PostProcess: 107 | needs: [ Initialization, Build ] 108 | if: always() 109 | runs-on: [ ubuntu-latest ] 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 113 | 114 | - name: Finalize the workflow 115 | id: PostProcess 116 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 117 | env: 118 | GITHUB_TOKEN: ${{ github.token }} 119 | with: 120 | shell: pwsh 121 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 122 | currentJobContext: ${{ toJson(job) }} 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.7.33...HEAD) 6 | 7 | 8 | 9 | ## [1.7.33](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/HEAD) 10 | 11 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.6.26...1.7.33) 12 | 13 | **Changes:** 14 | - Improve telemetry logging for rules, change logging to daily, give option to disable logging, log to "All" instead of "ExtensionPublisher" 15 | - Ability to completely turn off telemetry 16 | - Ability to control telemetry via ruleset per Alert individually 17 | - Added card page for alerts and remove long description and action recommendation from the list page 18 | - Improve Interface documentation 19 | 20 | ## [1.6.26](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.6.26) 21 | 22 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.5.25...1.6.26) 23 | 24 | - Next attempt to get Rule 7 fixed. It tried to insert alerts even if the extension was not installed. 25 | 26 | ## [1.5.25](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.5.25) 27 | 28 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.4.22...1.5.25) 29 | 30 | **Changes:** 31 | - Added JobsSetup to NoSeries Checks Merge pull request [\#6](https://github.com/StefanMaron/BusinessCentral.Sentinel/pull/6) from [pri-kise](https://github.com/pri-kise) 32 | - Added Rule in Ruleset to make the `this.` rule a warning 33 | - Added Instructions for the Sentinel Tool on the Alert list 34 | - Improved documentation on Rule 1-3 35 | 36 | **Closed Issues:** 37 | - Improve Rule SE-000007, there where cases where the App Name was not shown, also display the App ID [\#7](https://github.com/StefanMaron/BusinessCentral.Sentinel/issues/7) 38 | 39 | ## [1.4.22](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.4.22) (2024-12-05) 40 | 41 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.3.17...1.4.22) 42 | 43 | **Changes:** 44 | 45 | - Added extensions to Rule SE-000007: Shopify Connector, AMC Banking, API - Cross Environment Intercompany, Business Central Cloud Migration - Previous Release, Business Central Cloud Migration API, Business Central Intelligent Cloud, Ceridian Payroll 46 | - Adjusted rule SE-000007 to be more generic and not Shopify Connector specific 47 | - Fixed a bug that prevented the full rerun to work 48 | - Added Changelog 49 | - Move additional info to wiki instead of a message 50 | - hide ignored alerts by default 51 | - Add Sentinel Rule Set functionality and update alert severity handling 52 | 53 | 54 | ## [1.3.17](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.3.17) (2024-11-26) 55 | 56 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.2.15...1.3.17) 57 | 58 | **Closed Issues:** 59 | 60 | - The record is already open. [\#5](https://github.com/StefanMaron/BusinessCentral.Sentinel/issues/5) 61 | - The number sequence 'BCSentinelSESTMAlertId' does not exist. [\#5](https://github.com/StefanMaron/BusinessCentral.Sentinel/issues/5) 62 | 63 | ## [1.2.15](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.2.15) (2024-11-26) 64 | 65 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.1.12...1.2.15) 66 | 67 | **Changes:** 68 | 69 | - Added rule to warn if the Shopify Connector is installed but unused (no Shops configured) 70 | - Fixed an issue which prevented alerts to get created 71 | - Added Telemetry for alerts 72 | 73 | ## [1.1.12](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.1.12) (2024-11-25) 74 | 75 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/compare/1.0.10...1.1.12) 76 | 77 | **Changes:** 78 | 79 | - Changes of the Interface to seperate action recomendations, and auto fix action 80 | - moved the drill down link to seperate actions to make the UX better 81 | - `Alert.New()` for easier rule creation 82 | 83 | **Closed issues:** 84 | 85 | - Fix for Bug: Rec.FindFirst could give error [\#4](https://github.com/StefanMaron/BusinessCentral.Sentinel/issues/4) 86 | - Fix for Evaluation company in a Sandbox [\#2](https://github.com/StefanMaron/BusinessCentral.Sentinel/issues/2) 87 | 88 | 89 | ## [1.0.10](https://github.com/StefanMaron/BusinessCentral.Sentinel/tree/1.0.10) (2024-11-25) 90 | 91 | [Full Changelog](https://github.com/StefanMaron/BusinessCentral.Sentinel/commits/1.0.10) 92 | 93 | **Changes:** 94 | 95 | - First release to AppSource 96 | - Contains the "engine" to run the alerts 97 | - Warns if Per Tenant Extension do not allow Download Code 98 | - Warns if Extension in DEV Scope are installed 99 | - Evaluation Company detected in Production 100 | - Demo Data Extensions should get uninstalled from production 101 | - Inform about users with Super permissions 102 | - Consider configuring non-posting number series to allow gaps to increase performance -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/AlertCard.Page.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using System.Reflection; 4 | 5 | page 71180279 AlertCard 6 | { 7 | ApplicationArea = All; 8 | Caption = 'Alert Card'; 9 | Editable = false; 10 | Extensible = false; 11 | PageType = Card; 12 | SourceTable = AlertSESTM; 13 | UsageCategory = None; 14 | 15 | layout 16 | { 17 | area(Content) 18 | { 19 | group(General) 20 | { 21 | Caption = 'General'; 22 | field(AlertCode; Rec.AlertCode) { } 23 | field(ShortDescription; Rec.ShortDescription) 24 | { 25 | MultiLine = true; 26 | } 27 | field("Area"; Rec."Area") { } 28 | field(Severity; Rec.Severity) { } 29 | field(Ignore; Rec.Ignore) { } 30 | group(ActionRecommendationGrp) 31 | { 32 | Caption = 'Action Recommendation'; 33 | field(ActionRecommendation; Rec.ActionRecommendation) 34 | { 35 | MultiLine = true; 36 | ShowCaption = false; 37 | } 38 | } 39 | } 40 | group(LongDescriptionGrp) 41 | { 42 | Caption = 'Long Description'; 43 | field(LongDescription; this.LongDescriptionTxt) 44 | { 45 | ExtendedDatatype = RichContent; 46 | MultiLine = true; 47 | ShowCaption = false; 48 | } 49 | 50 | } 51 | } 52 | } 53 | 54 | actions 55 | { 56 | area(Processing) 57 | { 58 | action(SetToIgnore) 59 | { 60 | AboutText = 'Flags the alert as ignored. Ignored alerts will be excluded from reports and queues on the Role Center.'; 61 | AboutTitle = 'Ignore'; 62 | Caption = 'Ignore'; 63 | Image = Delete; 64 | ToolTip = 'Ignore this alert.'; 65 | 66 | trigger OnAction() 67 | begin 68 | Rec.SetToIgnore(); 69 | end; 70 | } 71 | action(ClearIgnore) 72 | { 73 | Caption = 'Stop Ignoring'; 74 | Image = Restore; 75 | ToolTip = 'Clear the ignore status of this alert.'; 76 | 77 | trigger OnAction() 78 | begin 79 | Rec.ClearIgnore(); 80 | end; 81 | } 82 | action(MoreDetails) 83 | { 84 | Caption = 'More Details'; 85 | Ellipsis = true; 86 | Image = LaunchWeb; 87 | Scope = Repeater; 88 | ToolTip = 'Show more details about this alert.'; 89 | 90 | trigger OnAction() 91 | var 92 | IAuditAlert: Interface IAuditAlertSESTM; 93 | begin 94 | IAuditAlert := Rec.AlertCode; 95 | IAuditAlert.ShowMoreDetails(Rec); 96 | end; 97 | } 98 | action(AutoFix) 99 | { 100 | Caption = 'Auto Fix'; 101 | Ellipsis = true; 102 | Image = Action; 103 | Scope = Repeater; 104 | ToolTip = 'Automatically fix the issue that caused this alert.'; 105 | 106 | trigger OnAction() 107 | var 108 | IAuditAlert: Interface IAuditAlertSESTM; 109 | begin 110 | IAuditAlert := Rec.AlertCode; 111 | IAuditAlert.AutoFix(Rec); 112 | end; 113 | } 114 | action(ShowRelatedInformation) 115 | { 116 | Caption = 'Show Related Information'; 117 | Ellipsis = true; 118 | Image = ViewDetails; 119 | Scope = Repeater; 120 | ToolTip = 'Show related information about this alert.'; 121 | 122 | trigger OnAction() 123 | var 124 | IAuditAlert: Interface IAuditAlertSESTM; 125 | begin 126 | IAuditAlert := Rec.AlertCode; 127 | IAuditAlert.ShowRelatedInformation(Rec); 128 | end; 129 | } 130 | } 131 | 132 | area(Promoted) 133 | { 134 | group(Ignore_promoted) 135 | { 136 | ShowAs = SplitButton; 137 | actionref(SetToIgnore_Promoted; SetToIgnore) { } 138 | actionref(ClearIgnore_Promoted; ClearIgnore) { } 139 | } 140 | actionref(MoreDetails_Promoted; MoreDetails) { } 141 | actionref(AutoFix_Promoted; AutoFix) { } 142 | actionref(ShowRelatedInformation_Promoted; ShowRelatedInformation) { } 143 | } 144 | } 145 | 146 | var 147 | LongDescriptionTxt: Text; 148 | 149 | trigger OnAfterGetRecord() 150 | begin 151 | this.LongDescriptionTxt := this.FormatLineBreaksForHTML(Rec.LongDescription); 152 | end; 153 | 154 | local procedure FormatLineBreaksForHTML(Value: Text): Text 155 | var 156 | TypeHelper: Codeunit "Type Helper"; 157 | begin 158 | exit(Value.Replace('\', '
').Replace(TypeHelper.CRLFSeparator(), '
')); 159 | end; 160 | } -------------------------------------------------------------------------------- /.github/workflows/PullRequestHandler.yaml: -------------------------------------------------------------------------------- 1 | name: 'Pull Request Build' 2 | 3 | on: 4 | pull_request_target: 5 | branches: [ 'main' ] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 9 | cancel-in-progress: true 10 | 11 | defaults: 12 | run: 13 | shell: pwsh 14 | 15 | permissions: 16 | actions: read 17 | contents: read 18 | id-token: write 19 | pull-requests: read 20 | 21 | env: 22 | workflowDepth: 1 23 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 24 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 25 | 26 | jobs: 27 | PregateCheck: 28 | if: (github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name) && (github.event_name != 'pull_request') 29 | runs-on: windows-latest 30 | steps: 31 | - uses: microsoft/AL-Go-Actions/VerifyPRChanges@v7.0 32 | 33 | Initialization: 34 | needs: [ PregateCheck ] 35 | if: (!failure() && !cancelled()) 36 | runs-on: [ ubuntu-latest ] 37 | outputs: 38 | projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} 39 | projectDependenciesJson: ${{ steps.determineProjectsToBuild.outputs.ProjectDependenciesJson }} 40 | buildOrderJson: ${{ steps.determineProjectsToBuild.outputs.BuildOrderJson }} 41 | baselineWorkflowRunId: ${{ steps.determineProjectsToBuild.outputs.BaselineWorkflowRunId }} 42 | baselineWorkflowSHA: ${{ steps.determineProjectsToBuild.outputs.BaselineWorkflowSHA }} 43 | workflowDepth: ${{ steps.DetermineWorkflowDepth.outputs.WorkflowDepth }} 44 | artifactsRetentionDays: ${{ steps.DetermineWorkflowDepth.outputs.ArtifactsRetentionDays }} 45 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 46 | steps: 47 | - name: Dump Workflow Information 48 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 49 | with: 50 | shell: pwsh 51 | 52 | - name: Checkout 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 54 | with: 55 | lfs: true 56 | ref: ${{ github.event_name == 'pull_request' && github.sha || format('refs/pull/{0}/merge', github.event.pull_request.number) }} 57 | 58 | - name: Initialize the workflow 59 | id: init 60 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 61 | with: 62 | shell: pwsh 63 | 64 | - name: Read settings 65 | id: ReadSettings 66 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 67 | with: 68 | shell: pwsh 69 | get: shortLivedArtifactsRetentionDays 70 | 71 | - name: Determine Workflow Depth 72 | id: DetermineWorkflowDepth 73 | run: | 74 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "WorkflowDepth=$($env:workflowDepth)" 75 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "ArtifactsRetentionDays=$($env:shortLivedArtifactsRetentionDays)" 76 | 77 | - name: Determine Projects To Build 78 | id: determineProjectsToBuild 79 | uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v7.0 80 | with: 81 | shell: pwsh 82 | maxBuildDepth: ${{ env.workflowDepth }} 83 | 84 | Build: 85 | needs: [ Initialization ] 86 | if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 87 | strategy: 88 | matrix: 89 | include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} 90 | fail-fast: false 91 | name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) 92 | uses: ./.github/workflows/_BuildALGoProject.yaml 93 | secrets: inherit 94 | with: 95 | shell: ${{ matrix.githubRunnerShell }} 96 | runsOn: ${{ matrix.githubRunner }} 97 | checkoutRef: ${{ github.event_name == 'pull_request' && github.sha || format('refs/pull/{0}/merge', github.event.pull_request.number) }} 98 | project: ${{ matrix.project }} 99 | projectName: ${{ matrix.projectName }} 100 | buildMode: ${{ matrix.buildMode }} 101 | projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} 102 | baselineWorkflowRunId: ${{ needs.Initialization.outputs.baselineWorkflowRunId }} 103 | baselineWorkflowSHA: ${{ needs.Initialization.outputs.baselineWorkflowSHA }} 104 | secrets: 'licenseFileUrl,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' 105 | artifactsRetentionDays: ${{ fromJson(needs.Initialization.outputs.artifactsRetentionDays) }} 106 | artifactsNameSuffix: 'PR${{ github.event.number }}' 107 | useArtifactCache: true 108 | 109 | StatusCheck: 110 | needs: [ Initialization, Build ] 111 | if: (!cancelled()) 112 | runs-on: [ ubuntu-latest ] 113 | name: Pull Request Status Check 114 | steps: 115 | - name: Pull Request Status Check 116 | id: PullRequestStatusCheck 117 | uses: microsoft/AL-Go-Actions/PullRequestStatusCheck@v7.0 118 | env: 119 | GITHUB_TOKEN: ${{ github.token }} 120 | with: 121 | shell: pwsh 122 | 123 | - name: Finalize the workflow 124 | id: PostProcess 125 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 126 | if: success() || failure() 127 | env: 128 | GITHUB_TOKEN: ${{ github.token }} 129 | with: 130 | shell: pwsh 131 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 132 | currentJobContext: ${{ toJson(job) }} 133 | -------------------------------------------------------------------------------- /.github/workflows/UpdateGitHubGoSystemFiles.yaml: -------------------------------------------------------------------------------- 1 | name: ' Update AL-Go System Files' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | templateUrl: 7 | description: Template Repository URL (current is https://github.com/microsoft/AL-Go-AppSource@main) 8 | required: false 9 | default: '' 10 | downloadLatest: 11 | description: Download latest from template repository 12 | type: boolean 13 | default: true 14 | directCommit: 15 | description: Direct Commit? 16 | type: boolean 17 | default: false 18 | includeBranches: 19 | description: Specify a comma-separated list of branches to update. Wildcards are supported. The AL-Go settings will be read for every branch. Leave empty to update the current branch only. 20 | required: false 21 | default: '' 22 | schedule: 23 | - cron: '1 1 * * SAT' 24 | 25 | permissions: 26 | actions: read 27 | contents: read 28 | id-token: write 29 | 30 | defaults: 31 | run: 32 | shell: pwsh 33 | 34 | env: 35 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 36 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 37 | 38 | jobs: 39 | Initialize: 40 | runs-on: windows-latest 41 | name: Initialize 42 | outputs: 43 | UpdateBranches: ${{ steps.GetBranches.outputs.Result }} 44 | TemplateUrl: ${{ steps.DetermineTemplateUrl.outputs.TemplateUrl }} 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 48 | 49 | - name: Read settings 50 | id: ReadSettings 51 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 52 | with: 53 | shell: pwsh 54 | get: templateUrl 55 | 56 | - name: Get Workflow Multi-Run Branches 57 | id: GetBranches 58 | uses: microsoft/AL-Go-Actions/GetWorkflowMultiRunBranches@v7.0 59 | with: 60 | shell: pwsh 61 | includeBranches: ${{ github.event.inputs.includeBranches }} 62 | 63 | - name: Determine Template URL 64 | id: DetermineTemplateUrl 65 | env: 66 | TemplateUrlAsInput: '${{ github.event.inputs.templateUrl }}' 67 | run: | 68 | $templateUrl = $env:templateUrl # Available from ReadSettings step 69 | if ($ENV:TemplateUrlAsInput) { 70 | # Use the input value if it is provided 71 | $templateUrl = $ENV:TemplateUrlAsInput 72 | } 73 | Write-Host "Using template URL: $templateUrl" 74 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "TemplateUrl=$templateUrl" 75 | 76 | UpdateALGoSystemFiles: 77 | name: "[${{ matrix.branch }}] Update AL-Go System Files" 78 | needs: [ Initialize ] 79 | runs-on: [ ubuntu-latest ] 80 | strategy: 81 | matrix: 82 | branch: ${{ fromJson(needs.Initialize.outputs.UpdateBranches).branches }} 83 | fail-fast: false 84 | 85 | steps: 86 | - name: Dump Workflow Information 87 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 88 | with: 89 | shell: pwsh 90 | 91 | - name: Checkout 92 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 93 | with: 94 | ref: ${{ matrix.branch }} 95 | 96 | - name: Initialize the workflow 97 | id: init 98 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 99 | with: 100 | shell: pwsh 101 | 102 | - name: Read settings 103 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 104 | with: 105 | shell: pwsh 106 | get: commitOptions 107 | 108 | - name: Read secrets 109 | id: ReadSecrets 110 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 111 | with: 112 | shell: pwsh 113 | gitHubSecrets: ${{ toJson(secrets) }} 114 | getSecrets: 'ghTokenWorkflow' 115 | 116 | - name: Calculate Commit Options 117 | env: 118 | directCommit: '${{ github.event.inputs.directCommit }}' 119 | downloadLatest: '${{ github.event.inputs.downloadLatest }}' 120 | run: | 121 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 122 | if('${{ github.event_name }}' -eq 'workflow_dispatch') { 123 | Write-Host "Using inputs from workflow_dispatch event" 124 | $directCommit = $env:directCommit 125 | $downloadLatest = $env:downloadLatest 126 | } 127 | else { 128 | Write-Host "Using inputs from commitOptions setting" 129 | $commitOptions = $env:commitOptions | ConvertFrom-Json # Available from ReadSettings step 130 | $directCommit=$(-not $commitOptions.createPullRequest) 131 | $downloadLatest=$true 132 | } 133 | Add-Content -Encoding UTF8 -Path $env:GITHUB_ENV -Value "directCommit=$directCommit" 134 | Add-Content -Encoding UTF8 -Path $env:GITHUB_ENV -Value "downloadLatest=$downloadLatest" 135 | 136 | - name: Update AL-Go system files 137 | uses: microsoft/AL-Go-Actions/CheckForUpdates@v7.0 138 | with: 139 | shell: pwsh 140 | token: ${{ fromJson(steps.ReadSecrets.outputs.Secrets).ghTokenWorkflow }} 141 | downloadLatest: ${{ env.downloadLatest }} 142 | update: 'Y' 143 | templateUrl: ${{ needs.Initialize.outputs.TemplateUrl }} 144 | directCommit: ${{ env.directCommit }} 145 | updateBranch: ${{ matrix.branch }} 146 | 147 | - name: Finalize the workflow 148 | if: always() 149 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 150 | env: 151 | GITHUB_TOKEN: ${{ github.token }} 152 | with: 153 | shell: pwsh 154 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 155 | currentJobContext: ${{ toJson(job) }} 156 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/NonPostNoSeriesGaps.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using Microsoft.Foundation.NoSeries; 4 | using Microsoft.Projects.Project.Setup; 5 | using Microsoft.Purchases.Setup; 6 | using Microsoft.Sales.Setup; 7 | using STM.BusinessCentral.Sentinel; 8 | 9 | codeunit 71180281 NonPostNoSeriesGapsSESTM implements IAuditAlertSESTM 10 | { 11 | Access = Internal; 12 | Permissions = 13 | tabledata AlertSESTM = RI, 14 | tabledata "Jobs Setup" = r, 15 | tabledata "No. Series" = R, 16 | tabledata "No. Series Line" = R, 17 | tabledata "Purchases & Payables Setup" = R, 18 | tabledata "Sales & Receivables Setup" = R; 19 | 20 | procedure CreateAlerts() 21 | begin 22 | this.CheckSalesSetup(); 23 | this.CheckPurchaseSetup(); 24 | this.CheckJobsSetup(); 25 | end; 26 | 27 | local procedure CheckPurchaseSetup() 28 | var 29 | PurchaseSetup: Record "Purchases & Payables Setup"; 30 | begin 31 | PurchaseSetup.ReadIsolation(IsolationLevel::ReadUncommitted); 32 | PurchaseSetup.SetLoadFields("Order Nos.", "Invoice Nos.", "Credit Memo Nos.", "Quote Nos.", "Vendor Nos.", "Blanket Order Nos.", "Price List Nos.", "Return Order Nos."); 33 | if not PurchaseSetup.Get() then 34 | exit; 35 | 36 | this.CheckNoSeries(PurchaseSetup."Order Nos."); 37 | this.CheckNoSeries(PurchaseSetup."Invoice Nos."); 38 | this.CheckNoSeries(PurchaseSetup."Credit Memo Nos."); 39 | this.CheckNoSeries(PurchaseSetup."Quote Nos."); 40 | this.CheckNoSeries(PurchaseSetup."Vendor Nos."); 41 | this.CheckNoSeries(PurchaseSetup."Blanket Order Nos."); 42 | this.CheckNoSeries(PurchaseSetup."Price List Nos."); 43 | this.CheckNoSeries(PurchaseSetup."Return Order Nos."); 44 | end; 45 | 46 | local procedure CheckSalesSetup() 47 | var 48 | SalesSetup: Record "Sales & Receivables Setup"; 49 | begin 50 | SalesSetup.ReadIsolation(IsolationLevel::ReadUncommitted); 51 | SalesSetup.SetLoadFields("Order Nos.", "Invoice Nos.", "Credit Memo Nos.", "Quote Nos.", "Customer Nos.", "Blanket Order Nos.", "Reminder Nos.", "Fin. Chrg. Memo Nos.", "Direct Debit Mandate Nos.", "Price List Nos."); 52 | if not SalesSetup.Get() then 53 | exit; 54 | 55 | this.CheckNoSeries(SalesSetup."Order Nos."); 56 | this.CheckNoSeries(SalesSetup."Invoice Nos."); 57 | this.CheckNoSeries(SalesSetup."Credit Memo Nos."); 58 | this.CheckNoSeries(SalesSetup."Quote Nos."); 59 | this.CheckNoSeries(SalesSetup."Customer Nos."); 60 | this.CheckNoSeries(SalesSetup."Blanket Order Nos."); 61 | this.CheckNoSeries(SalesSetup."Reminder Nos."); 62 | this.CheckNoSeries(SalesSetup."Fin. Chrg. Memo Nos."); 63 | this.CheckNoSeries(SalesSetup."Direct Debit Mandate Nos."); 64 | this.CheckNoSeries(SalesSetup."Price List Nos."); 65 | end; 66 | 67 | local procedure CheckJobsSetup() 68 | var 69 | JobsSetup: Record "Jobs Setup"; 70 | begin 71 | JobsSetup.ReadIsolation(IsolationLevel::ReadUncommitted); 72 | JobsSetup.SetLoadFields("Job Nos.", "Price List Nos."); 73 | if not JobsSetup.Get() then 74 | exit; 75 | 76 | this.CheckNoSeries(JobsSetup."Job Nos."); 77 | this.CheckNoSeries(JobsSetup."Price List Nos."); 78 | end; 79 | 80 | local procedure CheckNoSeries(NoSeriesCode: Code[20]) 81 | var 82 | Alert: Record AlertSESTM; 83 | NoSeriesLine: Record "No. Series Line"; 84 | NoSeriesSingle: Interface "No. Series - Single"; 85 | ActionRecommendationLbl: Label 'Change No Series %1 to allow gaps', Comment = '%1 = No. Series Code'; 86 | LongDescLbl: Label 'The No. Series %1 does not allow gaps and is responsible for non-posting documents/records. Consider configuring the No. Series to allow gaps to increase performance and decrease locking.', Comment = '%1 = No. Series Code'; 87 | ShortDescLbl: Label 'No Series %1 does not allow gaps', Comment = '%1 = No. Series Code'; 88 | begin 89 | if NoSeriesCode = '' then 90 | exit; 91 | 92 | NoSeriesLine.SetRange("Series Code", NoSeriesCode); 93 | if NoSeriesLine.FindSet() then 94 | repeat 95 | NoSeriesSingle := NoSeriesLine.Implementation; 96 | if not NoSeriesSingle.MayProduceGaps() then 97 | Alert.New( 98 | AlertCodeSESTM::"SE-000006", 99 | StrSubstNo(ShortDescLbl, NoSeriesCode), 100 | SeveritySESTM::Warning, 101 | AreaSESTM::Performance, 102 | StrSubstNo(LongDescLbl, NoSeriesCode), 103 | StrSubstNo(ActionRecommendationLbl, NoSeriesCode), 104 | NoSeriesCode 105 | ); 106 | until NoSeriesLine.Next() = 0; 107 | end; 108 | 109 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 110 | var 111 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000006', Locked = true; 112 | begin 113 | Hyperlink(WikiLinkTok); 114 | end; 115 | 116 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 117 | var 118 | NoSeries: Record "No. Series"; 119 | OpenRecordQst: Label 'Do you want to open the No. Series %1?', Comment = '%1 = No. Series Code'; 120 | begin 121 | if not Confirm(StrSubstNo(OpenRecordQst, Alert.UniqueIdentifier)) then 122 | exit; 123 | 124 | NoSeries.SetRange("Code", Alert.UniqueIdentifier); 125 | Page.Run(Page::"No. Series", NoSeries); 126 | end; 127 | 128 | 129 | procedure AutoFix(var Alert: Record AlertSESTM) 130 | var 131 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000006)'; 132 | begin 133 | Message(NoAutofixAvailableLbl); 134 | // TODO: Implement AutoFix 135 | end; 136 | 137 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 138 | begin 139 | CustomDimensions.Add('AlertNoSeriesCode', Alert.UniqueIdentifier); 140 | end; 141 | 142 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 143 | begin 144 | exit(Alert.ShortDescription); 145 | end; 146 | } -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Rules/UnusedExtensionInstalled.Codeunit.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | using System.Apps; 5 | using System.Environment; 6 | using System.Utilities; 7 | 8 | codeunit 71180283 UnusedExtensionInstalledSESTM implements IAuditAlertSESTM 9 | { 10 | Access = Internal; 11 | Permissions = 12 | tabledata AlertSESTM = RI, 13 | tabledata "NAV App Installed App" = R; 14 | 15 | procedure CreateAlerts() 16 | var 17 | AMCBankingTok: Label '16319982-4995-4fb1-8fb2-2b1e13773e3b', Locked = true; 18 | CeridianPayrollTok: Label '30828ce4-53e3-407f-ba80-13ce8d79d110', Locked = true; 19 | CloudMigrationApiTok: Label '57623bfa-0559-4bc2-ae1c-0979c29fc8d1', Locked = true; 20 | CloudMigrationTok: Label '6992416f-3f39-4d3c-8242-3fff61350bea', Locked = true; 21 | IntelligentCloudTok: Label '334ef79e-547e-4631-8ba1-7a7f18e14de6', Locked = true; 22 | IntercompanyAPITok: Label 'a190e87b-2f59-4e14-a727-421877802768', Locked = true; 23 | ShopifyConnectorIdTok: Label 'ec255f57-31d0-4ca2-b751-f2fa7c745abb', Locked = true; 24 | DynamicsSlMigrationIdTok: Label '237981b4-9e3c-437c-9b92-988aae978e8f', Locked = true; 25 | SubscriptionBillingIdTok: Label '3099ffc7-4cf7-4df6-9b96-7e4bc2bb587c', Locked = true; 26 | begin 27 | this.RaiseAlertIfExtensionIsUnused(ShopifyConnectorIdTok, 30102); 28 | this.RaiseAlertIfExtensionIsUnused(AMCBankingTok, 20101); 29 | this.RaiseAlertIfExtensionIsUnused(IntercompanyAPITok, 413); 30 | this.RaiseAlertIfExtensionIsUnused(CloudMigrationTok); 31 | this.RaiseAlertIfExtensionIsUnused(CloudMigrationApiTok); 32 | this.RaiseAlertIfExtensionIsUnused(IntelligentCloudTok); 33 | this.RaiseAlertIfExtensionIsUnused(DynamicsSlMigrationIdTok); 34 | this.RaiseAlertIfExtensionIsUnused(CeridianPayrollTok, 1665); 35 | this.RaiseAlertIfExtensionIsUnused(SubscriptionBillingIdTok, 8053); 36 | end; 37 | 38 | local procedure RaiseAlertIfExtensionIsUnused(AppId: Text) 39 | var 40 | TablesToVerify: List of [Integer]; 41 | begin 42 | this.RaiseAlertIfExtensionIsUnused(AppId, TablesToVerify); 43 | end; 44 | 45 | local procedure RaiseAlertIfExtensionIsUnused(AppId: Text; TableToVerify: Integer) 46 | var 47 | TablesToVerify: List of [Integer]; 48 | begin 49 | TablesToVerify.Add(TableToVerify); 50 | this.RaiseAlertIfExtensionIsUnused(AppId, TablesToVerify); 51 | end; 52 | 53 | local procedure RaiseAlertIfExtensionIsUnused(AppId: Text; TablesToVerify: List of [Integer]) 54 | var 55 | Alert: Record AlertSESTM; 56 | Company: Record Company; 57 | Extensions: Record "NAV App Installed App"; 58 | RecRef: RecordRef; 59 | TableId: Integer; 60 | ActionRecommendationLbl: Label 'If you are not using the extension, consider uninstalling it. This can have a positive impact on performance.'; 61 | LongDescLbl: Label 'Extension "%1" is installed in the environment but there is no data configured for it. This may indicate that the extension is not being used. App ID: %2', Comment = '%1 = Extension Name, %2 = App ID'; 62 | ShortDescLbl: Label 'Extension "%1" is installed but unused. App ID: %2', Comment = '%1 = Extension Name, %2 = App ID'; 63 | AppInfo: ModuleInfo; 64 | AppName: Text; 65 | begin 66 | Extensions.SetRange("App ID", AppId); 67 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 68 | if Extensions.IsEmpty() then 69 | exit; 70 | 71 | Company.SetRange("Evaluation Company", false); 72 | if Company.FindSet() then 73 | repeat 74 | foreach TableId in TablesToVerify do begin 75 | Clear(RecRef); 76 | 77 | RecRef.Open(TableId, false, Company.Name); 78 | if not RecRef.IsEmpty() then 79 | exit; 80 | RecRef.Close(); 81 | end; 82 | until Company.Next() = 0; 83 | 84 | AppName := NavApp.GetModuleInfo(AppId, AppInfo) ? AppInfo.Name : ''; 85 | 86 | Alert.New( 87 | AlertCodeSESTM::"SE-000007", 88 | StrSubstNo(ShortDescLbl, AppName, AppId), 89 | SeveritySESTM::Warning, 90 | AreaSESTM::Performance, 91 | StrSubstNo(LongDescLbl, AppName, AppId), 92 | ActionRecommendationLbl, 93 | CopyStr(AppId, 1, 100) 94 | ); 95 | end; 96 | 97 | procedure ShowMoreDetails(var Alert: Record AlertSESTM) 98 | var 99 | WikiLinkTok: Label 'https://github.com/StefanMaron/BusinessCentral.Sentinel/wiki/SE-000007', Locked = true; 100 | begin 101 | Hyperlink(WikiLinkTok); 102 | end; 103 | 104 | procedure ShowRelatedInformation(var Alert: Record AlertSESTM) 105 | var 106 | OpenPageQst: Label 'Do you want to open the page to manage the extension?'; 107 | begin 108 | if Confirm(OpenPageQst) then 109 | Page.Run(Page::"Extension Management"); 110 | end; 111 | 112 | procedure AutoFix(var Alert: Record AlertSESTM) 113 | var 114 | NoAutofixAvailableLbl: Label 'No autofix available for this alert. (SE-000007)'; 115 | begin 116 | Message(NoAutofixAvailableLbl); 117 | end; 118 | 119 | procedure AddCustomTelemetryDimensions(var Alert: Record AlertSESTM; var CustomDimensions: Dictionary of [Text, Text]) 120 | var 121 | Extensions: Record "NAV App Installed App"; 122 | begin 123 | Extensions.SetLoadFields("Name", "App ID", Publisher, "Version Major", "Version Minor", "Version Build", "Version Revision"); 124 | Extensions.ReadIsolation(IsolationLevel::ReadUncommitted); 125 | if not Extensions.Get(Alert.UniqueIdentifier) then 126 | exit; 127 | 128 | CustomDimensions.Add('AlertExtensionName', Extensions.Name); 129 | CustomDimensions.Add('AlertAppID', Extensions."App ID"); 130 | CustomDimensions.Add('AlertPublisher', Extensions.Publisher); 131 | CustomDimensions.Add('AlertAppVersion', StrSubstNo('%1.%2.%3.%4', Extensions."Version Major", Extensions."Version Minor", Extensions."Version Build", Extensions."Version Revision")); 132 | end; 133 | 134 | procedure GetTelemetryDescription(var Alert: Record AlertSESTM): Text 135 | begin 136 | exit(Alert.ShortDescription); 137 | end; 138 | } 139 | -------------------------------------------------------------------------------- /.AL-Go/localDevEnv.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Script for creating local development environment 3 | # Please do not modify this script as it will be auto-updated from the AL-Go Template 4 | # Recommended approach is to use as is or add a script (freddyk-devenv.ps1), which calls this script with the user specific parameters 5 | # 6 | Param( 7 | [string] $containerName = "", 8 | [ValidateSet("UserPassword", "Windows")] 9 | [string] $auth = "", 10 | [pscredential] $credential = $null, 11 | [string] $licenseFileUrl = "", 12 | [switch] $fromVSCode, 13 | [switch] $accept_insiderEula, 14 | [switch] $clean 15 | ) 16 | 17 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 18 | 19 | function DownloadHelperFile { 20 | param( 21 | [string] $url, 22 | [string] $folder 23 | ) 24 | 25 | $prevProgressPreference = $ProgressPreference; $ProgressPreference = 'SilentlyContinue' 26 | $name = [System.IO.Path]::GetFileName($url) 27 | Write-Host "Downloading $name from $url" 28 | $path = Join-Path $folder $name 29 | Invoke-WebRequest -UseBasicParsing -uri $url -OutFile $path 30 | $ProgressPreference = $prevProgressPreference 31 | return $path 32 | } 33 | 34 | try { 35 | Clear-Host 36 | Write-Host 37 | Write-Host -ForegroundColor Yellow @' 38 | _ _ _____ ______ 39 | | | | | | __ \ | ____| 40 | | | ___ ___ __ _| | | | | | _____ __ |__ _ ____ __ 41 | | | / _ \ / __/ _` | | | | | |/ _ \ \ / / __| | '_ \ \ / / 42 | | |____ (_) | (__ (_| | | | |__| | __/\ V /| |____| | | \ V / 43 | |______\___/ \___\__,_|_| |_____/ \___| \_/ |______|_| |_|\_/ 44 | 45 | '@ 46 | 47 | $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" 48 | New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null 49 | $GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/Github-Helper.psm1' -folder $tmpFolder 50 | $ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/AL-Go-Helper.ps1' -folder $tmpFolder 51 | DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/Packages.json' -folder $tmpFolder | Out-Null 52 | 53 | Import-Module $GitHubHelperPath 54 | . $ALGoHelperPath -local 55 | 56 | $baseFolder = GetBaseFolder -folder $PSScriptRoot 57 | $project = GetProject -baseFolder $baseFolder -projectALGoFolder $PSScriptRoot 58 | 59 | Write-Host @' 60 | 61 | This script will create a docker based local development environment for your project. 62 | 63 | NOTE: You need to have Docker installed, configured and be able to create Business Central containers for this to work. 64 | If this fails, you can setup a cloud based development environment by running cloudDevEnv.ps1 65 | 66 | All apps and test apps will be compiled and published to the environment in the development scope. 67 | The script will also modify launch.json to have a Local Sandbox configuration point to your environment. 68 | 69 | '@ 70 | 71 | $settings = ReadSettings -baseFolder $baseFolder -project $project -userName $env:USERNAME -workflowName 'localDevEnv' 72 | 73 | Write-Host "Checking System Requirements" 74 | $dockerProcess = (Get-Process "dockerd" -ErrorAction Ignore) 75 | if (!($dockerProcess)) { 76 | Write-Host -ForegroundColor Red "Dockerd process not found. Docker might not be started, not installed or not running Windows Containers." 77 | } 78 | if ($settings.keyVaultName) { 79 | if (-not (Get-Module -ListAvailable -Name 'Az.KeyVault')) { 80 | Write-Host -ForegroundColor Red "A keyvault name is defined in Settings, you need to have the Az.KeyVault PowerShell module installed (use Install-Module az) or you can set the keyVaultName to an empty string in the user settings file ($($ENV:UserName).settings.json)." 81 | } 82 | } 83 | 84 | Write-Host 85 | 86 | if (-not $containerName) { 87 | $containerName = Enter-Value ` 88 | -title "Container name" ` 89 | -question "Please enter the name of the container to create" ` 90 | -default "bcserver" ` 91 | -trimCharacters @('"',"'",' ') 92 | } 93 | 94 | if (-not $auth) { 95 | $auth = Select-Value ` 96 | -title "Authentication mechanism for container" ` 97 | -options @{ "Windows" = "Windows Authentication"; "UserPassword" = "Username/Password authentication" } ` 98 | -question "Select authentication mechanism for container" ` 99 | -default "UserPassword" 100 | } 101 | 102 | if (-not $credential) { 103 | if ($auth -eq "Windows") { 104 | $credential = Get-Credential -Message "Please enter your Windows Credentials" -UserName $env:USERNAME 105 | $CurrentDomain = "LDAP://" + ([ADSI]"").distinguishedName 106 | $domain = New-Object System.DirectoryServices.DirectoryEntry($CurrentDomain,$credential.UserName,$credential.GetNetworkCredential().password) 107 | if ($null -eq $domain.name) { 108 | Write-Host -ForegroundColor Red "Unable to verify your Windows Credentials, you might not be able to authenticate to your container" 109 | } 110 | } 111 | else { 112 | $credential = Get-Credential -Message "Please enter username and password for your container" -UserName "admin" 113 | } 114 | } 115 | 116 | if (-not $licenseFileUrl) { 117 | if ($settings.type -eq "AppSource App") { 118 | $description = "When developing AppSource Apps for Business Central versions prior to 22, your local development environment needs the developer licensefile with permissions to your AppSource app object IDs" 119 | $default = "none" 120 | } 121 | else { 122 | $description = "When developing PTEs, you can optionally specify a developer licensefile with permissions to object IDs of your dependant apps" 123 | $default = "none" 124 | } 125 | 126 | $licenseFileUrl = Enter-Value ` 127 | -title "LicenseFileUrl" ` 128 | -description $description ` 129 | -question "Local path or a secure download URL to license file " ` 130 | -default $default ` 131 | -doNotConvertToLower ` 132 | -trimCharacters @('"',"'",' ') 133 | } 134 | 135 | if ($licenseFileUrl -eq "none") { 136 | $licenseFileUrl = "" 137 | } 138 | 139 | CreateDevEnv ` 140 | -kind local ` 141 | -caller local ` 142 | -containerName $containerName ` 143 | -baseFolder $baseFolder ` 144 | -project $project ` 145 | -auth $auth ` 146 | -credential $credential ` 147 | -licenseFileUrl $licenseFileUrl ` 148 | -accept_insiderEula:$accept_insiderEula ` 149 | -clean:$clean 150 | } 151 | catch { 152 | Write-Host -ForegroundColor Red "Error: $($_.Exception.Message)`nStacktrace: $($_.scriptStackTrace)" 153 | } 154 | finally { 155 | if ($fromVSCode) { 156 | Read-Host "Press ENTER to close this window" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.github/workflows/CreateOnlineDevelopmentEnvironment.yaml: -------------------------------------------------------------------------------- 1 | name: ' Create Online Dev. Environment' 2 | 3 | run-name: "Create Online Dev. Environment for [${{ github.ref_name }} / ${{ github.event.inputs.project }}]" 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | project: 9 | description: Project name if the repository is setup for multiple projects 10 | required: false 11 | default: '.' 12 | environmentName: 13 | description: Name of the online environment 14 | required: true 15 | reUseExistingEnvironment: 16 | description: Reuse environment if it exists? 17 | type: boolean 18 | default: false 19 | directCommit: 20 | description: Direct Commit? 21 | type: boolean 22 | default: false 23 | useGhTokenWorkflow: 24 | description: Use GhTokenWorkflow for PR/Commit? 25 | type: boolean 26 | default: false 27 | 28 | permissions: 29 | actions: read 30 | contents: write 31 | id-token: write 32 | pull-requests: write 33 | 34 | defaults: 35 | run: 36 | shell: pwsh 37 | 38 | env: 39 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 40 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 41 | 42 | jobs: 43 | Initialization: 44 | needs: [ ] 45 | runs-on: [ ubuntu-latest ] 46 | outputs: 47 | deviceCode: ${{ steps.authenticate.outputs.deviceCode }} 48 | githubRunner: ${{ steps.ReadSettings.outputs.GitHubRunnerJson }} 49 | githubRunnerShell: ${{ steps.ReadSettings.outputs.GitHubRunnerShell }} 50 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 51 | steps: 52 | - name: Dump Workflow Information 53 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 54 | with: 55 | shell: pwsh 56 | 57 | - name: Checkout 58 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 59 | 60 | - name: Initialize the workflow 61 | id: init 62 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 63 | with: 64 | shell: pwsh 65 | 66 | - name: Read settings 67 | id: ReadSettings 68 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 69 | with: 70 | shell: pwsh 71 | 72 | - name: Read secrets 73 | id: ReadSecrets 74 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 75 | with: 76 | shell: pwsh 77 | gitHubSecrets: ${{ toJson(secrets) }} 78 | getSecrets: 'adminCenterApiCredentials' 79 | 80 | - name: Check AdminCenterApiCredentials / Initiate Device Login (open to see code) 81 | id: authenticate 82 | run: | 83 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 84 | $settings = $env:Settings | ConvertFrom-Json 85 | if ('${{ fromJson(steps.ReadSecrets.outputs.Secrets).adminCenterApiCredentials }}') { 86 | Write-Host "AdminCenterApiCredentials provided in secret $($settings.adminCenterApiCredentialsSecretName)!" 87 | Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication." 88 | } 89 | else { 90 | Write-Host "AdminCenterApiCredentials not provided, initiating Device Code flow" 91 | $ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" 92 | $webClient = New-Object System.Net.WebClient 93 | $webClient.DownloadFile('https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/AL-Go-Helper.ps1', $ALGoHelperPath) 94 | . $ALGoHelperPath 95 | DownloadAndImportBcContainerHelper 96 | $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) 97 | Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)" 98 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" 99 | } 100 | 101 | CreateDevelopmentEnvironment: 102 | needs: [ Initialization ] 103 | runs-on: ${{ fromJson(needs.Initialization.outputs.githubRunner) }} 104 | defaults: 105 | run: 106 | shell: ${{ needs.Initialization.outputs.githubRunnerShell }} 107 | name: Create Development Environment 108 | env: 109 | deviceCode: ${{ needs.Initialization.outputs.deviceCode }} 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 113 | 114 | - name: Read settings 115 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 116 | with: 117 | shell: pwsh 118 | 119 | - name: Read secrets 120 | id: ReadSecrets 121 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 122 | with: 123 | shell: pwsh 124 | gitHubSecrets: ${{ toJson(secrets) }} 125 | getSecrets: 'adminCenterApiCredentials,TokenForPush' 126 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 127 | 128 | - name: Set AdminCenterApiCredentials 129 | id: SetAdminCenterApiCredentials 130 | run: | 131 | if ($env:deviceCode) { 132 | $adminCenterApiCredentials = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("{""deviceCode"":""$($env:deviceCode)""}")) 133 | } 134 | else { 135 | $adminCenterApiCredentials = '${{ fromJson(steps.ReadSecrets.outputs.Secrets).adminCenterApiCredentials }}' 136 | } 137 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -value "adminCenterApiCredentials=$adminCenterApiCredentials" 138 | 139 | - name: Create Development Environment 140 | uses: microsoft/AL-Go-Actions/CreateDevelopmentEnvironment@v7.0 141 | with: 142 | shell: pwsh 143 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 144 | environmentName: ${{ github.event.inputs.environmentName }} 145 | project: ${{ github.event.inputs.project }} 146 | reUseExistingEnvironment: ${{ github.event.inputs.reUseExistingEnvironment }} 147 | directCommit: ${{ github.event.inputs.directCommit }} 148 | adminCenterApiCredentials: ${{ steps.SetAdminCenterApiCredentials.outputs.adminCenterApiCredentials }} 149 | 150 | - name: Finalize the workflow 151 | if: always() 152 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 153 | env: 154 | GITHUB_TOKEN: ${{ github.token }} 155 | with: 156 | shell: pwsh 157 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 158 | currentJobContext: ${{ toJson(job) }} 159 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/Alert.Table.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | using STM.BusinessCentral.Sentinel; 4 | 5 | /// 6 | /// This table is used to store all the alerts that are generated by the system 7 | /// 8 | table 71180275 AlertSESTM 9 | { 10 | Access = Internal; 11 | Caption = 'Alert'; 12 | DataCaptionFields = AlertCode; 13 | DataClassification = SystemMetadata; 14 | DrillDownPageId = AlertListSESTM; 15 | Extensible = false; 16 | LookupPageId = AlertListSESTM; 17 | Permissions = 18 | tabledata AlertSESTM = RID, 19 | tabledata IgnoredAlertsSESTM = RID, 20 | tabledata SentinelRuleSetSESTM = R, 21 | tabledata SentinelSetup = R; 22 | 23 | fields 24 | { 25 | field(1; Id; BigInteger) 26 | { 27 | Caption = 'ID'; 28 | NotBlank = true; 29 | ToolTip = 'The unique identifier for the alert.'; 30 | 31 | } 32 | field(2; AlertCode; Enum "AlertCodeSESTM") 33 | { 34 | Caption = 'Code'; 35 | 36 | NotBlank = true; 37 | ToolTip = 'The code representing the type of alert.'; 38 | } 39 | field(3; ShortDescription; Text[100]) 40 | { 41 | Caption = 'Short Description'; 42 | ToolTip = 'A brief description of the alert.'; 43 | 44 | } 45 | field(4; LongDescription; Text[2048]) 46 | { 47 | Caption = 'Long Description'; 48 | ToolTip = 'A detailed description of the alert.'; 49 | 50 | } 51 | field(5; Severity; Enum SeveritySESTM) 52 | { 53 | Caption = 'Severity'; 54 | ToolTip = 'The severity level of the alert.'; 55 | 56 | } 57 | field(6; ActionRecommendation; Text[1024]) 58 | { 59 | Caption = 'Action Recommendation'; 60 | ToolTip = 'Recommended actions to take in response to the alert.'; 61 | 62 | } 63 | field(7; "Area"; Enum AreaSESTM) 64 | { 65 | Caption = 'Area'; 66 | ToolTip = 'The area or module where the alert is relevant.'; 67 | 68 | } 69 | 70 | /// 71 | /// This is the unique Guid for a specific warning per Alert Code, not a an ID for the Alert Code. 72 | /// Its used to allow the user to mark a warning as read 73 | /// 74 | field(8; UniqueIdentifier; Text[100]) 75 | { 76 | AllowInCustomizations = Always; 77 | Caption = 'Unique Identifier'; 78 | ToolTip = 'A unique identifier for a specific warning per alert code.'; 79 | } 80 | field(9; Ignore; Boolean) 81 | { 82 | CalcFormula = exist(IgnoredAlertsSESTM where(AlertCode = field(AlertCode), UniqueIdentifier = field(UniqueIdentifier))); 83 | Caption = 'Ignore'; 84 | Editable = false; 85 | FieldClass = FlowField; 86 | ToolTip = 'Indicates whether the alert should be ignored.'; 87 | } 88 | } 89 | 90 | keys 91 | { 92 | key(PK; Id) 93 | { 94 | Clustered = true; 95 | } 96 | key(UniqueId; AlertCode, UniqueIdentifier) { } 97 | } 98 | 99 | trigger OnInsert() 100 | var 101 | Setup: Record SentinelSetup; 102 | begin 103 | if not NumberSequence.Exists('BCSentinelSESTMAlertId') then 104 | NumberSequence.Insert('BCSentinelSESTMAlertId'); 105 | 106 | Rec.Id := NumberSequence.Next('BCSentinelSESTMAlertId'); 107 | 108 | if Setup.GetTelemetryLoggingSetting(Rec.AlertCode) = Setup.TelemetryLogging::OnRuleLogging then 109 | this.LogUsage(); 110 | end; 111 | 112 | procedure FindNewAlerts() 113 | var 114 | currOrdinal: Integer; 115 | Alert: Interface IAuditAlertSESTM; 116 | AlertsToRun: List of [Interface IAuditAlertSESTM]; 117 | begin 118 | foreach currOrdinal in Enum::AlertCodeSESTM.Ordinals() do 119 | AlertsToRun.Add(Enum::AlertCodeSESTM.FromInteger(currOrdinal)); 120 | 121 | // TODO: add event to allow other extensions to add Alerts 122 | 123 | foreach Alert in AlertsToRun do 124 | Alert.CreateAlerts(); 125 | 126 | if not Rec.FindFirst() then 127 | ; // Move to the first record, after Alert creation. If no alerts where created, do nothing 128 | end; 129 | 130 | procedure SetToIgnore() 131 | var 132 | IgnoredAlerts: Record IgnoredAlertsSESTM; 133 | begin 134 | if Rec.Ignore then 135 | exit; 136 | 137 | IgnoredAlerts.Validate(AlertCode, Rec.AlertCode); 138 | IgnoredAlerts.Validate(UniqueIdentifier, Rec.UniqueIdentifier); 139 | if not IgnoredAlerts.Insert(true) then 140 | exit; 141 | end; 142 | 143 | procedure ClearIgnore() 144 | var 145 | IgnoredAlerts: Record IgnoredAlertsSESTM; 146 | begin 147 | if not Rec.Ignore then 148 | exit; 149 | 150 | IgnoredAlerts.SetRange(AlertCode, Rec.AlertCode); 151 | IgnoredAlerts.SetRange(UniqueIdentifier, Rec.UniqueIdentifier); 152 | if not IgnoredAlerts.IsEmpty() then 153 | IgnoredAlerts.DeleteAll(true); 154 | end; 155 | 156 | procedure FullRerun() 157 | var 158 | Alert: Record AlertSESTM; 159 | begin 160 | Alert.ClearAllAlerts(); 161 | Commit(); // Commit the transaction to ensure that the alerts are deleted before they are recreated 162 | Alert.FindNewAlerts(); 163 | end; 164 | 165 | procedure ClearAllAlerts() 166 | begin 167 | Rec.DeleteAll(true); 168 | 169 | if NumberSequence.Exists('BCSentinelSESTMAlertId') then 170 | NumberSequence.Restart('BCSentinelSESTMAlertId'); 171 | end; 172 | 173 | /// 174 | /// This function is used to create a new alert in the system. It will only create the alert if it does not already exist. 175 | /// 176 | /// The alert code to create the alert for. 177 | /// A brief description of what the issue is. 178 | /// The severity of the alert 179 | /// The area of the alert. 180 | /// A longer description of what the issue is. 181 | /// A description of how to resolve the issue. 182 | /// Any value that can distinguish different alerts within the same Alert code. 183 | procedure New(AlertCodeIn: Enum "AlertCodeSESTM"; ShortDescriptionIn: Text; SeverityIn: Enum SeveritySESTM; AreaIn: Enum AreaSESTM; LongDescriptionIn: Text; ActionRecommendationIn: Text; UniqueIdentifierIn: Text[100]) 184 | var 185 | Alert: Record AlertSESTM; 186 | SentinelRuleSet: Record SentinelRuleSetSESTM; 187 | CurrSeverity: Enum SeveritySESTM; 188 | begin 189 | SentinelRuleSet.ReadIsolation(IsolationLevel::ReadUncommitted); 190 | 191 | if SentinelRuleSet.Get(AlertCodeIn) and (SentinelRuleSet.Severity <> SentinelRuleSet.Severity::" ") then 192 | CurrSeverity := SentinelRuleSet.Severity 193 | else 194 | CurrSeverity := SeverityIn; 195 | 196 | if CurrSeverity = Severity::Disabled then 197 | exit; 198 | 199 | Alert.SetRange(AlertCode, AlertCodeIn); 200 | Alert.SetRange("UniqueIdentifier", UniqueIdentifierIn); 201 | Alert.ReadIsolation(IsolationLevel::ReadUncommitted); 202 | if not Alert.IsEmpty() then 203 | exit; 204 | 205 | Alert.Validate(AlertCode, AlertCodeIn); 206 | Alert.Validate(ShortDescription, CopyStr(ShortDescriptionIn, 1, MaxStrLen(Alert.ShortDescription))); 207 | Alert.Validate(Severity, CurrSeverity); 208 | Alert.Validate("Area", AreaIn); 209 | Alert.Validate(LongDescription, CopyStr(LongDescriptionIn, 1, MaxStrLen(Alert.LongDescription))); 210 | Alert.Validate(ActionRecommendation, CopyStr(ActionRecommendationIn, 1, MaxStrLen(Alert.ActionRecommendation))); 211 | Alert.Validate(UniqueIdentifier, UniqueIdentifierIn); 212 | Alert.Insert(true); 213 | end; 214 | 215 | internal procedure LogUsage() 216 | var 217 | TelemetryHelper: Codeunit TelemetryHelperSESTM; 218 | CustomDimensions: Dictionary of [Text, Text]; 219 | Alert: Interface IAuditAlertSESTM; 220 | begin 221 | CustomDimensions.Add('AlertSeverity', Format(Rec.Severity)); 222 | CustomDimensions.Add('AlertArea', Format(Rec."Area")); 223 | Rec.CalcFields(Ignore); 224 | CustomDimensions.Add('AlertIgnore', Format(Rec.Ignore)); 225 | 226 | Alert := Rec.AlertCode; 227 | Alert.AddCustomTelemetryDimensions(Rec, CustomDimensions); 228 | 229 | TelemetryHelper.LogUsage(Rec.AlertCode, Alert.GetTelemetryDescription(Rec), CustomDimensions); 230 | end; 231 | } -------------------------------------------------------------------------------- /.github/workflows/PublishToEnvironment.yaml: -------------------------------------------------------------------------------- 1 | name: ' Publish To Environment' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | appVersion: 7 | description: App version to deploy to environment(s) (current, prerelease, draft, latest, version number or PR_) 8 | required: false 9 | default: 'current' 10 | environmentName: 11 | description: Environment mask to receive the new version (* for all, PROD* for all environments starting with PROD) 12 | required: true 13 | 14 | permissions: 15 | actions: read 16 | contents: read 17 | id-token: write 18 | 19 | defaults: 20 | run: 21 | shell: pwsh 22 | 23 | env: 24 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 25 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 26 | 27 | jobs: 28 | Initialization: 29 | needs: [ ] 30 | runs-on: [ ubuntu-latest ] 31 | outputs: 32 | environmentsMatrixJson: ${{ steps.DetermineDeploymentEnvironments.outputs.EnvironmentsMatrixJson }} 33 | environmentCount: ${{ steps.DetermineDeploymentEnvironments.outputs.EnvironmentCount }} 34 | deploymentEnvironmentsJson: ${{ steps.DetermineDeploymentEnvironments.outputs.DeploymentEnvironmentsJson }} 35 | deviceCode: ${{ steps.Authenticate.outputs.deviceCode }} 36 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 37 | steps: 38 | - name: Dump Workflow Information 39 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 40 | with: 41 | shell: pwsh 42 | 43 | - name: Checkout 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | 46 | - name: Initialize the workflow 47 | id: init 48 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 49 | with: 50 | shell: pwsh 51 | 52 | - name: Read settings 53 | id: ReadSettings 54 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 55 | with: 56 | shell: pwsh 57 | 58 | - name: Determine Deployment Environments 59 | id: DetermineDeploymentEnvironments 60 | uses: microsoft/AL-Go-Actions/DetermineDeploymentEnvironments@v7.0 61 | env: 62 | GITHUB_TOKEN: ${{ github.token }} 63 | with: 64 | shell: pwsh 65 | getEnvironments: ${{ github.event.inputs.environmentName }} 66 | type: 'Publish' 67 | 68 | - name: EnvName 69 | id: envName 70 | if: steps.DetermineDeploymentEnvironments.outputs.UnknownEnvironment == 1 71 | run: | 72 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 73 | $envName = '${{ fromJson(steps.DetermineDeploymentEnvironments.outputs.environmentsMatrixJson).matrix.include[0].environment }}'.split(' ')[0] 74 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "envName=$envName" 75 | 76 | - name: Read secrets 77 | id: ReadSecrets 78 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 79 | if: steps.DetermineDeploymentEnvironments.outputs.UnknownEnvironment == 1 80 | with: 81 | shell: pwsh 82 | gitHubSecrets: ${{ toJson(secrets) }} 83 | getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' 84 | 85 | - name: Authenticate 86 | id: Authenticate 87 | if: steps.DetermineDeploymentEnvironments.outputs.UnknownEnvironment == 1 88 | run: | 89 | $envName = '${{ steps.envName.outputs.envName }}' 90 | $secretName = '' 91 | $secrets = '${{ steps.ReadSecrets.outputs.Secrets }}' | ConvertFrom-Json 92 | $authContext = $null 93 | "$($envName)-AuthContext", "$($envName)_AuthContext", "AuthContext" | ForEach-Object { 94 | if (!($authContext)) { 95 | if ($secrets."$_") { 96 | Write-Host "Using $_ secret as AuthContext" 97 | $authContext = $secrets."$_" 98 | $secretName = $_ 99 | } 100 | } 101 | } 102 | if ($authContext) { 103 | Write-Host "AuthContext provided in secret $secretName!" 104 | Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication." 105 | } 106 | else { 107 | Write-Host "No AuthContext provided for $envName, initiating Device Code flow" 108 | $ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" 109 | $webClient = New-Object System.Net.WebClient 110 | $webClient.DownloadFile('https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v7.0/AL-Go-Helper.ps1', $ALGoHelperPath) 111 | . $ALGoHelperPath 112 | DownloadAndImportBcContainerHelper 113 | $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) 114 | Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)" 115 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" 116 | } 117 | 118 | Deploy: 119 | needs: [ Initialization ] 120 | if: needs.Initialization.outputs.environmentCount > 0 121 | strategy: ${{ fromJson(needs.Initialization.outputs.environmentsMatrixJson) }} 122 | runs-on: ${{ fromJson(matrix.os) }} 123 | name: Deploy to ${{ matrix.environment }} 124 | defaults: 125 | run: 126 | shell: ${{ matrix.shell }} 127 | environment: 128 | name: ${{ matrix.environment }} 129 | url: ${{ steps.Deploy.outputs.environmentUrl }} 130 | env: 131 | deviceCode: ${{ needs.Initialization.outputs.deviceCode }} 132 | steps: 133 | - name: Checkout 134 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 135 | 136 | - name: EnvName 137 | id: envName 138 | run: | 139 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 140 | $envName = '${{ matrix.environment }}'.split(' ')[0] 141 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "envName=$envName" 142 | 143 | - name: Read settings 144 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 145 | with: 146 | shell: ${{ matrix.shell }} 147 | get: type,powerPlatformSolutionFolder 148 | 149 | - name: Read secrets 150 | id: ReadSecrets 151 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 152 | with: 153 | shell: ${{ matrix.shell }} 154 | gitHubSecrets: ${{ toJson(secrets) }} 155 | getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' 156 | 157 | - name: Get Artifacts for deployment 158 | uses: microsoft/AL-Go-Actions/GetArtifactsForDeployment@v7.0 159 | with: 160 | shell: ${{ matrix.shell }} 161 | artifactsVersion: ${{ github.event.inputs.appVersion }} 162 | artifactsFolder: '.artifacts' 163 | 164 | - name: Deploy to Business Central 165 | id: Deploy 166 | uses: microsoft/AL-Go-Actions/Deploy@v7.0 167 | env: 168 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 169 | with: 170 | shell: ${{ matrix.shell }} 171 | environmentName: ${{ matrix.environment }} 172 | artifactsFolder: '.artifacts' 173 | type: 'Publish' 174 | deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} 175 | artifactsVersion: ${{ github.event.inputs.appVersion }} 176 | 177 | - name: Deploy to Power Platform 178 | if: env.type == 'PTE' && env.powerPlatformSolutionFolder != '' 179 | uses: microsoft/AL-Go-Actions/DeployPowerPlatform@v7.0 180 | env: 181 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 182 | with: 183 | shell: ${{ matrix.shell }} 184 | environmentName: ${{ matrix.environment }} 185 | artifactsFolder: '.artifacts' 186 | deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} 187 | 188 | PostProcess: 189 | needs: [ Initialization, Deploy ] 190 | if: always() 191 | runs-on: [ ubuntu-latest ] 192 | steps: 193 | - name: Checkout 194 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 195 | 196 | - name: Finalize the workflow 197 | id: PostProcess 198 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 199 | env: 200 | GITHUB_TOKEN: ${{ github.token }} 201 | with: 202 | shell: pwsh 203 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 204 | currentJobContext: ${{ toJson(job) }} 205 | -------------------------------------------------------------------------------- /BusinessCentral.Sentinel/src/AlertList.Page.al: -------------------------------------------------------------------------------- 1 | namespace STM.BusinessCentral.Sentinel; 2 | 3 | page 71180275 AlertListSESTM 4 | { 5 | AboutText = 'This page shows a list of all alerts that have been generated by the system. Alerts are generated when the system detects an issue that requires attention.'; 6 | AboutTitle = 'Business Central Sentinel: Alert List'; 7 | AdditionalSearchTerms = 'Sentinel'; 8 | ApplicationArea = All; 9 | Caption = 'Sentinel Alerts'; 10 | CardPageId = AlertCard; 11 | Editable = false; 12 | Extensible = false; 13 | PageType = List; 14 | SourceTable = AlertSESTM; 15 | UsageCategory = Lists; 16 | 17 | layout 18 | { 19 | area(Content) 20 | { 21 | group(Instructions) 22 | { 23 | Caption = 'Instructions'; 24 | 25 | field(Usage; this.InstructionsLbl) 26 | { 27 | Editable = false; 28 | MultiLine = true; 29 | ShowCaption = false; 30 | } 31 | } 32 | repeater(Group) 33 | { 34 | field(Id; Rec.Id) 35 | { 36 | Visible = false; 37 | } 38 | field("Code"; Rec.AlertCode) 39 | { 40 | AboutText = 'The code representing the type of alert. You may see more than one alert with the same code if the same issue is detected multiple times.'; 41 | AboutTitle = 'Alert Code'; 42 | StyleExpr = this.SeverityStyle; 43 | } 44 | field("Short Description"; Rec."ShortDescription") 45 | { 46 | AboutText = 'A brief description of the alert. Click on the field to see the full explanation.'; 47 | AboutTitle = 'Short Description'; 48 | } 49 | field(Severity; Rec.Severity) { } 50 | field("Area"; Rec."Area") { } 51 | field(Ignore; Rec.Ignore) 52 | { 53 | AboutText = 'Indicates whether the alert has been marked as ignored. Ignored alerts will be excluded from reports and queues on the Role Center.'; 54 | AboutTitle = 'Ignore'; 55 | } 56 | } 57 | } 58 | } 59 | 60 | actions 61 | { 62 | area(Processing) 63 | { 64 | action(RunAnalysis) 65 | { 66 | AboutText = 'Run the analysis of the current environment, and check for new alerts.'; 67 | AboutTitle = 'Run Analysis'; 68 | Caption = 'Run Analysis'; 69 | Image = Suggest; 70 | ToolTip = 'Run the analysis of the current environment, and check for new alerts.'; 71 | 72 | trigger OnAction() 73 | begin 74 | Rec.FindNewAlerts(); 75 | end; 76 | } 77 | action(SetToIgnore) 78 | { 79 | AboutText = 'Flags the alert as ignored. Ignored alerts will be excluded from reports and queues on the Role Center.'; 80 | AboutTitle = 'Ignore'; 81 | Caption = 'Ignore'; 82 | Image = Delete; 83 | ToolTip = 'Ignore this alert.'; 84 | 85 | trigger OnAction() 86 | begin 87 | Rec.SetToIgnore(); 88 | end; 89 | } 90 | action(ClearIgnore) 91 | { 92 | Caption = 'Stop Ignoring'; 93 | Image = Restore; 94 | ToolTip = 'Clear the ignore status of this alert.'; 95 | 96 | trigger OnAction() 97 | begin 98 | Rec.ClearIgnore(); 99 | end; 100 | } 101 | action(ClearAllAlerts) 102 | { 103 | AboutText = 'Clear all alerts that have been generated by the system. It will save which alerts have been ignored. But lets you start fresh to remove resolved alerts.'; 104 | AboutTitle = 'Clear All Alerts'; 105 | Caption = 'Clear All Alerts'; 106 | Image = Delete; 107 | ToolTip = 'Clear all alerts.'; 108 | 109 | trigger OnAction() 110 | begin 111 | Rec.ClearAllAlerts(); 112 | end; 113 | } 114 | action(FullReRun) 115 | { 116 | AboutText = 'Remove all alerts and re-runs the analysis of the current environment.'; 117 | AboutTitle = 'Full Re-Run'; 118 | Caption = 'Full Re-Run'; 119 | Image = Suggest; 120 | ToolTip = 'Run the analysis of the current environment, and check for new alerts.'; 121 | 122 | trigger OnAction() 123 | begin 124 | Rec.FullRerun(); 125 | end; 126 | } 127 | action(MoreDetails) 128 | { 129 | Caption = 'More Details'; 130 | Ellipsis = true; 131 | Image = LaunchWeb; 132 | Scope = Repeater; 133 | ToolTip = 'Show more details about this alert.'; 134 | 135 | trigger OnAction() 136 | var 137 | IAuditAlert: Interface IAuditAlertSESTM; 138 | begin 139 | IAuditAlert := Rec.AlertCode; 140 | IAuditAlert.ShowMoreDetails(Rec); 141 | end; 142 | } 143 | action(AutoFix) 144 | { 145 | Caption = 'Auto Fix'; 146 | Ellipsis = true; 147 | Image = Action; 148 | Scope = Repeater; 149 | ToolTip = 'Automatically fix the issue that caused this alert.'; 150 | 151 | trigger OnAction() 152 | var 153 | IAuditAlert: Interface IAuditAlertSESTM; 154 | begin 155 | IAuditAlert := Rec.AlertCode; 156 | IAuditAlert.AutoFix(Rec); 157 | end; 158 | } 159 | action(ShowRelatedInformation) 160 | { 161 | Caption = 'Show Related Information'; 162 | Ellipsis = true; 163 | Image = ViewDetails; 164 | Scope = Repeater; 165 | ToolTip = 'Show related information about this alert.'; 166 | 167 | trigger OnAction() 168 | var 169 | IAuditAlert: Interface IAuditAlertSESTM; 170 | begin 171 | IAuditAlert := Rec.AlertCode; 172 | IAuditAlert.ShowRelatedInformation(Rec); 173 | end; 174 | } 175 | } 176 | area(Navigation) 177 | { 178 | action(RuleSet) 179 | { 180 | Caption = 'Rule Set'; 181 | Image = CheckRulesSyntax; 182 | RunObject = page SentinelRuleSetSESTM; 183 | ToolTip = 'Open the Rule Set page to view and manage the rules that generate alerts. You can change the severity of the alerts, or disable them.'; 184 | } 185 | } 186 | area(Promoted) 187 | { 188 | group(Run) 189 | { 190 | ShowAs = SplitButton; 191 | actionref(RunAnalysis_Promoted; RunAnalysis) { } 192 | actionref(ClearAllAlerts_Promoted; ClearAllAlerts) { } 193 | actionref(FullReRun_Promoted; FullReRun) { } 194 | } 195 | group(Ignore_promoted) 196 | { 197 | ShowAs = SplitButton; 198 | actionref(SetToIgnore_Promoted; SetToIgnore) { } 199 | actionref(ClearIgnore_Promoted; ClearIgnore) { } 200 | } 201 | actionref(MoreDetails_Promoted; MoreDetails) { } 202 | actionref(ShowRelatedInformation_Promoted; ShowRelatedInformation) { } 203 | actionref(AutoFix_Promoted; AutoFix) { } 204 | } 205 | } 206 | 207 | var 208 | InstructionsLbl: Label 'This page shows a list of all alerts that have been found for your environment.\Its important to understand that those alerts are recommendations and should be reviewed before taking any action.\Not all alerts may be relevant to your environment or your business processes.\\You can collapse this message by a click on "Instructions" above.'; 209 | SeverityStyle: Text; 210 | 211 | trigger OnOpenPage() 212 | begin 213 | Rec.SetRange(Ignore, false); 214 | end; 215 | 216 | trigger OnAfterGetRecord() 217 | begin 218 | case Rec.Severity of 219 | SeveritySESTM::Info: 220 | this.SeverityStyle := Format(PageStyle::StandardAccent); 221 | SeveritySESTM::Warning: 222 | this.SeverityStyle := Format(PageStyle::Ambiguous); 223 | SeveritySESTM::Error: 224 | this.SeverityStyle := Format(PageStyle::Attention); 225 | SeveritySESTM::Critical: 226 | this.SeverityStyle := Format(PageStyle::Unfavorable); 227 | end; 228 | end; 229 | 230 | 231 | } -------------------------------------------------------------------------------- /.github/workflows/_BuildALGoProject.yaml: -------------------------------------------------------------------------------- 1 | name: '_Build AL-Go project' 2 | 3 | run-name: 'Build ${{ inputs.project }}' 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | shell: 9 | description: Shell in which you want to run the action (powershell or pwsh) 10 | required: false 11 | default: powershell 12 | type: string 13 | runsOn: 14 | description: JSON-formatted string of the types of machine to run the build job on 15 | required: true 16 | type: string 17 | checkoutRef: 18 | description: Ref to checkout 19 | required: false 20 | default: ${{ github.sha }} 21 | type: string 22 | project: 23 | description: Name of the built project 24 | required: true 25 | type: string 26 | projectName: 27 | description: Friendly name of the built project 28 | required: true 29 | type: string 30 | skippedProjectsJson: 31 | description: An array of AL-Go projects to skip in compressed JSON format 32 | required: false 33 | default: '[]' 34 | type: string 35 | projectDependenciesJson: 36 | description: Dependencies of the built project in compressed Json format 37 | required: false 38 | default: '{}' 39 | type: string 40 | buildMode: 41 | description: Build mode used when building the artifacts 42 | required: true 43 | type: string 44 | baselineWorkflowRunId: 45 | description: ID of the baseline workflow run, from where to download the current project dependencies, in case they are not built in the current workflow run 46 | required: false 47 | default: '0' 48 | type: string 49 | baselineWorkflowSHA: 50 | description: SHA of the baseline workflow run 51 | required: false 52 | default: '' 53 | type: string 54 | secrets: 55 | description: A comma-separated string with the names of the secrets, required for the workflow. 56 | required: false 57 | default: '' 58 | type: string 59 | artifactsRetentionDays: 60 | description: Number of days to keep the artifacts 61 | type: number 62 | default: 0 63 | artifactsNameSuffix: 64 | description: Suffix to add to the artifacts names 65 | required: false 66 | default: '' 67 | type: string 68 | signArtifacts: 69 | description: Flag indicating whether the apps should be signed 70 | type: boolean 71 | default: false 72 | useArtifactCache: 73 | description: Flag determining whether to use the Artifacts Cache 74 | type: boolean 75 | default: false 76 | 77 | permissions: 78 | actions: read 79 | contents: read 80 | id-token: write 81 | 82 | env: 83 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 84 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 85 | 86 | jobs: 87 | BuildALGoProject: 88 | needs: [ ] 89 | runs-on: ${{ fromJson(inputs.runsOn) }} 90 | defaults: 91 | run: 92 | shell: ${{ inputs.shell }} 93 | name: ${{ inputs.projectName }} (${{ inputs.buildMode }}) 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 97 | with: 98 | ref: ${{ inputs.checkoutRef }} 99 | lfs: true 100 | 101 | - name: Read settings 102 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 103 | with: 104 | shell: ${{ inputs.shell }} 105 | project: ${{ inputs.project }} 106 | buildMode: ${{ inputs.buildMode }} 107 | get: useCompilerFolder,keyVaultCodesignCertificateName,doNotSignApps,doNotRunTests,doNotRunBcptTests,doNotRunpageScriptingTests,artifact,generateDependencyArtifact,trustedSigning,useGitSubmodules 108 | 109 | - name: Determine whether to build project 110 | id: DetermineBuildProject 111 | uses: microsoft/AL-Go-Actions/DetermineBuildProject@v7.0 112 | with: 113 | shell: ${{ inputs.shell }} 114 | skippedProjectsJson: ${{ inputs.skippedProjectsJson }} 115 | project: ${{ inputs.project }} 116 | baselineWorkflowRunId: ${{ inputs.baselineWorkflowRunId }} 117 | 118 | - name: Read secrets 119 | id: ReadSecrets 120 | if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && github.event_name != 'pull_request' 121 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 122 | with: 123 | shell: ${{ inputs.shell }} 124 | gitHubSecrets: ${{ toJson(secrets) }} 125 | getSecrets: '${{ inputs.secrets }},appDependencySecrets,AZURE_CREDENTIALS,-gitSubmodulesToken' 126 | 127 | - name: Checkout Submodules 128 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 129 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 130 | with: 131 | ref: ${{ inputs.checkoutRef }} 132 | lfs: true 133 | submodules: ${{ env.useGitSubmodules }} 134 | token: '${{ fromJson(steps.ReadSecrets.outputs.Secrets).gitSubmodulesToken }}' 135 | 136 | - name: Determine ArtifactUrl 137 | id: determineArtifactUrl 138 | if: steps.DetermineBuildProject.outputs.BuildIt == 'True' 139 | uses: microsoft/AL-Go-Actions/DetermineArtifactUrl@v7.0 140 | with: 141 | shell: ${{ inputs.shell }} 142 | project: ${{ inputs.project }} 143 | 144 | - name: Cache Business Central Artifacts 145 | if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && env.useCompilerFolder == 'True' && inputs.useArtifactCache && env.artifactCacheKey 146 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 147 | with: 148 | path: .artifactcache 149 | key: ${{ env.artifactCacheKey }} 150 | 151 | - name: Download Project Dependencies 152 | id: DownloadProjectDependencies 153 | if: steps.DetermineBuildProject.outputs.BuildIt == 'True' 154 | uses: microsoft/AL-Go-Actions/DownloadProjectDependencies@v7.0 155 | env: 156 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 157 | with: 158 | shell: ${{ inputs.shell }} 159 | project: ${{ inputs.project }} 160 | buildMode: ${{ inputs.buildMode }} 161 | projectDependenciesJson: ${{ inputs.projectDependenciesJson }} 162 | baselineWorkflowRunId: ${{ inputs.baselineWorkflowRunId }} 163 | 164 | - name: Build 165 | uses: microsoft/AL-Go-Actions/RunPipeline@v7.0 166 | if: steps.DetermineBuildProject.outputs.BuildIt == 'True' 167 | env: 168 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 169 | BuildMode: ${{ inputs.buildMode }} 170 | with: 171 | shell: ${{ inputs.shell }} 172 | artifact: ${{ env.artifact }} 173 | project: ${{ inputs.project }} 174 | buildMode: ${{ inputs.buildMode }} 175 | installAppsJson: ${{ steps.DownloadProjectDependencies.outputs.DownloadedApps }} 176 | installTestAppsJson: ${{ steps.DownloadProjectDependencies.outputs.DownloadedTestApps }} 177 | baselineWorkflowRunId: ${{ inputs.baselineWorkflowRunId }} 178 | baselineWorkflowSHA: ${{ inputs.baselineWorkflowSHA }} 179 | 180 | - name: Sign 181 | id: sign 182 | if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && inputs.signArtifacts && env.doNotSignApps == 'False' && (env.keyVaultCodesignCertificateName != '' || (fromJson(env.trustedSigning).Endpoint != '' && fromJson(env.trustedSigning).Account != '' && fromJson(env.trustedSigning).CertificateProfile != '')) 183 | uses: microsoft/AL-Go-Actions/Sign@v7.0 184 | with: 185 | shell: ${{ inputs.shell }} 186 | azureCredentialsJson: '${{ fromJson(steps.ReadSecrets.outputs.Secrets).AZURE_CREDENTIALS }}' 187 | pathToFiles: '${{ inputs.project }}/.buildartifacts/Apps/*.app' 188 | 189 | - name: Calculate Artifact names 190 | id: calculateArtifactsNames 191 | uses: microsoft/AL-Go-Actions/CalculateArtifactNames@v7.0 192 | if: success() || failure() 193 | with: 194 | shell: ${{ inputs.shell }} 195 | project: ${{ inputs.project }} 196 | buildMode: ${{ inputs.buildMode }} 197 | suffix: ${{ inputs.artifactsNameSuffix }} 198 | 199 | - name: Publish artifacts - apps 200 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 201 | if: inputs.artifactsRetentionDays >= 0 && (hashFiles(format('{0}/.buildartifacts/Apps/*',inputs.project)) != '') 202 | with: 203 | name: ${{ steps.calculateArtifactsNames.outputs.AppsArtifactsName }} 204 | path: '${{ inputs.project }}/.buildartifacts/Apps/' 205 | if-no-files-found: ignore 206 | retention-days: ${{ inputs.artifactsRetentionDays }} 207 | 208 | - name: Publish artifacts - dependencies 209 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 210 | if: inputs.artifactsRetentionDays >= 0 && env.generateDependencyArtifact == 'True' && (hashFiles(format('{0}/.buildartifacts/Dependencies/*',inputs.project)) != '') 211 | with: 212 | name: ${{ steps.calculateArtifactsNames.outputs.DependenciesArtifactsName }} 213 | path: '${{ inputs.project }}/.buildartifacts/Dependencies/' 214 | if-no-files-found: ignore 215 | retention-days: ${{ inputs.artifactsRetentionDays }} 216 | 217 | - name: Publish artifacts - test apps 218 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 219 | if: inputs.artifactsRetentionDays >= 0 && (hashFiles(format('{0}/.buildartifacts/TestApps/*',inputs.project)) != '') 220 | with: 221 | name: ${{ steps.calculateArtifactsNames.outputs.TestAppsArtifactsName }} 222 | path: '${{ inputs.project }}/.buildartifacts/TestApps/' 223 | if-no-files-found: ignore 224 | retention-days: ${{ inputs.artifactsRetentionDays }} 225 | 226 | - name: Publish artifacts - build output 227 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 228 | if: (success() || failure()) && (hashFiles(format('{0}/BuildOutput.txt',inputs.project)) != '') 229 | with: 230 | name: ${{ steps.calculateArtifactsNames.outputs.BuildOutputArtifactsName }} 231 | path: '${{ inputs.project }}/BuildOutput.txt' 232 | if-no-files-found: ignore 233 | 234 | - name: Publish artifacts - container event log 235 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 236 | if: (failure()) && (hashFiles(format('{0}/ContainerEventLog.evtx',inputs.project)) != '') 237 | with: 238 | name: ${{ steps.calculateArtifactsNames.outputs.ContainerEventLogArtifactsName }} 239 | path: '${{ inputs.project }}/ContainerEventLog.evtx' 240 | if-no-files-found: ignore 241 | 242 | - name: Publish artifacts - test results 243 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 244 | if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/TestResults.xml',inputs.project)) != '') 245 | with: 246 | name: ${{ steps.calculateArtifactsNames.outputs.TestResultsArtifactsName }} 247 | path: '${{ inputs.project }}/.buildartifacts/TestResults.xml' 248 | if-no-files-found: ignore 249 | 250 | - name: Publish artifacts - bcpt test results 251 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 252 | if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/bcptTestResults.json',inputs.project)) != '') 253 | with: 254 | name: ${{ steps.calculateArtifactsNames.outputs.BcptTestResultsArtifactsName }} 255 | path: '${{ inputs.project }}/.buildartifacts/bcptTestResults.json' 256 | if-no-files-found: ignore 257 | 258 | - name: Publish artifacts - page scripting test results 259 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 260 | if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/PageScriptingTestResults.xml',inputs.project)) != '') 261 | with: 262 | name: ${{ steps.calculateArtifactsNames.outputs.PageScriptingTestResultsArtifactsName }} 263 | path: '${{ inputs.project }}/.buildartifacts/PageScriptingTestResults.xml' 264 | if-no-files-found: ignore 265 | 266 | - name: Publish artifacts - page scripting test result details 267 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 268 | if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/PageScriptingTestResultDetails/*',inputs.project)) != '') 269 | with: 270 | name: ${{ steps.calculateArtifactsNames.outputs.PageScriptingTestResultDetailsArtifactsName }} 271 | path: '${{ inputs.project }}/.buildartifacts/PageScriptingTestResultDetails/' 272 | if-no-files-found: ignore 273 | 274 | - name: Analyze Test Results 275 | id: analyzeTestResults 276 | if: (success() || failure()) && env.doNotRunTests == 'False' && ((hashFiles(format('{0}/.buildartifacts/TestResults.xml',inputs.project)) != '') || (hashFiles(format('{0}/.buildartifacts/bcptTestResults.json',inputs.project)) != '')) 277 | uses: microsoft/AL-Go-Actions/AnalyzeTests@v7.0 278 | with: 279 | shell: ${{ inputs.shell }} 280 | project: ${{ inputs.project }} 281 | testType: "normal" 282 | 283 | - name: Analyze BCPT Test Results 284 | id: analyzeTestResultsBCPT 285 | if: (success() || failure()) && env.doNotRunBcptTests == 'False' 286 | uses: microsoft/AL-Go-Actions/AnalyzeTests@v7.0 287 | with: 288 | shell: ${{ inputs.shell }} 289 | project: ${{ inputs.project }} 290 | testType: "bcpt" 291 | 292 | - name: Analyze Page Scripting Test Results 293 | id: analyzeTestResultsPageScripting 294 | if: (success() || failure()) && env.doNotRunpageScriptingTests == 'False' 295 | uses: microsoft/AL-Go-Actions/AnalyzeTests@v7.0 296 | with: 297 | shell: ${{ inputs.shell }} 298 | project: ${{ inputs.project }} 299 | testType: "pageScripting" 300 | 301 | - name: Cleanup 302 | if: always() && steps.DetermineBuildProject.outputs.BuildIt == 'True' 303 | uses: microsoft/AL-Go-Actions/PipelineCleanup@v7.0 304 | with: 305 | shell: ${{ inputs.shell }} 306 | project: ${{ inputs.project }} 307 | -------------------------------------------------------------------------------- /.github/workflows/CICD.yaml: -------------------------------------------------------------------------------- 1 | name: ' CI/CD' 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | - '.github/workflows/*.yaml' 9 | - '!.github/workflows/CICD.yaml' 10 | branches: [ 'main', 'release/*', 'feature/*' ] 11 | 12 | defaults: 13 | run: 14 | shell: pwsh 15 | 16 | permissions: 17 | actions: read 18 | contents: read 19 | id-token: write 20 | pages: read 21 | 22 | env: 23 | workflowDepth: 1 24 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 25 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 26 | 27 | jobs: 28 | Initialization: 29 | needs: [ ] 30 | runs-on: [ ubuntu-latest ] 31 | outputs: 32 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 33 | environmentsMatrixJson: ${{ steps.DetermineDeploymentEnvironments.outputs.EnvironmentsMatrixJson }} 34 | environmentCount: ${{ steps.DetermineDeploymentEnvironments.outputs.EnvironmentCount }} 35 | deploymentEnvironmentsJson: ${{ steps.DetermineDeploymentEnvironments.outputs.DeploymentEnvironmentsJson }} 36 | generateALDocArtifact: ${{ steps.DetermineDeploymentEnvironments.outputs.GenerateALDocArtifact }} 37 | deployALDocArtifact: ${{ steps.DetermineDeploymentEnvironments.outputs.DeployALDocArtifact }} 38 | deliveryTargetsJson: ${{ steps.DetermineDeliveryTargets.outputs.DeliveryTargetsJson }} 39 | githubRunner: ${{ steps.ReadSettings.outputs.GitHubRunnerJson }} 40 | githubRunnerShell: ${{ steps.ReadSettings.outputs.GitHubRunnerShell }} 41 | projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} 42 | skippedProjects: ${{ steps.determineProjectsToBuild.outputs.SkippedProjectsJson }} 43 | projectDependenciesJson: ${{ steps.determineProjectsToBuild.outputs.ProjectDependenciesJson }} 44 | buildOrderJson: ${{ steps.determineProjectsToBuild.outputs.BuildOrderJson }} 45 | baselineWorkflowRunId: ${{ steps.determineProjectsToBuild.outputs.BaselineWorkflowRunId }} 46 | baselineWorkflowSHA: ${{ steps.determineProjectsToBuild.outputs.BaselineWorkflowSHA }} 47 | workflowDepth: ${{ steps.DetermineWorkflowDepth.outputs.WorkflowDepth }} 48 | powerPlatformSolutionFolder: ${{ steps.DeterminePowerPlatformSolutionFolder.outputs.powerPlatformSolutionFolder }} 49 | steps: 50 | - name: Dump Workflow Information 51 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 52 | with: 53 | shell: pwsh 54 | 55 | - name: Checkout 56 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 57 | with: 58 | lfs: true 59 | 60 | - name: Initialize the workflow 61 | id: init 62 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 63 | with: 64 | shell: pwsh 65 | 66 | - name: Read settings 67 | id: ReadSettings 68 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 69 | with: 70 | shell: pwsh 71 | get: type,powerPlatformSolutionFolder,useGitSubmodules 72 | 73 | - name: Read submodules token 74 | id: ReadSubmodulesToken 75 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 76 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 77 | with: 78 | shell: pwsh 79 | gitHubSecrets: ${{ toJson(secrets) }} 80 | getSecrets: '-gitSubmodulesToken' 81 | 82 | - name: Checkout Submodules 83 | if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' 84 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 85 | with: 86 | lfs: true 87 | submodules: ${{ env.useGitSubmodules }} 88 | token: '${{ fromJson(steps.ReadSubmodulesToken.outputs.Secrets).gitSubmodulesToken }}' 89 | 90 | - name: Determine Workflow Depth 91 | id: DetermineWorkflowDepth 92 | run: | 93 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "WorkflowDepth=$($env:workflowDepth)" 94 | 95 | - name: Determine Projects To Build 96 | id: determineProjectsToBuild 97 | uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v7.0 98 | with: 99 | shell: pwsh 100 | maxBuildDepth: ${{ env.workflowDepth }} 101 | 102 | - name: Determine PowerPlatform Solution Folder 103 | id: DeterminePowerPlatformSolutionFolder 104 | if: env.type == 'PTE' 105 | run: | 106 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "powerPlatformSolutionFolder=$($env:powerPlatformSolutionFolder)" 107 | 108 | - name: Determine Delivery Target Secrets 109 | id: DetermineDeliveryTargetSecrets 110 | uses: microsoft/AL-Go-Actions/DetermineDeliveryTargets@v7.0 111 | with: 112 | shell: pwsh 113 | projectsJson: '${{ steps.determineProjectsToBuild.outputs.ProjectsJson }}' 114 | checkContextSecrets: 'false' 115 | 116 | - name: Read secrets 117 | id: ReadSecrets 118 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 119 | with: 120 | shell: pwsh 121 | gitHubSecrets: ${{ toJson(secrets) }} 122 | getSecrets: ${{ steps.DetermineDeliveryTargetSecrets.outputs.ContextSecrets }} 123 | 124 | - name: Determine Delivery Targets 125 | id: DetermineDeliveryTargets 126 | uses: microsoft/AL-Go-Actions/DetermineDeliveryTargets@v7.0 127 | env: 128 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 129 | with: 130 | shell: pwsh 131 | projectsJson: '${{ steps.determineProjectsToBuild.outputs.ProjectsJson }}' 132 | checkContextSecrets: 'true' 133 | 134 | - name: Determine Deployment Environments 135 | id: DetermineDeploymentEnvironments 136 | uses: microsoft/AL-Go-Actions/DetermineDeploymentEnvironments@v7.0 137 | env: 138 | GITHUB_TOKEN: ${{ github.token }} 139 | with: 140 | shell: pwsh 141 | getEnvironments: '*' 142 | type: 'CD' 143 | 144 | CheckForUpdates: 145 | needs: [ Initialization ] 146 | runs-on: [ ubuntu-latest ] 147 | steps: 148 | - name: Checkout 149 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 150 | 151 | - name: Read settings 152 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 153 | with: 154 | shell: pwsh 155 | get: templateUrl 156 | 157 | - name: Read secrets 158 | id: ReadSecrets 159 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 160 | with: 161 | shell: pwsh 162 | gitHubSecrets: ${{ toJson(secrets) }} 163 | getSecrets: 'ghTokenWorkflow' 164 | 165 | - name: Check for updates to AL-Go system files 166 | uses: microsoft/AL-Go-Actions/CheckForUpdates@v7.0 167 | with: 168 | shell: pwsh 169 | templateUrl: ${{ env.templateUrl }} 170 | token: ${{ fromJson(steps.ReadSecrets.outputs.Secrets).ghTokenWorkflow }} 171 | downloadLatest: true 172 | 173 | Build: 174 | needs: [ Initialization ] 175 | if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 176 | strategy: 177 | matrix: 178 | include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} 179 | fail-fast: false 180 | name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) 181 | uses: ./.github/workflows/_BuildALGoProject.yaml 182 | secrets: inherit 183 | with: 184 | shell: ${{ matrix.githubRunnerShell }} 185 | runsOn: ${{ matrix.githubRunner }} 186 | project: ${{ matrix.project }} 187 | projectName: ${{ matrix.projectName }} 188 | buildMode: ${{ matrix.buildMode }} 189 | skippedProjectsJson: ${{ needs.Initialization.outputs.skippedProjects }} 190 | projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} 191 | baselineWorkflowRunId: ${{ needs.Initialization.outputs.baselineWorkflowRunId }} 192 | baselineWorkflowSHA: ${{ needs.Initialization.outputs.baselineWorkflowSHA }} 193 | secrets: 'licenseFileUrl,codeSignCertificateUrl,*codeSignCertificatePassword,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' 194 | signArtifacts: true 195 | useArtifactCache: true 196 | 197 | DeployALDoc: 198 | needs: [ Initialization, Build ] 199 | if: (!cancelled()) && needs.Build.result == 'Success' && needs.Initialization.outputs.generateALDocArtifact == 1 && github.ref_name == 'main' 200 | runs-on: [ ubuntu-latest ] 201 | name: Deploy Reference Documentation 202 | permissions: 203 | contents: read 204 | actions: read 205 | pages: write 206 | id-token: write 207 | environment: 208 | name: github-pages 209 | url: ${{ steps.deployment.outputs.page_url }} 210 | steps: 211 | - name: Checkout 212 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 213 | 214 | - name: Download artifacts 215 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 216 | with: 217 | path: '.artifacts' 218 | 219 | - name: Read settings 220 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 221 | with: 222 | shell: pwsh 223 | 224 | - name: Setup Pages 225 | if: needs.Initialization.outputs.deployALDocArtifact == 1 226 | uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 227 | 228 | - name: Build Reference Documentation 229 | uses: microsoft/AL-Go-Actions/BuildReferenceDocumentation@v7.0 230 | with: 231 | shell: pwsh 232 | artifacts: '.artifacts' 233 | 234 | - name: Upload pages artifact 235 | uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 236 | with: 237 | path: ".aldoc/_site/" 238 | 239 | - name: Deploy to GitHub Pages 240 | if: needs.Initialization.outputs.deployALDocArtifact == 1 241 | id: deployment 242 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 243 | 244 | Deploy: 245 | needs: [ Initialization, Build ] 246 | if: (!cancelled()) && (needs.Build.result == 'success' || needs.Build.result == 'skipped') && needs.Initialization.outputs.environmentCount > 0 247 | strategy: ${{ fromJson(needs.Initialization.outputs.environmentsMatrixJson) }} 248 | runs-on: ${{ fromJson(matrix.os) }} 249 | name: Deploy to ${{ matrix.environment }} 250 | defaults: 251 | run: 252 | shell: ${{ matrix.shell }} 253 | environment: 254 | name: ${{ matrix.environment }} 255 | url: ${{ steps.Deploy.outputs.environmentUrl }} 256 | steps: 257 | - name: Checkout 258 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 259 | 260 | - name: Download artifacts 261 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 262 | with: 263 | path: '.artifacts' 264 | 265 | - name: Read settings 266 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 267 | with: 268 | shell: ${{ matrix.shell }} 269 | get: type,powerPlatformSolutionFolder 270 | 271 | - name: EnvName 272 | id: envName 273 | run: | 274 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 275 | $envName = '${{ matrix.environment }}'.split(' ')[0] 276 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "envName=$envName" 277 | 278 | - name: Read secrets 279 | id: ReadSecrets 280 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 281 | with: 282 | shell: ${{ matrix.shell }} 283 | gitHubSecrets: ${{ toJson(secrets) }} 284 | getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' 285 | 286 | - name: Deploy to Business Central 287 | id: Deploy 288 | uses: microsoft/AL-Go-Actions/Deploy@v7.0 289 | env: 290 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 291 | with: 292 | shell: ${{ matrix.shell }} 293 | environmentName: ${{ matrix.environment }} 294 | artifactsFolder: '.artifacts' 295 | type: 'CD' 296 | deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} 297 | 298 | - name: Deploy to Power Platform 299 | if: env.type == 'PTE' && env.powerPlatformSolutionFolder != '' 300 | uses: microsoft/AL-Go-Actions/DeployPowerPlatform@v7.0 301 | env: 302 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 303 | with: 304 | shell: pwsh 305 | environmentName: ${{ matrix.environment }} 306 | artifactsFolder: '.artifacts' 307 | deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} 308 | 309 | Deliver: 310 | needs: [ Initialization, Build ] 311 | if: (!cancelled()) && (needs.Build.result == 'success' || needs.Build.result == 'skipped') && needs.Initialization.outputs.deliveryTargetsJson != '[]' 312 | strategy: 313 | matrix: 314 | deliveryTarget: ${{ fromJson(needs.Initialization.outputs.deliveryTargetsJson) }} 315 | fail-fast: false 316 | runs-on: [ ubuntu-latest ] 317 | name: Deliver to ${{ matrix.deliveryTarget }} 318 | steps: 319 | - name: Checkout 320 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 321 | 322 | - name: Download artifacts 323 | uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 324 | with: 325 | path: '.artifacts' 326 | 327 | - name: Read settings 328 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 329 | with: 330 | shell: pwsh 331 | 332 | - name: Read secrets 333 | id: ReadSecrets 334 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 335 | with: 336 | shell: pwsh 337 | gitHubSecrets: ${{ toJson(secrets) }} 338 | getSecrets: '${{ matrix.deliveryTarget }}Context' 339 | 340 | - name: Deliver 341 | uses: microsoft/AL-Go-Actions/Deliver@v7.0 342 | env: 343 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 344 | with: 345 | shell: pwsh 346 | type: 'CD' 347 | projects: ${{ needs.Initialization.outputs.projects }} 348 | deliveryTarget: ${{ matrix.deliveryTarget }} 349 | artifacts: '.artifacts' 350 | 351 | PostProcess: 352 | needs: [ Initialization, Build, Deploy, Deliver, DeployALDoc ] 353 | if: (!cancelled()) 354 | runs-on: [ ubuntu-latest ] 355 | steps: 356 | - name: Checkout 357 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 358 | 359 | - name: Finalize the workflow 360 | id: PostProcess 361 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 362 | env: 363 | GITHUB_TOKEN: ${{ github.token }} 364 | with: 365 | shell: pwsh 366 | telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} 367 | currentJobContext: ${{ toJson(job) }} 368 | -------------------------------------------------------------------------------- /.github/workflows/CreateRelease.yaml: -------------------------------------------------------------------------------- 1 | name: ' Create release' 2 | run-name: "Create release - Version ${{ inputs.tag }}" 3 | 4 | concurrency: 5 | group: ${{ github.workflow }} 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | appVersion: 11 | description: App version to promote to release (default is latest) 12 | required: false 13 | default: 'latest' 14 | name: 15 | description: Name of this release 16 | required: true 17 | default: '' 18 | tag: 19 | description: Tag of this release (needs to be semantic version string https://semver.org, ex. 1.0.0) 20 | required: true 21 | default: '' 22 | releaseType: 23 | description: Release, prerelease or draft? 24 | type: choice 25 | options: 26 | - Release 27 | - Prerelease 28 | - Draft 29 | default: Release 30 | createReleaseBranch: 31 | description: Create Release Branch? 32 | type: boolean 33 | default: false 34 | releaseBranchPrefix: 35 | description: The prefix for the release branch. Used only if 'Create Release Branch?' is checked. 36 | type: string 37 | default: release/ 38 | updateVersionNumber: 39 | description: New Version Number in main branch. Use Major.Minor (optionally add .Build for versioningstrategy 3) for absolute change, or +1, +0.1 (or +0.0.1 for versioningstrategy 3) incremental change. 40 | required: false 41 | default: '' 42 | skipUpdatingDependencies: 43 | description: Skip updating dependency version numbers in all apps. 44 | type: boolean 45 | default: false 46 | directCommit: 47 | description: Direct Commit? 48 | type: boolean 49 | default: false 50 | useGhTokenWorkflow: 51 | description: Use GhTokenWorkflow for PR/Commit? 52 | type: boolean 53 | default: false 54 | 55 | permissions: 56 | actions: read 57 | contents: write 58 | id-token: write 59 | pull-requests: write 60 | 61 | defaults: 62 | run: 63 | shell: pwsh 64 | 65 | env: 66 | ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} 67 | ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} 68 | 69 | jobs: 70 | CreateRelease: 71 | needs: [ ] 72 | runs-on: [ ubuntu-latest ] 73 | outputs: 74 | artifacts: ${{ steps.analyzeartifacts.outputs.artifacts }} 75 | releaseId: ${{ steps.createrelease.outputs.releaseId }} 76 | commitish: ${{ steps.analyzeartifacts.outputs.commitish }} 77 | releaseVersion: ${{ steps.createreleasenotes.outputs.releaseVersion }} 78 | telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} 79 | steps: 80 | - name: Dump Workflow Information 81 | uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v7.0 82 | with: 83 | shell: pwsh 84 | 85 | - name: Checkout 86 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 87 | 88 | - name: Initialize the workflow 89 | id: init 90 | uses: microsoft/AL-Go-Actions/WorkflowInitialize@v7.0 91 | with: 92 | shell: pwsh 93 | 94 | - name: Read settings 95 | id: ReadSettings 96 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 97 | with: 98 | shell: pwsh 99 | get: templateUrl,repoName,type,powerPlatformSolutionFolder 100 | 101 | - name: Read secrets 102 | id: ReadSecrets 103 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 104 | with: 105 | shell: pwsh 106 | gitHubSecrets: ${{ toJson(secrets) }} 107 | getSecrets: 'TokenForPush,ghTokenWorkflow' 108 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 109 | 110 | - name: Determine Projects 111 | id: determineProjects 112 | uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v7.0 113 | with: 114 | shell: pwsh 115 | 116 | - name: Check for updates to AL-Go system files 117 | uses: microsoft/AL-Go-Actions/CheckForUpdates@v7.0 118 | with: 119 | shell: pwsh 120 | templateUrl: ${{ env.templateUrl }} 121 | token: ${{ fromJson(steps.ReadSecrets.outputs.Secrets).ghTokenWorkflow }} 122 | downloadLatest: true 123 | 124 | - name: Analyze Artifacts 125 | id: analyzeartifacts 126 | env: 127 | _appVersion: ${{ github.event.inputs.appVersion }} 128 | run: | 129 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 130 | $projects = '${{ steps.determineProjects.outputs.ProjectsJson }}' | ConvertFrom-Json 131 | Write-Host "projects:" 132 | $projects | ForEach-Object { Write-Host "- $_" } 133 | if ($env:type -eq "PTE" -and $env:powerPlatformSolutionFolder -ne "") { 134 | Write-Host "PowerPlatformSolution:" 135 | Write-Host "- $($env:powerPlatformSolutionFolder)" 136 | $projects += @($env:powerPlatformSolutionFolder) 137 | } 138 | $include = @() 139 | $sha = '' 140 | $allArtifacts = @() 141 | $page = 1 142 | $headers = @{ 143 | "Authorization" = "token ${{ github.token }}" 144 | "X-GitHub-Api-Version" = "2022-11-28" 145 | "Accept" = "application/vnd.github+json; charset=utf-8" 146 | } 147 | do { 148 | $repoArtifacts = Invoke-RestMethod -UseBasicParsing -Headers $headers -Uri "$($ENV:GITHUB_API_URL)/repos/$($ENV:GITHUB_REPOSITORY)/actions/artifacts?per_page=100&page=$page" 149 | $allArtifacts += $repoArtifacts.Artifacts | Where-Object { !$_.expired } 150 | $page++ 151 | } 152 | while ($repoArtifacts.Artifacts.Count -gt 0) 153 | Write-Host "Repo Artifacts count: $($repoArtifacts.total_count)" 154 | Write-Host "Downloaded Artifacts count: $($allArtifacts.Count)" 155 | $projects | ForEach-Object { 156 | $thisProject = $_ 157 | if ($thisProject -and ($thisProject -ne '.')) { 158 | $project = $thisProject.Replace('\','_').Replace('/','_') 159 | } 160 | else { 161 | $project = $env:repoName 162 | } 163 | $refname = "$ENV:GITHUB_REF_NAME".Replace('/','_') 164 | Write-Host "Analyzing artifacts for project $project" 165 | $appVersion = "$env:_appVersion" 166 | if ($appVersion -eq "latest") { 167 | Write-Host "Grab latest" 168 | $artifact = $allArtifacts | Where-Object { $_.name -like "$project-$refname-Apps-*.*.*.*" -or $_.name -like "$project-$refname-PowerPlatformSolution-*.*.*.*" } | Select-Object -First 1 169 | } 170 | else { 171 | Write-Host "Search for $project-$refname-Apps-$appVersion or $project-$refname-PowerPlatformSolution-$appVersion" 172 | $artifact = $allArtifacts | Where-Object { $_.name -eq "$project-$refname-Apps-$appVersion"-or $_.name -eq "$project-$refname-PowerPlatformSolution-$appVersion" } | Select-Object -First 1 173 | } 174 | if ($artifact) { 175 | $startIndex = $artifact.name.LastIndexOf('-') + 1 176 | $artifactsVersion = $artifact.name.SubString($startIndex) 177 | } 178 | else { 179 | Write-Host "::Error::No artifacts found for this project" 180 | exit 1 181 | } 182 | if ($sha) { 183 | if ($artifact.workflow_run.head_sha -ne $sha) { 184 | Write-Host "::Error::The build selected for release doesn't contain all projects. Please rebuild all projects by manually running the CI/CD workflow and recreate the release." 185 | throw "The build selected for release doesn't contain all projects. Please rebuild all projects by manually running the CI/CD workflow and recreate the release." 186 | } 187 | } 188 | else { 189 | $sha = $artifact.workflow_run.head_sha 190 | } 191 | 192 | Write-host "Looking for $project-$refname-Apps-$artifactsVersion or $project-$refname-TestApps-$artifactsVersion or $project-$refname-Dependencies-$artifactsVersion or $project-$refname-PowerPlatformSolution-$artifactsVersion" 193 | $allArtifacts | Where-Object { ($_.name -like "$project-$refname-Apps-$artifactsVersion" -or $_.name -like "$project-$refname-TestApps-$artifactsVersion" -or $_.name -like "$project-$refname-Dependencies-$artifactsVersion" -or $_.name -like "$project-$refname-PowerPlatformSolution-$artifactsVersion") } | ForEach-Object { 194 | $atype = $_.name.SubString(0,$_.name.Length-$artifactsVersion.Length-1) 195 | $atype = $atype.SubString($atype.LastIndexOf('-')+1) 196 | $include += $( [ordered]@{ "name" = $_.name; "url" = $_.archive_download_url; "atype" = $atype; "project" = $thisproject } ) 197 | } 198 | if ($include.Count -eq 0) { 199 | Write-Host "::Error::No artifacts found for version $artifactsVersion" 200 | exit 1 201 | } 202 | } 203 | $artifacts = @{ "include" = $include } 204 | $artifactsJson = $artifacts | ConvertTo-Json -compress 205 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "artifacts=$artifactsJson" 206 | Write-Host "artifacts=$artifactsJson" 207 | Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "commitish=$sha" 208 | Write-Host "commitish=$sha" 209 | 210 | - name: Prepare release notes 211 | id: createreleasenotes 212 | uses: microsoft/AL-Go-Actions/CreateReleaseNotes@v7.0 213 | with: 214 | shell: pwsh 215 | tag_name: ${{ github.event.inputs.tag }} 216 | target_commitish: ${{ steps.analyzeartifacts.outputs.commitish }} 217 | 218 | - name: Create release 219 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 220 | id: createrelease 221 | env: 222 | bodyMD: ${{ steps.createreleasenotes.outputs.releaseNotes }} 223 | with: 224 | github-token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 225 | script: | 226 | var bodyMD = process.env.bodyMD 227 | const createReleaseResponse = await github.rest.repos.createRelease({ 228 | owner: context.repo.owner, 229 | repo: context.repo.repo, 230 | tag_name: '${{ github.event.inputs.tag }}', 231 | name: '${{ github.event.inputs.name }}', 232 | body: bodyMD.replaceAll('\\n','\n').replaceAll('%0A','\n').replaceAll('%0D','\n').replaceAll('%25','%'), 233 | draft: ${{ github.event.inputs.releaseType=='Draft' }}, 234 | prerelease: ${{ github.event.inputs.releaseType=='Prerelease' }}, 235 | make_latest: 'legacy', 236 | target_commitish: '${{ steps.analyzeartifacts.outputs.commitish }}' 237 | }); 238 | const { 239 | data: { id: releaseId, html_url: htmlUrl, upload_url: uploadUrl } 240 | } = createReleaseResponse; 241 | core.setOutput('releaseId', releaseId); 242 | 243 | UploadArtifacts: 244 | needs: [ CreateRelease ] 245 | runs-on: [ ubuntu-latest ] 246 | strategy: 247 | matrix: ${{ fromJson(needs.CreateRelease.outputs.artifacts) }} 248 | fail-fast: true 249 | steps: 250 | - name: Checkout 251 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 252 | 253 | - name: Read settings 254 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 255 | with: 256 | shell: pwsh 257 | 258 | - name: Read secrets 259 | id: ReadSecrets 260 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 261 | with: 262 | shell: pwsh 263 | gitHubSecrets: ${{ toJson(secrets) }} 264 | getSecrets: 'nuGetContext,storageContext,TokenForPush' 265 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 266 | 267 | - name: Download artifact 268 | run: | 269 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 270 | Write-Host "Downloading artifact ${{ matrix.name}}" 271 | $headers = @{ 272 | "Authorization" = "token ${{ github.token }}" 273 | "X-GitHub-Api-Version" = "2022-11-28" 274 | "Accept" = "application/vnd.github+json" 275 | } 276 | Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri '${{ matrix.url }}' -OutFile '${{ matrix.name }}.zip' 277 | 278 | - name: Upload release artifacts 279 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 280 | env: 281 | releaseId: ${{ needs.createrelease.outputs.releaseId }} 282 | with: 283 | github-token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 284 | script: | 285 | const releaseId = process.env.releaseId 286 | const assetPath = '${{ matrix.name }}.zip' 287 | const assetName = encodeURIComponent('${{ matrix.name }}.zip'.replaceAll(' ','.')).replaceAll('%','') 288 | const fs = require('fs'); 289 | const uploadAssetResponse = await github.rest.repos.uploadReleaseAsset({ 290 | owner: context.repo.owner, 291 | repo: context.repo.repo, 292 | release_id: releaseId, 293 | name: assetName, 294 | data: fs.readFileSync(assetPath) 295 | }); 296 | 297 | - name: Deliver to NuGet 298 | uses: microsoft/AL-Go-Actions/Deliver@v7.0 299 | if: ${{ fromJson(steps.ReadSecrets.outputs.Secrets).nuGetContext != '' }} 300 | env: 301 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 302 | with: 303 | shell: pwsh 304 | type: 'Release' 305 | projects: ${{ matrix.project }} 306 | deliveryTarget: 'NuGet' 307 | artifacts: ${{ github.event.inputs.appVersion }} 308 | atypes: 'Apps,TestApps' 309 | 310 | - name: Deliver to Storage 311 | uses: microsoft/AL-Go-Actions/Deliver@v7.0 312 | if: ${{ fromJson(steps.ReadSecrets.outputs.Secrets).storageContext != '' }} 313 | env: 314 | Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' 315 | with: 316 | shell: pwsh 317 | type: 'Release' 318 | projects: ${{ matrix.project }} 319 | deliveryTarget: 'Storage' 320 | artifacts: ${{ github.event.inputs.appVersion }} 321 | atypes: 'Apps,TestApps,Dependencies' 322 | 323 | CreateReleaseBranch: 324 | needs: [ CreateRelease, UploadArtifacts ] 325 | if: ${{ github.event.inputs.createReleaseBranch=='true' }} 326 | runs-on: [ ubuntu-latest ] 327 | steps: 328 | - name: Checkout 329 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 330 | with: 331 | ref: '${{ needs.createRelease.outputs.commitish }}' 332 | 333 | - name: Create Release Branch 334 | env: 335 | releaseBranchPrefix: ${{ github.event.inputs.releaseBranchPrefix }} 336 | run: | 337 | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 338 | $releaseBranch = "$($env:releaseBranchPrefix)" + "${{ needs.CreateRelease.outputs.releaseVersion }}" 339 | Write-Host "Creating release branch $releaseBranch" 340 | git checkout -b $releaseBranch 341 | git config user.name ${{ github.actor}} 342 | git config user.email ${{ github.actor}}@users.noreply.github.com 343 | git commit --allow-empty -m "Release branch $releaseBranch" 344 | git push origin $releaseBranch 345 | 346 | UpdateVersionNumber: 347 | needs: [ CreateRelease, UploadArtifacts ] 348 | if: ${{ github.event.inputs.updateVersionNumber!='' }} 349 | runs-on: [ ubuntu-latest ] 350 | steps: 351 | - name: Checkout 352 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 353 | 354 | - name: Read settings 355 | uses: microsoft/AL-Go-Actions/ReadSettings@v7.0 356 | with: 357 | shell: pwsh 358 | 359 | - name: Read secrets 360 | id: ReadSecrets 361 | uses: microsoft/AL-Go-Actions/ReadSecrets@v7.0 362 | with: 363 | shell: pwsh 364 | gitHubSecrets: ${{ toJson(secrets) }} 365 | getSecrets: 'TokenForPush' 366 | useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' 367 | 368 | - name: Update Version Number 369 | uses: microsoft/AL-Go-Actions/IncrementVersionNumber@v7.0 370 | with: 371 | shell: pwsh 372 | token: ${{ steps.ReadSecrets.outputs.TokenForPush }} 373 | versionNumber: ${{ github.event.inputs.updateVersionNumber }} 374 | skipUpdatingDependencies: ${{ github.event.inputs.skipUpdatingDependencies }} 375 | directCommit: ${{ github.event.inputs.directCommit }} 376 | 377 | PostProcess: 378 | needs: [ CreateRelease, UploadArtifacts, CreateReleaseBranch, UpdateVersionNumber ] 379 | if: always() 380 | runs-on: [ ubuntu-latest ] 381 | steps: 382 | - name: Checkout 383 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 384 | 385 | - name: Finalize the workflow 386 | id: PostProcess 387 | uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v7.0 388 | env: 389 | GITHUB_TOKEN: ${{ github.token }} 390 | with: 391 | shell: pwsh 392 | telemetryScopeJson: ${{ needs.CreateRelease.outputs.telemetryScopeJson }} 393 | currentJobContext: ${{ toJson(job) }} 394 | --------------------------------------------------------------------------------