├── 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 | [](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 |
--------------------------------------------------------------------------------