Skip to content

Commit 9f454bd

Browse files
committed
Merge branch 'master' into devel-AzureResourceGraph
2 parents 91bb849 + 0e91f01 commit 9f454bd

File tree

1,467 files changed

+94365
-27633
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,467 files changed

+94365
-27633
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,24 @@
11
import { runCheckOverChangedFiles } from "./../utils/changedFilesValidator";
22
import * as logger from "./../utils/logger";
33
import { ExitCode } from "./../utils/exitCode";
4-
import fs from "fs";
5-
import { MainTemplateValidationError } from "./../utils/validationError";
4+
import { IsValidSolutionDomainsVerticals } from "./validDomainsVerticals";
5+
import { IsValidSupportObject } from "./validSupportObject";
6+
import { IsValidBrandingContent } from "./validMSBranding";
7+
import { IsValidSolutionID } from "./validSolutionID";
8+
import { MainTemplateDomainVerticalValidationError, MainTemplateSupportObjectValidationError, InvalidFileContentError, InvalidSolutionIDValidationError } from "../utils/validationError";
69

7-
// initialize arrays to store valid domains and verticals
8-
let validDomains: string[] = [];
9-
let validVerticals: string[] = [];
1010

11-
// read the valid domains and verticals from the JSON file
12-
try {
13-
const validDomainsVerticals = JSON.parse(fs.readFileSync('./.script/SolutionValidations/ValidDomainsVerticals.json', "utf8"));
14-
validDomains = validDomainsVerticals.validDomains;
15-
validVerticals = validDomainsVerticals.validVerticals;
16-
} catch (error) {
17-
logger.logError(`Error reading ValidDomainsVerticals.json file: ${error}`);
18-
}
1911

2012
// function to check if the solution is valid
2113
export async function IsValidSolution(filePath: string): Promise<ExitCode> {
22-
23-
// check if the file is a mainTemplate.json file
24-
if (filePath.endsWith("mainTemplate.json")) {
25-
// read the content of the file
26-
let jsonFile = JSON.parse(fs.readFileSync(filePath, "utf8"));
27-
28-
// check if the file has a "resources" field
29-
if (!jsonFile.hasOwnProperty("resources")) {
30-
console.warn(`No "resources" field found in the file. Skipping file path: ${filePath}`);
31-
return ExitCode.SUCCESS;
32-
}
33-
34-
// get the resources from the file
35-
let resources = jsonFile.resources;
36-
37-
// filter resources that have type "Microsoft.OperationalInsights/workspaces/providers/metadata"
38-
const filteredResource = resources.filter(function (resource: { type: string; }) {
39-
return resource.type === "Microsoft.OperationalInsights/workspaces/providers/metadata";
40-
});
41-
if (filteredResource.length > 0) {
42-
filteredResource.forEach((element: { hasOwnProperty: (arg0: string) => boolean; properties: { hasOwnProperty: (arg0: string) => boolean; categories: any; }; }) => {
43-
// check if the resource has a "properties" field
44-
if (element.hasOwnProperty("properties") === true) {
45-
// check if the "properties" field has a "categories" field
46-
if (element.properties.hasOwnProperty("categories") === true) {
47-
const categories = element.properties.categories;
48-
49-
let invalidDomains = [];
50-
let invalidVerticals = [];
51-
52-
// check if the categories have a "domains" field
53-
if (categories.hasOwnProperty("domains")) {
54-
let domains = categories.domains;
55-
if (domains.length === 0) {
56-
throw new MainTemplateValidationError("The solution must include at least one valid domain. Please provide a domain in the 'domains' field of the 'categories' object.");
57-
}
58-
for (const domain of domains) {
59-
// check if the domain is valid
60-
if (!validDomains.includes(domain)) {
61-
invalidDomains.push(domain);
62-
}
63-
}
64-
}
65-
else {
66-
throw new MainTemplateValidationError("The solution must include at least one valid domain. Please provide a domain in the 'domains' field of the 'categories' object.");
67-
}
68-
69-
// check if the categories have a "verticals" field
70-
if (categories.hasOwnProperty("verticals")) {
71-
let verticals = categories.verticals;
72-
for (const vertical of verticals) {
73-
if (!validVerticals.includes(vertical)) {
74-
invalidVerticals.push(vertical);
75-
}
76-
}
77-
}
78-
79-
if (invalidDomains.length > 0 || invalidVerticals.length > 0) {
80-
let errorMessage = "Invalid";
81-
if (invalidDomains.length > 0) {
82-
errorMessage += ` domains: [${invalidDomains.join(", ")}]`;
83-
}
84-
if (invalidVerticals.length > 0) {
85-
errorMessage += ` verticals: [${invalidVerticals.join(", ")}]`;
86-
}
87-
errorMessage += ` provided.`;
88-
throw new MainTemplateValidationError(errorMessage);
89-
}
90-
}
91-
}
92-
});
93-
}
94-
95-
// If the file is not identified as a main template, log a warning message
96-
} else {
97-
console.warn(`Could not identify json file as a Main Template. Skipping File path: ${filePath}`);
98-
}
99-
100-
// Return success code after completion of the check
14+
IsValidSolutionDomainsVerticals(filePath);
15+
IsValidSupportObject(filePath);
16+
IsValidBrandingContent(filePath);
17+
IsValidSolutionID(filePath);
10118
return ExitCode.SUCCESS;
10219
}
10320

10421

105-
106-
107-
10822
// Array to store file type suffixes for the check
10923
let fileTypeSuffixes = ["json"];
11024

@@ -122,13 +36,27 @@ let CheckOptions = {
12236
},
12337
// Callback function to handle errors during execution
12438
onExecError: async (e: any, filePath: string) => {
125-
console.log(`Solution Validation Failed. File path: ${filePath}. Error message: ${e.message}`);
39+
console.log(`Solution Validation Failed. File path: ${filePath} \nError message: ${e.message}`);
40+
if (e instanceof MainTemplateDomainVerticalValidationError) {
41+
logger.logError("Please refer link https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/sentinel/sentinel-solutions.md?msclkid=9a240b52b11411ec99ae6736bd089c4a#categories-for-microsoft-sentinel-out-of-the-box-content-and-solutions for valid Domains and Verticals.");
42+
} else if (e instanceof MainTemplateSupportObjectValidationError) {
43+
logger.logError("Validation for Support object failed in Main Template.");
44+
}
45+
else if (e instanceof InvalidFileContentError) {
46+
logger.logError("Validation for Microsoft Sentinel Branding Failed.");
47+
}
48+
else if (e instanceof InvalidSolutionIDValidationError) {
49+
logger.logError("Validation for Solution ID Failed.");
50+
}
12651
},
12752
// Callback function to handle final failure
12853
onFinalFailed: async () => {
129-
logger.logError("Please refer link https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/sentinel/sentinel-solutions.md?msclkid=9a240b52b11411ec99ae6736bd089c4a#categories-for-microsoft-sentinel-out-of-the-box-content-and-solutions for valid Domains and Verticals.");
54+
logger.logError("Validation failed, please fix the errors.");
13055
},
56+
13157
};
13258

13359
// Function call to start the check process
13460
runCheckOverChangedFiles(CheckOptions, fileKinds, fileTypeSuffixes, filePathFolderPrefixes);
61+
62+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { MainTemplateDomainVerticalValidationError } from "./../utils/validationError";
2+
import * as logger from "./../utils/logger";
3+
import { ExitCode } from "../utils/exitCode";
4+
import fs from "fs";
5+
6+
// initialize arrays to store valid domains and verticals
7+
let validDomains: string[] = [];
8+
let validVerticals: string[] = [];
9+
10+
// read the valid domains and verticals from the JSON file
11+
try {
12+
const validDomainsVerticals = JSON.parse(fs.readFileSync('./.script/SolutionValidations/ValidDomainsVerticals.json', "utf8"));
13+
validDomains = validDomainsVerticals.validDomains;
14+
validVerticals = validDomainsVerticals.validVerticals;
15+
} catch (error) {
16+
logger.logError(`Error reading ValidDomainsVerticals.json file: ${error}`);
17+
}
18+
19+
// function to check if the solution is valid
20+
export function IsValidSolutionDomainsVerticals(filePath: string): ExitCode {
21+
22+
// check if the file is a mainTemplate.json file
23+
if (filePath.endsWith("mainTemplate.json")) {
24+
// read the content of the file
25+
let jsonFile = JSON.parse(fs.readFileSync(filePath, "utf8"));
26+
27+
// check if the file has a "resources" field
28+
if (!jsonFile.hasOwnProperty("resources")) {
29+
throw new MainTemplateDomainVerticalValidationError(`No "resources" field found in the file. File path: ${filePath}`);
30+
}
31+
32+
// get the resources from the file
33+
let resources = jsonFile.resources;
34+
35+
// filter resources that have type "Microsoft.OperationalInsights/workspaces/providers/metadata"
36+
const filteredResource = resources.filter(function (resource: { type: string; }) {
37+
return resource.type === "Microsoft.OperationalInsights/workspaces/providers/metadata";
38+
});
39+
if (filteredResource.length > 0) {
40+
filteredResource.forEach((element: { hasOwnProperty: (arg0: string) => boolean; properties: { hasOwnProperty: (arg0: string) => boolean; categories: any; }; }) => {
41+
// check if the resource has a "properties" field
42+
if (element.hasOwnProperty("properties") === true) {
43+
// check if the "properties" field has a "categories" field
44+
if (element.properties.hasOwnProperty("categories") === true) {
45+
const categories = element.properties.categories;
46+
47+
let invalidDomains = [];
48+
let invalidVerticals = [];
49+
50+
// check if the categories have a "domains" field
51+
if (categories.hasOwnProperty("domains")) {
52+
let domains = categories.domains;
53+
if (domains.length === 0) {
54+
throw new MainTemplateDomainVerticalValidationError("The solution must include at least one valid domain. Please provide a domain in the 'domains' field of the 'categories' object.");
55+
}
56+
for (const domain of domains) {
57+
// check if the domain is valid
58+
if (!validDomains.includes(domain)) {
59+
invalidDomains.push(domain);
60+
}
61+
}
62+
}
63+
else {
64+
throw new MainTemplateDomainVerticalValidationError("The solution must include at least one valid domain. Please provide a domain in the 'domains' field of the 'categories' object.");
65+
}
66+
67+
// check if the categories have a "verticals" field
68+
if (categories.hasOwnProperty("verticals")) {
69+
let verticals = categories.verticals;
70+
for (const vertical of verticals) {
71+
if (!validVerticals.includes(vertical)) {
72+
invalidVerticals.push(vertical);
73+
}
74+
}
75+
}
76+
77+
if (invalidDomains.length > 0 || invalidVerticals.length > 0) {
78+
let errorMessage = "Invalid";
79+
if (invalidDomains.length > 0) {
80+
errorMessage += ` domains: [${invalidDomains.join(", ")}]`;
81+
}
82+
if (invalidVerticals.length > 0) {
83+
errorMessage += ` verticals: [${invalidVerticals.join(", ")}]`;
84+
}
85+
errorMessage += ` provided.`;
86+
throw new MainTemplateDomainVerticalValidationError(errorMessage);
87+
}
88+
}
89+
}
90+
});
91+
}
92+
else {
93+
throw new MainTemplateDomainVerticalValidationError(`There are no metadata resoruces found in the file. File path: ${filePath}`);
94+
}
95+
96+
// If the file is not identified as a main template, log a warning message
97+
} else {
98+
console.warn(`Could not identify json file as a Main Template. Skipping File path: ${filePath}`);
99+
}
100+
101+
// Return success code after completion of the check
102+
return ExitCode.SUCCESS;
103+
}
104+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { InvalidFileContentError } from "./../utils/validationError";
2+
import { ExitCode } from "../utils/exitCode";
3+
import fs from "fs";
4+
5+
type AttributeConfig = {
6+
mainTemplateAttributes: string[];
7+
createUIDefinitionAttributes: string[];
8+
};
9+
10+
const attributeConfig: AttributeConfig = {
11+
mainTemplateAttributes: ["descriptionMarkdown", "description"],
12+
createUIDefinitionAttributes: ["text", "description"],
13+
};
14+
15+
export function IsValidBrandingContent(filePath: string): ExitCode {
16+
// Skip validation if file path contains "SentinelOne"
17+
if (filePath.includes("SentinelOne")) {
18+
return ExitCode.SUCCESS;
19+
}
20+
21+
const errors: string[] = [];
22+
23+
// check if the file is mainTemplate.json or createUiDefinition.json
24+
if (filePath.endsWith("mainTemplate.json")) {
25+
validateFileContent(filePath, attributeConfig.mainTemplateAttributes, errors);
26+
} else if (filePath.endsWith("createUiDefinition.json")) {
27+
validateFileContent(filePath, attributeConfig.createUIDefinitionAttributes, errors);
28+
} else {
29+
console.warn(`Could not identify JSON file as mainTemplate.json or createUiDefinition.json. Skipping. File path: ${filePath}`);
30+
}
31+
32+
// Throw a single error with all the error messages concatenated
33+
if (errors.length > 0) {
34+
throw new InvalidFileContentError(errors.join("\n"));
35+
}
36+
37+
// Return success code after completion of the check
38+
return ExitCode.SUCCESS;
39+
}
40+
41+
function validateFileContent(filePath: string, attributeNames: string[], errors: string[]): void {
42+
const fileContent = fs.readFileSync(filePath, "utf8");
43+
const jsonContent = JSON.parse(fileContent);
44+
45+
traverseAttributes(jsonContent, attributeNames, errors);
46+
}
47+
48+
function traverseAttributes(jsonContent: any, attributeNames: string[], errors: string[]): void {
49+
for (const key in jsonContent) {
50+
if (jsonContent.hasOwnProperty(key)) {
51+
const attributeValue = jsonContent[key];
52+
if (attributeNames.includes(key) && typeof attributeValue === "string") {
53+
validateAttribute(attributeValue, key, errors);
54+
}
55+
if (typeof attributeValue === "object" && attributeValue !== null) {
56+
traverseAttributes(attributeValue, attributeNames, errors);
57+
}
58+
}
59+
}
60+
}
61+
62+
function validateAttribute(attributeValue: string, attributeName: string, errors: string[]): void {
63+
const sentinelRegex = /(?<!Microsoft\s)(?<!\S)Sentinel(?!\S)/g;
64+
const updatedValue = attributeValue.replace(sentinelRegex, "Microsoft Sentinel");
65+
66+
if (attributeValue !== updatedValue) {
67+
const error = `Inaccurate product branding used in '${attributeName}' for '${attributeValue}'. Use "Microsoft Sentinel" instead of "Sentinel"`;
68+
errors.push(error);
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { InvalidSolutionIDValidationError } from "./../utils/validationError";
2+
import { ExitCode } from "../utils/exitCode";
3+
import fs from "fs";
4+
5+
export function IsValidSolutionID(filePath: string): ExitCode {
6+
// Check if the file path ends with mainTemplate.json
7+
if (!filePath.endsWith("mainTemplate.json")) {
8+
return ExitCode.SUCCESS;
9+
}
10+
11+
// Parse the JSON content from the file
12+
const jsonContent = JSON.parse(fs.readFileSync(filePath, "utf8"));
13+
14+
// Get the 'variables' section from the JSON
15+
const variables = jsonContent.variables;
16+
17+
// Check if 'solutionId' attribute is present
18+
if (variables && "solutionId" in variables) {
19+
const solutionId = variables.solutionId;
20+
21+
// Validate if the solution ID is empty
22+
if (!solutionId) {
23+
throw new InvalidSolutionIDValidationError(`Empty solution ID. Expected format: publisherID.offerID. and it must be in lowercase. Found empty value.`);
24+
}
25+
26+
// Validate the solution ID format
27+
const regex = /^[^.]+\.[^.]+$/;
28+
if (!regex.test(solutionId)) {
29+
throw new InvalidSolutionIDValidationError(`Invalid solution ID format. Expected format: publisherID.offerID. and it must be in lowercase. Found: ${solutionId}`);
30+
}
31+
32+
// Validate the solution ID case (lowercase)
33+
if (solutionId !== solutionId.toLowerCase()) {
34+
throw new InvalidSolutionIDValidationError(`Invalid solution ID format. Expected format: publisherID.offerID. and it must be in lowercase. Found: ${solutionId}`);
35+
}
36+
} else {
37+
throw new InvalidSolutionIDValidationError(`Missing 'solutionId' attribute in the file. File path: ${filePath}`);
38+
}
39+
40+
// Return success code after completion of the check
41+
return ExitCode.SUCCESS;
42+
}

0 commit comments

Comments
 (0)