diff --git a/.github/workflows/loc-update.yml b/.github/workflows/loc-update.yml
index 3a29e6605..406f2bbb8 100644
--- a/.github/workflows/loc-update.yml
+++ b/.github/workflows/loc-update.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- - 'loc/**'
+ - 'loc/translations-import/**'
jobs:
update-localization:
diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json
index dd1f1a767..3982a9962 100644
--- a/l10n/bundle.l10n.json
+++ b/l10n/bundle.l10n.json
@@ -213,6 +213,81 @@
"Please select the folder that contains your compiled output to upload your site.": "Please select the folder that contains your compiled output to upload your site.",
"Upload failed. Please try again later.": "Upload failed. Please try again later.",
"Missing required site information for reactivation.": "Missing required site information for reactivation.",
+ "The CodeQL extension is required to run this command. Do you want to install it now?": "The CodeQL extension is required to run this command. Do you want to install it now?",
+ "CodeQL screening started. Creating database and analyzing": "CodeQL screening started. Creating database and analyzing",
+ "CodeQL database created successfully. You can now run queries from the CodeQL extension.": "CodeQL database created successfully. You can now run queries from the CodeQL extension.",
+ "CodeQL screening failed. Please try again later.": "CodeQL screening failed. Please try again later.",
+ "Current site path not found. Please ensure you have a site folder open.": "Current site path not found. Please ensure you have a site folder open.",
+ "CodeQL database created. You can now:\n\n1. Run custom queries if you have any\n2. Use the prebuilt queries from the CodeQL extension\n\nCheck the CodeQL extension panel for available queries.": "CodeQL database created. You can now:\n\n1. Run custom queries if you have any\n2. Use the prebuilt queries from the CodeQL extension\n\nCheck the CodeQL extension panel for available queries.",
+ "The SARIF Viewer extension is recommended for viewing CodeQL results. Would you like to install it now?": "The SARIF Viewer extension is recommended for viewing CodeQL results. Would you like to install it now?",
+ "Failed to install SARIF viewer extension. Opening results as text file.": "Failed to install SARIF viewer extension. Opening results as text file.",
+ "Open as Text": "Open as Text",
+ "CodeQL Analysis": "CodeQL Analysis",
+ "Starting CodeQL analysis for: {0}": "Starting CodeQL analysis for: {0}",
+ "Database location: {0}": "Database location: {0}",
+ "Creating CodeQL database...": "Creating CodeQL database...",
+ "Running CodeQL analysis...": "Running CodeQL analysis...",
+ "Analysis completed using query suite: {0}": "Analysis completed using query suite: {0}",
+ "Primary query suite {0} failed: {1}": "Primary query suite {0} failed: {1}",
+ "Analysis complete! Results saved to: {0}": "Analysis complete! Results saved to: {0}",
+ "Error during CodeQL analysis: {0}": "Error during CodeQL analysis: {0}",
+ "CodeQL analysis failed: {0}": "CodeQL analysis failed: {0}",
+ "Found CodeQL CLI at: {0}": "Found CodeQL CLI at: {0}",
+ "CodeQL CLI is not installed or not in PATH. Please install the CodeQL extension or add CodeQL CLI to your PATH.": "CodeQL CLI is not installed or not in PATH. Please install the CodeQL extension or add CodeQL CLI to your PATH.",
+ "CodeQL version: {0}": "CodeQL version: {0}",
+ "CodeQL extension is not installed.": "CodeQL extension is not installed.",
+ "Activating CodeQL extension...": "Activating CodeQL extension...",
+ "Could not determine user data path.": "Could not determine user data path.",
+ "Looking for CodeQL CLI in: {0}": "Looking for CodeQL CLI in: {0}",
+ "CodeQL global storage path does not exist: {0}": "CodeQL global storage path does not exist: {0}",
+ "Found CodeQL CLI via extension API: {0}": "Found CodeQL CLI via extension API: {0}",
+ "Error getting CLI path from extension API: {0}": "Error getting CLI path from extension API: {0}",
+ "Could not locate CodeQL CLI in global storage.": "Could not locate CodeQL CLI in global storage.",
+ "Error accessing CodeQL extension: {0}": "Error accessing CodeQL extension: {0}",
+ "Found distribution directories: {0}": "Found distribution directories: {0}",
+ "Error searching for CodeQL CLI: {0}": "Error searching for CodeQL CLI: {0}",
+ "Executing: {0}": "Executing: {0}",
+ "Command completed successfully (exit code: {0})": "Command completed successfully (exit code: {0})",
+ "Command failed with exit code: {0}": "Command failed with exit code: {0}",
+ "Command failed with exit code {0}: {1}": "Command failed with exit code {0}: {1}",
+ "Process error: {0}": "Process error: {0}",
+ "Found {0} issue(s):": "Found {0} issue(s):",
+ "No message": "No message",
+ "File: {0}": "File: {0}",
+ "Line: {0}": "Line: {0}",
+ "No issues found!": "No issues found!",
+ "No analysis results found.": "No analysis results found.",
+ "CodeQL analysis completed successfully with no issues found! π": "CodeQL analysis completed successfully with no issues found! π",
+ "Error reading results: {0}": "Error reading results: {0}",
+ "SARIF viewer extension not found.": "SARIF viewer extension not found.",
+ "Installing SARIF viewer extension...": "Installing SARIF viewer extension...",
+ "SARIF viewer extension installed successfully. Activating...": "SARIF viewer extension installed successfully. Activating...",
+ "Opening results with newly installed SARIF viewer...": "Opening results with newly installed SARIF viewer...",
+ "Results opened in SARIF viewer successfully.": "Results opened in SARIF viewer successfully.",
+ "Extension installed but API not available yet. Opening as text file...": "Extension installed but API not available yet. Opening as text file...",
+ "Failed to install SARIF viewer extension: {0}": "Failed to install SARIF viewer extension: {0}",
+ "User cancelled opening results.": "User cancelled opening results.",
+ "Activating SARIF viewer extension...": "Activating SARIF viewer extension...",
+ "Opening results with SARIF viewer...": "Opening results with SARIF viewer...",
+ "SARIF viewer extension does not expose expected API. Falling back to text editor...": "SARIF viewer extension does not expose expected API. Falling back to text editor...",
+ "Error opening with SARIF viewer: {0}": "Error opening with SARIF viewer: {0}",
+ "CodeQL analysis completed. Would you like to open the full results file?": "CodeQL analysis completed. Would you like to open the full results file?",
+ "Open Results": "Open Results",
+ "Close": "Close",
+ "Error opening results file: {0}": "Error opening results file: {0}",
+ "Select folder for CodeQL database": "Select folder for CodeQL database",
+ "Use current site folder": "Use current site folder",
+ "CodeQL screening is not supported for this site.": "CodeQL screening is not supported for this site.",
+ "Found PowerPages config file: {0}": "Found PowerPages config file: {0}",
+ "Using custom CodeQL query suite: {0}": "Using custom CodeQL query suite: {0}",
+ "Added default query suite to config: {0}": "Added default query suite to config: {0}",
+ "Created PowerPages config file at {0} with default query suite: {1}": "Created PowerPages config file at {0} with default query suite: {1}",
+ "Error reading config file: {0}": "Error reading config file: {0}",
+ "Using default query suite: {0}": "Using default query suite: {0}",
+ "PowerPages config file created successfully: {0}": "PowerPages config file created successfully: {0}",
+ "Error creating config file: {0}": "Error creating config file: {0}",
+ "PowerPages config file updated successfully: {0}": "PowerPages config file updated successfully: {0}",
+ "Error updating config file: {0}": "Error updating config file: {0}",
"Timestamp: {0}/{0} is the timestamp": {
"message": "Timestamp: {0}",
"comment": [
diff --git a/loc/translations-export/vscode-powerplatform.xlf b/loc/translations-export/vscode-powerplatform.xlf
index 457eeba56..30854b536 100644
--- a/loc/translations-export/vscode-powerplatform.xlf
+++ b/loc/translations-export/vscode-powerplatform.xlf
@@ -25,6 +25,12 @@
AI-generated content can contain mistakes
+
+ Activating CodeQL extension...
+
+
+ Activating SARIF viewer extension...
+
Active Sites
@@ -37,10 +43,19 @@
Add content snippet name (name should be unique)
+
+ Added default query suite to config: {0}
+
Adding {0} web file(s). Existing files will be skipped
{0} represents the number of web files
+
+ Analysis complete! Results saved to: {0}
+
+
+ Analysis completed using query suite: {0}
+
Are you sure you want to clear all the Auth Profiles?
@@ -107,6 +122,9 @@
This is a markdown formatting which must persist across translations.
The second line should be '[TRANSLATION HERE](https://aka.ms/pages-clear-cache).', keeping brackets and the text in the parentheses unmodified
+
+ Close
+
Cloud Instance: {0}
The {0} represents profile's Azure Cloud Instances
@@ -126,6 +144,59 @@ The second line should be '[TRANSLATION HERE](https://aka.ms/pages-clear-cache).
Code Block
+
+ CodeQL Analysis
+
+
+ CodeQL CLI is not installed or not in PATH. Please install the CodeQL extension or add CodeQL CLI to your PATH.
+
+
+ CodeQL analysis completed successfully with no issues found! π
+
+
+ CodeQL analysis completed. Would you like to open the full results file?
+
+
+ CodeQL analysis failed: {0}
+
+
+ CodeQL database created successfully. You can now run queries from the CodeQL extension.
+
+
+ CodeQL database created. You can now:
+
+1. Run custom queries if you have any
+2. Use the prebuilt queries from the CodeQL extension
+
+Check the CodeQL extension panel for available queries.
+
+
+ CodeQL extension is not installed.
+
+
+ CodeQL global storage path does not exist: {0}
+
+
+ CodeQL screening failed. Please try again later.
+
+
+ CodeQL screening is not supported for this site.
+
+
+ CodeQL screening started. Creating database and analyzing
+
+
+ CodeQL version: {0}
+
+
+ Command completed successfully (exit code: {0})
+
+
+ Command failed with exit code {0}: {1}
+
+
+ Command failed with exit code: {0}
+
Confirm
@@ -144,10 +215,22 @@ The second line should be '[TRANSLATION HERE](https://aka.ms/pages-clear-cache).
Copy to clipboard
+
+ Could not determine user data path.
+
+
+ Could not locate CodeQL CLI in global storage.
+
+
+ Created PowerPages config file at {0} with default query suite: {1}
+
Created on: {0}
{0} is the created date
+
+ Creating CodeQL database...
+
Creating {0}...
{0} will be replaced by the entity type.
@@ -162,10 +245,16 @@ The second line should be '[TRANSLATION HERE](https://aka.ms/pages-clear-cache).
Current site path not found.
+
+ Current site path not found. Please ensure you have a site folder open.
+
Data model version: {0}
{0} is the data model version
+
+ Database location: {0}
+
Default Environment: {0}
The {0} represents profile's resource/environment URL
@@ -209,12 +298,48 @@ The {3} represents Solution's Type (Managed or Unmanaged), but that test is loca
Environment changed successfully.
+
+ Error accessing CodeQL extension: {0}
+
+
+ Error creating config file: {0}
+
+
+ Error during CodeQL analysis: {0}
+
+
+ Error getting CLI path from extension API: {0}
+
+
+ Error opening results file: {0}
+
+
+ Error opening with SARIF viewer: {0}
+
+
+ Error reading config file: {0}
+
+
+ Error reading results: {0}
+
+
+ Error searching for CodeQL CLI: {0}
+
+
+ Error updating config file: {0}
+
+
+ Executing: {0}
+
Explain the following code snippet:
Explain the following code {% include 'Page Copy'%}
+
+ Extension installed but API not available yet. Opening as text file...
+
Failed to create a new Power Pages site. Please try again.
@@ -240,6 +365,12 @@ The {3} represents Solution's Type (Managed or Unmanaged), but that test is loca
Failed to get website endpoint. Please try again later
+
+ Failed to install SARIF viewer extension. Opening results as text file.
+
+
+ Failed to install SARIF viewer extension: {0}
+
Feature is not enabled for this geo.
@@ -256,6 +387,24 @@ The {3} represents Solution's Type (Managed or Unmanaged), but that test is loca
File(s) already exist. No new files to add
+
+ File: {0}
+
+
+ Found CodeQL CLI at: {0}
+
+
+ Found CodeQL CLI via extension API: {0}
+
+
+ Found PowerPages config file: {0}
+
+
+ Found distribution directories: {0}
+
+
+ Found {0} issue(s):
+
Friendly name: {0}
{0} is the website name
@@ -324,6 +473,9 @@ Return to this chat and @powerpages can help you write and edit your website cod
Installing Power Pages generator(v{0})...
{0} represents the version number
+
+ Installing SARIF viewer extension...
+
Instance url: {0}
{0} is the Instance Url
@@ -337,12 +489,18 @@ Return to this chat and @powerpages can help you write and edit your website cod
Like something? Tell us more.
+
+ Line: {0}
+
Login
Login to PowerPages
+
+ Looking for CodeQL CLI in: {0}
+
Make sure AI-generated content is accurate and appropriate before using. <a href="https://go.microsoft.com/fwlink/?linkid=2240145">Learn more</a> | <a href="https://go.microsoft.com/fwlink/?linkid=2189520">View
terms</a>
@@ -395,9 +553,18 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
No Website Data Found in Current Directory. Please switch to a directory that contains the PAC downloaded website data to continue.
+
+ No analysis results found.
+
No environments found
+
+ No issues found!
+
+
+ No message
+
No page templates found
@@ -423,12 +590,24 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
One or more attribute names have been changed or removed. Contact your admin.
+
+ Open Results
+
+
+ Open as Text
+
Open in Power Pages studio
Opening preview site...
+
+ Opening results with SARIF viewer...
+
+
+ Opening results with newly installed SARIF viewer...
+
Opening site preview...
@@ -479,6 +658,12 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Power Pages studio URL is not available
+
+ PowerPages config file created successfully: {0}
+
+
+ PowerPages config file updated successfully: {0}
+
Preparing pac CLI (v{0})...
{0} represents the version number
@@ -489,6 +674,12 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Preview site URL is not valid
+
+ Primary query suite {0} failed: {1}
+
+
+ Process error: {0}
+
Profile Kind: {0}
The {0} represents the profile type (Admin vs Dataverse)
@@ -496,6 +687,21 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Response data is empty
+
+ Results opened in SARIF viewer successfully.
+
+
+ Running CodeQL analysis...
+
+
+ SARIF viewer extension does not expose expected API. Falling back to text editor...
+
+
+ SARIF viewer extension installed successfully. Activating...
+
+
+ SARIF viewer extension not found.
+
Saving your file ...
@@ -512,6 +718,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Select an environment
+
+ Select folder for CodeQL database
+
Select the folder that contains your compiled output
@@ -553,6 +762,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Something went wrong. Donβt worry, you can try again.
+
+ Starting CodeQL analysis for: {0}
+
Submit
@@ -563,9 +775,15 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Tenant ID: {0}
{0} is the Tenant ID
+
+ The CodeQL extension is required to run this command. Do you want to install it now?
+
The Power Pages generator is ready for use in your VS Code extension!
+
+ The SARIF Viewer extension is recommended for viewing CodeQL results. Would you like to install it now?
+
The extension 'Microsoft Edge Tools' is required to run this command. Do you want to install it now?
Do not translate 'Microsoft Edge Tools'
@@ -635,10 +853,22 @@ The {3} represents Dataverse Environment's Organization ID (GUID)
Upload failed. Please try again later.
+
+ Use current site folder
+
+
+ User cancelled opening results.
+
User: {0}
The {0} represents auth profile's user name (email address))
+
+ Using custom CodeQL query suite: {0}
+
+
+ Using default query suite: {0}
+
We encountered an error preparing the files for edit.
@@ -890,6 +1120,9 @@ The second line should be '[TRANSLATION HERE](command:powerplatform-walkthrough.
Reveal in Finder
+
+ Run CodeQL Screening
+
Save conflict
diff --git a/package.json b/package.json
index a13a4c6d3..769face26 100644
--- a/package.json
+++ b/package.json
@@ -479,6 +479,12 @@
{
"command": "microsoft.powerplatform.pages.actionsHub.inactiveSite.reactivateSite",
"title": "%microsoft.powerplatform.pages.actionsHub.inactiveSite.reactivateSite.title%"
+ },
+ {
+ "command": "microsoft.powerplatform.pages.actionsHub.currentActiveSite.runCodeQLScreening",
+ "category": "Power Pages",
+ "title": "%microsoft.powerplatform.pages.actionsHub.currentActiveSite.runCodeQLScreening.title%",
+ "when": "microsoft.powerplatform.pages.codeQlScanEnabled"
}
],
"configuration": {
@@ -1156,6 +1162,11 @@
"command": "microsoft.powerplatform.pages.actionsHub.inactiveSite.reactivateSite",
"when": "view == microsoft.powerplatform.pages.actionsHub && viewItem == inactiveSite",
"group": "siteAction@1"
+ },
+ {
+ "command": "microsoft.powerplatform.pages.actionsHub.currentActiveSite.runCodeQLScreening",
+ "when": "view == microsoft.powerplatform.pages.actionsHub && viewItem == currentActiveSite && microsoft.powerplatform.pages.codeQlScanEnabled",
+ "group": "siteAction@7"
}
]
},
diff --git a/package.nls.json b/package.nls.json
index ff5693dcc..7b98ca430 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -115,5 +115,6 @@
"microsoft.powerplatform.pages.actionsHub.activeSite.downloadSite.title": "Download Site",
"microsoft.powerplatform.pages.actionsHub.activeSite.openInStudio.title": "Open in Power Pages Studio",
"microsoft.powerplatform.pages.actionsHub.inactiveSite.reactivateSite.title": "Reactivate Site",
+ "microsoft.powerplatform.pages.actionsHub.currentActiveSite.runCodeQLScreening.title": "Run CodeQL Screening",
"microsoft.powerplatform.pages.actionsHub.configuration.downloadSite.description": "The folder where site will be downloaded when using Power Pages Actions. Leave this empty to ask for a folder every time you download a site."
}
diff --git a/src/client/power-pages/actions-hub/ActionsHub.ts b/src/client/power-pages/actions-hub/ActionsHub.ts
index bd5b6690e..360193a37 100644
--- a/src/client/power-pages/actions-hub/ActionsHub.ts
+++ b/src/client/power-pages/actions-hub/ActionsHub.ts
@@ -5,7 +5,7 @@
import * as vscode from "vscode";
import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient";
-import { EnableActionsHub } from "../../../common/ecs-features/ecsFeatureGates";
+import { EnableActionsHub, EnableCodeQlScan } from "../../../common/ecs-features/ecsFeatureGates";
import { ActionsHubTreeDataProvider } from "./ActionsHubTreeDataProvider";
import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper";
import { PacTerminal } from "../../lib/PacTerminal";
@@ -25,6 +25,16 @@ export class ActionsHub {
return enableActionsHub;
}
+ static isCodeQlScanEnabled(): boolean {
+ const enableCodeQlScan = ECSFeaturesClient.getConfig(EnableCodeQlScan).enableCodeQlScan;
+
+ if (enableCodeQlScan === undefined) {
+ return false;
+ }
+
+ return enableCodeQlScan;
+ }
+
static async initialize(context: vscode.ExtensionContext, pacTerminal: PacTerminal): Promise {
if (ActionsHub._isInitialized) {
return;
@@ -32,6 +42,7 @@ export class ActionsHub {
try {
const isActionsHubEnabled = ActionsHub.isEnabled();
+ const isCodeQlScanEnabled = ActionsHub.isCodeQlScanEnabled();
oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.ACTIONS_HUB_ENABLED, {
isEnabled: isActionsHubEnabled.toString(),
@@ -39,12 +50,13 @@ export class ActionsHub {
});
vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.actionsHubEnabled", isActionsHubEnabled);
+ vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.codeQlScanEnabled", isCodeQlScanEnabled);
if (!isActionsHubEnabled) {
return;
}
- ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ ActionsHubTreeDataProvider.initialize(context, pacTerminal, isCodeQlScanEnabled);
ActionsHub._isInitialized = true;
oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.ACTIONS_HUB_INITIALIZED, getBaseEventInfo());
} catch (exception) {
diff --git a/src/client/power-pages/actions-hub/ActionsHubCommandHandlers.ts b/src/client/power-pages/actions-hub/ActionsHubCommandHandlers.ts
index fae43f039..5bd3a2b6f 100644
--- a/src/client/power-pages/actions-hub/ActionsHubCommandHandlers.ts
+++ b/src/client/power-pages/actions-hub/ActionsHubCommandHandlers.ts
@@ -8,7 +8,7 @@ import * as fs from 'fs';
import * as yaml from 'yaml';
import { Constants } from './Constants';
import { PacTerminal } from '../../lib/PacTerminal';
-import { POWERPAGES_SITE_FOLDER, SUCCESS, UTF8_ENCODING, WEBSITE_YML } from '../../../common/constants';
+import { POWERPAGES_SITE_FOLDER, SUCCESS, UTF8_ENCODING, CODEQL_EXTENSION_ID } from '../../../common/constants';
import { AuthInfo, OrgListOutput } from '../../pac/PacTypes';
import { extractAuthInfo } from '../commonUtility';
import { showProgressWithNotification } from '../../../common/utilities/Utils';
@@ -24,7 +24,7 @@ import { IOtherSiteInfo, IWebsiteDetails, WebsiteYaml } from '../../../common/se
import { getActiveWebsites, getAllWebsites } from '../../../common/utilities/WebsiteUtil';
import CurrentSiteContext from './CurrentSiteContext';
import path from 'path';
-import { getWebsiteRecordId } from '../../../common/utilities/WorkspaceInfoFinderUtil';
+import { getWebsiteRecordId, hasWebsiteYaml, hasPowerPagesSiteFolder, getWebsiteYamlPath } from '../../../common/utilities/WorkspaceInfoFinderUtil';
import { isEdmEnvironment } from '../../../common/copilot/dataverseMetadata';
import { IWebsiteInfo } from './models/IWebsiteInfo';
import moment from 'moment';
@@ -32,6 +32,8 @@ import { SiteVisibility } from './models/SiteVisibility';
import { getBaseEventInfo, traceError, traceInfo } from './TelemetryHelper';
import { IPowerPagesConfig, IPowerPagesConfigData } from './models/IPowerPagesConfig';
import { oneDSLoggerWrapper } from '../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper';
+import { CodeQLAction } from './actions/codeQLAction';
+import { getDefaultCodeQLDatabasePath } from './ActionsHubUtils';
const sortByCreatedOn = (item1: T, item2: T): number => {
const date1 = new Date(item1.createdOn || '').valueOf(); //NaN if createdOn is null or undefined
@@ -571,19 +573,18 @@ export function findOtherSites(knownSiteIds: Set, fsModule = fs, yamlMod
// Check each directory for website.yml or .powerpages-site folder
const otherSites: IOtherSiteInfo[] = [];
for (const dir of directories) {
- let websiteYamlPath = path.join(dir, WEBSITE_YML);
- let hasWebsiteYaml = fsModule.existsSync(websiteYamlPath);
- const powerPagesSiteFolderPath = path.join(dir, POWERPAGES_SITE_FOLDER);
- const hasPowerPagesSiteFolder = fsModule.existsSync(powerPagesSiteFolderPath);
+ let websiteYamlPath = getWebsiteYamlPath(dir);
+ let hasWebsiteYamlFile = hasWebsiteYaml(dir);
+ const hasPowerPagesSiteFolderExists = hasPowerPagesSiteFolder(dir);
let workingDir = dir;
- if (hasPowerPagesSiteFolder) {
+ if (hasPowerPagesSiteFolderExists) {
workingDir = path.join(dir, POWERPAGES_SITE_FOLDER);
- websiteYamlPath = path.join(workingDir, WEBSITE_YML);
- hasWebsiteYaml = fsModule.existsSync(websiteYamlPath);
+ websiteYamlPath = getWebsiteYamlPath(workingDir);
+ hasWebsiteYamlFile = hasWebsiteYaml(workingDir);
}
- if (hasWebsiteYaml) {
+ if (hasWebsiteYamlFile) {
try {
// Use the utility function to get website record ID
const websiteId = getWebsiteRecordId(workingDir);
@@ -598,7 +599,7 @@ export function findOtherSites(knownSiteIds: Set, fsModule = fs, yamlMod
name: websiteData?.adx_name || path.basename(dir), // Use folder name as fallback
websiteId: websiteId,
folderPath: dir,
- isCodeSite: hasPowerPagesSiteFolder
+ isCodeSite: hasPowerPagesSiteFolderExists
});
}
} catch (error) {
@@ -939,3 +940,85 @@ export const reactivateSite = async (siteTreeItem: SiteTreeItem) => {
await vscode.env.openExternal(vscode.Uri.parse(reactivateSiteUrl));
};
+
+export const runCodeQLScreening = async (siteTreeItem?: SiteTreeItem) => {
+ traceInfo(Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_CALLED, { methodName: runCodeQLScreening.name });
+
+ try {
+ // Get the current site path
+ let sitePath = "";
+ if (siteTreeItem && siteTreeItem.contextValue === Constants.ContextValues.OTHER_SITE) {
+ sitePath = siteTreeItem.siteInfo.folderPath || "";
+ } else {
+ sitePath = CurrentSiteContext.currentSiteFolderPath || "";
+ }
+
+ if (!sitePath) {
+ await vscode.window.showErrorMessage(Constants.Strings.CODEQL_CURRENT_SITE_PATH_NOT_FOUND);
+ return;
+ }
+
+ if(!hasPowerPagesSiteFolder(sitePath)) {
+ traceInfo(Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_NOT_SUPPORTED, { methodName: runCodeQLScreening.name });
+ await vscode.window.showErrorMessage(Constants.Strings.CODEQL_SCREENING_NOT_SUPPORTED);
+ return;
+ }
+
+ // Check if CodeQL extension is installed
+ const codeQLExtension = vscode.extensions.getExtension(CODEQL_EXTENSION_ID);
+
+ if (!codeQLExtension) {
+ // Prompt user to install the CodeQL extension
+ const install = await vscode.window.showWarningMessage(
+ Constants.Strings.CODEQL_EXTENSION_NOT_INSTALLED,
+ Constants.Strings.INSTALL,
+ Constants.Strings.CANCEL
+ );
+
+ if (install === Constants.Strings.INSTALL) {
+ await vscode.commands.executeCommand('workbench.extensions.search', CODEQL_EXTENSION_ID);
+ traceInfo(Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_EXTENSION_NOT_INSTALLED, { methodName: runCodeQLScreening.name });
+ return;
+ } else {
+ return;
+ }
+ }
+
+ // Use default database location (site folder)
+ const databaseLocation = getDefaultCodeQLDatabasePath();
+
+ traceInfo(Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_EXTENSION_INSTALLED, { methodName: runCodeQLScreening.name });
+
+ const codeQLAction = new CodeQLAction();
+
+ try {
+ await showProgressWithNotification(
+ Constants.Strings.CODEQL_SCREENING_STARTED,
+ async () => {
+ // Use a custom method that allows specifying the database location
+ await codeQLAction.executeCodeQLAnalysisWithCustomPath(sitePath, databaseLocation);
+ }
+ );
+
+ traceInfo(Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_COMPLETED, { methodName: runCodeQLScreening.name });
+ } catch (error) {
+ traceError(
+ Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_FAILED,
+ error as Error,
+ { methodName: runCodeQLScreening.name }
+ );
+ await vscode.window.showErrorMessage(Constants.Strings.CODEQL_SCREENING_FAILED);
+ } finally {
+ //codeQLAction.dispose();
+ }
+
+ } catch (error) {
+ traceError(
+ Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_FAILED,
+ error as Error,
+ { methodName: runCodeQLScreening.name }
+ );
+ await vscode.window.showErrorMessage(Constants.Strings.CODEQL_SCREENING_FAILED);
+ }
+};
+
diff --git a/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts b/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts
index 3cb946365..8991fcf59 100644
--- a/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts
+++ b/src/client/power-pages/actions-hub/ActionsHubTreeDataProvider.ts
@@ -11,7 +11,7 @@ import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLo
import { EnvironmentGroupTreeItem } from "./tree-items/EnvironmentGroupTreeItem";
import { IEnvironmentInfo } from "./models/IEnvironmentInfo";
import { PacTerminal } from "../../lib/PacTerminal";
-import { fetchWebsites, openActiveSitesInStudio, openInactiveSitesInStudio, previewSite, createNewAuthProfile, refreshEnvironment, showEnvironmentDetails, switchEnvironment, revealInOS, openSiteManagement, uploadSite, showSiteDetails, downloadSite, openInStudio, reactivateSite } from "./ActionsHubCommandHandlers";
+import { fetchWebsites, openActiveSitesInStudio, openInactiveSitesInStudio, previewSite, createNewAuthProfile, refreshEnvironment, showEnvironmentDetails, switchEnvironment, revealInOS, openSiteManagement, uploadSite, showSiteDetails, downloadSite, openInStudio, reactivateSite, runCodeQLScreening } from "./ActionsHubCommandHandlers";
import PacContext from "../../pac/PacContext";
import CurrentSiteContext from "./CurrentSiteContext";
import { IOtherSiteInfo, IWebsiteDetails } from "../../../common/services/Interfaces";
@@ -29,8 +29,10 @@ export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider {
@@ -131,7 +133,7 @@ export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider await refreshEnvironment(pacTerminal)),
@@ -166,5 +168,13 @@ export class ActionsHubTreeDataProvider implements vscode.TreeDataProvider {
+ // Use a temporary directory for the CodeQL database
+ const tempDir = os.tmpdir();
+ const dbName = `codeql-database-${Date.now()}`;
+ return path.join(tempDir, dbName);
+};
+
diff --git a/src/client/power-pages/actions-hub/Constants.ts b/src/client/power-pages/actions-hub/Constants.ts
index 0833fdd31..2a27ca29e 100644
--- a/src/client/power-pages/actions-hub/Constants.ts
+++ b/src/client/power-pages/actions-hub/Constants.ts
@@ -51,7 +51,84 @@ export const Constants = {
UPLOAD_CODE_SITE_COMPILED_OUTPUT_FOLDER_NOT_FOUND: vscode.l10n.t("Please select the folder that contains your compiled output to upload your site."),
UPLOAD_CODE_SITE_FAILED: vscode.l10n.t("Upload failed. Please try again later."),
POWER_PAGES_CONFIG_FILE_NAME: "powerpages.config.json",
- MISSING_REACTIVATION_URL_INFO: vscode.l10n.t("Missing required site information for reactivation.")
+ MISSING_REACTIVATION_URL_INFO: vscode.l10n.t("Missing required site information for reactivation."),
+ INSTALL: vscode.l10n.t("Install"),
+ CANCEL: vscode.l10n.t("Cancel"),
+ CODEQL_EXTENSION_NOT_INSTALLED: vscode.l10n.t("The CodeQL extension is required to run this command. Do you want to install it now?"),
+ CODEQL_SCREENING_STARTED: vscode.l10n.t("CodeQL screening started. Creating database and analyzing"),
+ CODEQL_DATABASE_CREATED: vscode.l10n.t("CodeQL database created successfully. You can now run queries from the CodeQL extension."),
+ CODEQL_SCREENING_FAILED: vscode.l10n.t("CodeQL screening failed. Please try again later."),
+ CODEQL_CURRENT_SITE_PATH_NOT_FOUND: vscode.l10n.t("Current site path not found. Please ensure you have a site folder open."),
+ CODEQL_GUIDE_MESSAGE: vscode.l10n.t("CodeQL database created. You can now:\n\n1. Run custom queries if you have any\n2. Use the prebuilt queries from the CodeQL extension\n\nCheck the CodeQL extension panel for available queries."),
+ SARIF_VIEWER_NOT_INSTALLED: vscode.l10n.t("The SARIF Viewer extension is recommended for viewing CodeQL results. Would you like to install it now?"),
+ SARIF_VIEWER_INSTALL_FAILED: vscode.l10n.t("Failed to install SARIF viewer extension. Opening results as text file."),
+ OPEN_AS_TEXT: vscode.l10n.t("Open as Text"),
+ CODEQL_ANALYSIS_CHANNEL_NAME: vscode.l10n.t("CodeQL Analysis"),
+ CODEQL_ANALYSIS_STARTING: vscode.l10n.t("Starting CodeQL analysis for: {0}"),
+ CODEQL_DATABASE_LOCATION: vscode.l10n.t("Database location: {0}"),
+ CODEQL_CREATING_DATABASE: vscode.l10n.t("Creating CodeQL database..."),
+ CODEQL_RUNNING_ANALYSIS: vscode.l10n.t("Running CodeQL analysis..."),
+ CODEQL_ANALYSIS_COMPLETED: vscode.l10n.t("Analysis completed using query suite: {0}"),
+ CODEQL_QUERY_SUITE_FAILED: vscode.l10n.t("Primary query suite {0} failed: {1}"),
+ CODEQL_ANALYSIS_COMPLETE: vscode.l10n.t("Analysis complete! Results saved to: {0}"),
+ CODEQL_ANALYSIS_ERROR: vscode.l10n.t("Error during CodeQL analysis: {0}"),
+ CODEQL_ANALYSIS_FAILED: vscode.l10n.t("CodeQL analysis failed: {0}"),
+ CODEQL_CLI_FOUND_AT: vscode.l10n.t("Found CodeQL CLI at: {0}"),
+ CODEQL_CLI_NOT_INSTALLED: vscode.l10n.t("CodeQL CLI is not installed or not in PATH. Please install the CodeQL extension or add CodeQL CLI to your PATH."),
+ CODEQL_VERSION: vscode.l10n.t("CodeQL version: {0}"),
+ CODEQL_EXTENSION_NOT_INSTALLED_LOG: vscode.l10n.t("CodeQL extension is not installed."),
+ CODEQL_ACTIVATING_EXTENSION: vscode.l10n.t("Activating CodeQL extension..."),
+ CODEQL_USER_DATA_PATH_ERROR: vscode.l10n.t("Could not determine user data path."),
+ CODEQL_LOOKING_FOR_CLI: vscode.l10n.t("Looking for CodeQL CLI in: {0}"),
+ CODEQL_GLOBAL_STORAGE_NOT_EXISTS: vscode.l10n.t("CodeQL global storage path does not exist: {0}"),
+ CODEQL_CLI_VIA_API: vscode.l10n.t("Found CodeQL CLI via extension API: {0}"),
+ CODEQL_API_ERROR: vscode.l10n.t("Error getting CLI path from extension API: {0}"),
+ CODEQL_CLI_NOT_LOCATED: vscode.l10n.t("Could not locate CodeQL CLI in global storage."),
+ CODEQL_EXTENSION_ACCESS_ERROR: vscode.l10n.t("Error accessing CodeQL extension: {0}"),
+ CODEQL_DISTRIBUTION_DIRS: vscode.l10n.t("Found distribution directories: {0}"),
+ CODEQL_CLI_SEARCH_ERROR: vscode.l10n.t("Error searching for CodeQL CLI: {0}"),
+ CODEQL_EXECUTING_COMMAND: vscode.l10n.t("Executing: {0}"),
+ CODEQL_COMMAND_SUCCESS: vscode.l10n.t("Command completed successfully (exit code: {0})"),
+ CODEQL_COMMAND_FAILED: vscode.l10n.t("Command failed with exit code: {0}"),
+ CODEQL_COMMAND_FAILED_ERROR: vscode.l10n.t("Command failed with exit code {0}: {1}"),
+ CODEQL_PROCESS_ERROR: vscode.l10n.t("Process error: {0}"),
+ CODEQL_ISSUES_FOUND: vscode.l10n.t("Found {0} issue(s):"),
+ CODEQL_NO_MESSAGE: vscode.l10n.t("No message"),
+ CODEQL_FILE_LABEL: vscode.l10n.t("File: {0}"),
+ CODEQL_LINE_LABEL: vscode.l10n.t("Line: {0}"),
+ CODEQL_NO_ISSUES_FOUND: vscode.l10n.t("No issues found!"),
+ CODEQL_NO_ANALYSIS_RESULTS: vscode.l10n.t("No analysis results found."),
+ CODEQL_ANALYSIS_SUCCESS_NO_ISSUES: vscode.l10n.t("CodeQL analysis completed successfully with no issues found! π"),
+ CODEQL_ERROR_READING_RESULTS: vscode.l10n.t("Error reading results: {0}"),
+ CODEQL_SARIF_VIEWER_NOT_FOUND: vscode.l10n.t("SARIF viewer extension not found."),
+ CODEQL_INSTALLING_SARIF_VIEWER: vscode.l10n.t("Installing SARIF viewer extension..."),
+ CODEQL_SARIF_VIEWER_INSTALLED: vscode.l10n.t("SARIF viewer extension installed successfully. Activating..."),
+ CODEQL_OPENING_WITH_NEW_SARIF_VIEWER: vscode.l10n.t("Opening results with newly installed SARIF viewer..."),
+ CODEQL_SARIF_VIEWER_OPENED: vscode.l10n.t("Results opened in SARIF viewer successfully."),
+ CODEQL_SARIF_VIEWER_API_NOT_AVAILABLE: vscode.l10n.t("Extension installed but API not available yet. Opening as text file..."),
+ CODEQL_SARIF_VIEWER_INSTALL_ERROR: vscode.l10n.t("Failed to install SARIF viewer extension: {0}"),
+ CODEQL_USER_CANCELLED: vscode.l10n.t("User cancelled opening results."),
+ CODEQL_ACTIVATING_SARIF_VIEWER: vscode.l10n.t("Activating SARIF viewer extension..."),
+ CODEQL_OPENING_WITH_SARIF_VIEWER: vscode.l10n.t("Opening results with SARIF viewer..."),
+ CODEQL_SARIF_VIEWER_API_FALLBACK: vscode.l10n.t("SARIF viewer extension does not expose expected API. Falling back to text editor..."),
+ CODEQL_SARIF_VIEWER_ERROR: vscode.l10n.t("Error opening with SARIF viewer: {0}"),
+ CODEQL_ANALYSIS_COMPLETED_OPEN_FILE: vscode.l10n.t("CodeQL analysis completed. Would you like to open the full results file?"),
+ CODEQL_OPEN_RESULTS: vscode.l10n.t("Open Results"),
+ CODEQL_CLOSE: vscode.l10n.t("Close"),
+ CODEQL_ERROR_OPENING_RESULTS: vscode.l10n.t("Error opening results file: {0}"),
+ CODEQL_DATABASE_FOLDER_PROMPT: vscode.l10n.t("Select folder for CodeQL database"),
+ CODEQL_USE_CURRENT_SITE_FOLDER: vscode.l10n.t("Use current site folder"),
+ CODEQL_SCREENING_NOT_SUPPORTED: vscode.l10n.t("CodeQL screening is not supported for this site."),
+ CODEQL_CONFIG_FILE_FOUND: vscode.l10n.t("Found PowerPages config file: {0}"),
+ CODEQL_USING_CUSTOM_QUERY: vscode.l10n.t("Using custom CodeQL query suite: {0}"),
+ CODEQL_ADDED_DEFAULT_QUERY_TO_CONFIG: vscode.l10n.t("Added default query suite to config: {0}"),
+ CODEQL_CREATED_CONFIG_FILE: vscode.l10n.t("Created PowerPages config file at {0} with default query suite: {1}"),
+ CODEQL_CONFIG_ERROR: vscode.l10n.t("Error reading config file: {0}"),
+ CODEQL_USING_DEFAULT_QUERY: vscode.l10n.t("Using default query suite: {0}"),
+ CODEQL_CONFIG_FILE_CREATED_SUCCESSFULLY: vscode.l10n.t("PowerPages config file created successfully: {0}"),
+ CODEQL_CONFIG_FILE_CREATE_ERROR: vscode.l10n.t("Error creating config file: {0}"),
+ CODEQL_CONFIG_FILE_UPDATED_SUCCESSFULLY: vscode.l10n.t("PowerPages config file updated successfully: {0}"),
+ CODEQL_CONFIG_FILE_UPDATE_ERROR: vscode.l10n.t("Error updating config file: {0}")
},
EventNames: {
ACTIONS_HUB_ENABLED: "ActionsHubEnabled",
@@ -109,7 +186,14 @@ export const Constants = {
ACTIONS_HUB_UPLOAD_CODE_SITE_CALLED: "ActionsHubUploadCodeSiteCalled",
ACTIONS_HUB_UPLOAD_CODE_SITE_FAILED: "ActionsHubUploadCodeSiteFailed",
ACTIONS_HUB_UPLOAD_OTHER_CODE_SITE_PAC_TRIGGERED: "ActionsHubUploadOtherCodeSitePacTriggered",
- POWER_PAGES_CONFIG_PARSE_FAILED: "PowerPagesConfigParseFailed"
+ POWER_PAGES_CONFIG_PARSE_FAILED: "PowerPagesConfigParseFailed",
+ ACTIONS_HUB_CODEQL_SCREENING_CALLED: "ActionsHubCodeQLScreeningCalled",
+ ACTIONS_HUB_CODEQL_SCREENING_FAILED: "ActionsHubCodeQLScreeningFailed",
+ ACTIONS_HUB_CODEQL_SCREENING_EXTENSION_NOT_INSTALLED: "ActionsHubCodeQLScreeningExtensionNotInstalled",
+ ACTIONS_HUB_CODEQL_SCREENING_EXTENSION_INSTALLED: "ActionsHubCodeQLScreeningExtensionInstalled",
+ ACTIONS_HUB_CODEQL_SCREENING_DATABASE_CREATED: "ActionsHubCodeQLScreeningDatabaseCreated",
+ ACTIONS_HUB_CODEQL_SCREENING_NOT_SUPPORTED: "ActionsHubCodeQLScreeningNotSupported",
+ ACTIONS_HUB_CODEQL_SCREENING_COMPLETED: "ActionsHubCodeQLScreeningCompleted"
},
FeatureNames: {
REFRESH_ENVIRONMENT: "RefreshEnvironment"
diff --git a/src/client/power-pages/actions-hub/actions/codeQLAction.ts b/src/client/power-pages/actions-hub/actions/codeQLAction.ts
new file mode 100644
index 000000000..208ea83a2
--- /dev/null
+++ b/src/client/power-pages/actions-hub/actions/codeQLAction.ts
@@ -0,0 +1,469 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import * as vscode from 'vscode';
+import { exec, spawn } from 'child_process';
+import * as path from 'path';
+import * as fs from 'fs';
+import { CODEQL_EXTENSION_ID } from '../../../../common/constants';
+import { Constants } from '../Constants';
+
+interface PowerPagesConfig {
+ codeQlQuery?: string;
+}
+
+export class CodeQLAction {
+ private outputChannel: vscode.OutputChannel;
+
+ constructor() {
+ this.outputChannel = vscode.window.createOutputChannel(Constants.Strings.CODEQL_ANALYSIS_CHANNEL_NAME);
+ }
+
+ public async executeCodeQLAnalysisWithCustomPath(sitePath: string, databaseLocation: string): Promise {
+ this.outputChannel.show();
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ANALYSIS_STARTING, path.basename(sitePath)));
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_DATABASE_LOCATION, databaseLocation));
+
+ try {
+ const codeqlCliPath = await this.checkCodeQLInstallation();
+ const databaseName = `codeql-db-${path.basename(sitePath)}`;
+ const dbPath = path.join(databaseLocation, '.codeql', databaseName);
+
+ if (!fs.existsSync(path.dirname(dbPath))) {
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
+ }
+
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_CREATING_DATABASE);
+ await this.runCodeQLCommand(`"${codeqlCliPath}" database create "${dbPath}" --language=javascript-typescript --source-root="${sitePath}" --overwrite --no-run-unnecessary-builds`);
+
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_RUNNING_ANALYSIS);
+ const resultsPath = path.join(path.dirname(dbPath), 'results.sarif');
+
+ // Get the query suite from config or use default
+ const querySuite = await this.getCodeQLQuerySuite(sitePath);
+
+ try {
+ // Use the correct syntax for JavaScript code scanning query suite
+ await this.runCodeQLCommand(`"${codeqlCliPath}" database analyze "${dbPath}" ${querySuite} --format=sarif-latest --output="${resultsPath}" --download`);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ANALYSIS_COMPLETED, querySuite));
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_QUERY_SUITE_FAILED, querySuite, errorMessage));
+ }
+
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ANALYSIS_COMPLETE, resultsPath));
+
+ if (fs.existsSync(resultsPath)) {
+ await this.displayResults(resultsPath);
+ }
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ANALYSIS_ERROR, errorMessage));
+ vscode.window.showErrorMessage(vscode.l10n.t(Constants.Strings.CODEQL_ANALYSIS_FAILED, errorMessage));
+ }
+ }
+
+ private async checkCodeQLInstallation(): Promise {
+ // First try to get the CodeQL CLI path from the CodeQL extension
+ const codeqlCliPath = await this.getCodeQLCliPath();
+ if (codeqlCliPath) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CLI_FOUND_AT, codeqlCliPath));
+ return codeqlCliPath;
+ }
+
+ // Fallback to checking if CodeQL is in PATH
+ return new Promise((resolve, reject) => {
+ exec('codeql version', (error, stdout, _) => {
+ if (error) {
+ reject(new Error(Constants.Strings.CODEQL_CLI_NOT_INSTALLED));
+ } else {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_VERSION, stdout.trim()));
+ resolve('codeql'); // Use the command as-is from PATH
+ }
+ });
+ });
+ }
+
+
+ private async getCodeQLCliPath(): Promise {
+ try {
+ // Get the CodeQL extension
+ const codeqlExtension = vscode.extensions.getExtension(CODEQL_EXTENSION_ID);
+
+ if (!codeqlExtension) {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_EXTENSION_NOT_INSTALLED_LOG);
+ return null;
+ }
+
+ if (!codeqlExtension.isActive) {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_ACTIVATING_EXTENSION);
+ await codeqlExtension.activate();
+ }
+
+ // The CodeQL extension stores the CLI in VS Code's global storage
+ // Path pattern: %APPDATA%\Code\User\globalStorage\github.vscode-codeql\distribution{N}\codeql\codeql.exe
+ const userDataPath = process.env.APPDATA || process.env.HOME;
+ if (!userDataPath) {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_USER_DATA_PATH_ERROR);
+ return null;
+ }
+
+ const codeqlBasePath = path.join(userDataPath, 'Code', 'User', 'globalStorage', 'github.vscode-codeql');
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_LOOKING_FOR_CLI, codeqlBasePath));
+
+ if (!fs.existsSync(codeqlBasePath)) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_GLOBAL_STORAGE_NOT_EXISTS, codeqlBasePath));
+ return null;
+ }
+
+ const foundCliPath = await this.searchCodeQLInPath(codeqlBasePath);
+ if (foundCliPath) {
+ return foundCliPath;
+ }
+
+ // Try to get the CLI path from the extension's API
+ if (codeqlExtension.exports && typeof codeqlExtension.exports.getCliPath === 'function') {
+ try {
+ const cliPath = await codeqlExtension.exports.getCliPath();
+ if (cliPath && fs.existsSync(cliPath)) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CLI_VIA_API, cliPath));
+ return cliPath;
+ }
+ } catch (apiError) {
+ const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_API_ERROR, errorMessage));
+ }
+ }
+
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_CLI_NOT_LOCATED);
+ return null;
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_EXTENSION_ACCESS_ERROR, errorMessage));
+ return null;
+ }
+ }
+
+ private async searchCodeQLInPath(basePath: string): Promise {
+ try {
+ const distributionDirs = fs.readdirSync(basePath)
+ .filter(dir => dir.startsWith('distribution'))
+ .sort()
+ .reverse(); // Get the latest distribution first
+
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_DISTRIBUTION_DIRS, distributionDirs.join(', ')));
+
+ for (const distributionDir of distributionDirs) {
+ const codeqlPaths = [
+ path.join(basePath, distributionDir, 'codeql', 'codeql.exe'), // Windows
+ path.join(basePath, distributionDir, 'codeql', 'codeql') // Unix/Linux/macOS
+ ];
+
+ for (const codeqlCliPath of codeqlPaths) {
+ if (fs.existsSync(codeqlCliPath)) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CLI_FOUND_AT, codeqlCliPath));
+ return codeqlCliPath;
+ }
+ }
+ }
+
+ return null;
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CLI_SEARCH_ERROR, errorMessage));
+ return null;
+ }
+ }
+
+ private async runCodeQLCommand(command: string): Promise {
+ return new Promise((resolve, reject) => {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_EXECUTING_COMMAND, command));
+
+ // Parse the command and arguments
+ const parts = this.parseCommand(command);
+ const cmd = parts[0];
+ const args = parts.slice(1);
+
+ const process = spawn(cmd, args, {
+ shell: true,
+ stdio: ['pipe', 'pipe', 'pipe']
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ // Stream stdout in real-time
+ process.stdout?.on('data', (data: Buffer) => {
+ const output = data.toString();
+ stdout += output;
+ // Show real-time output in the output channel
+ this.outputChannel.appendLine(`[STDOUT] ${output.trim()}`);
+ });
+
+ // Stream stderr in real-time
+ process.stderr?.on('data', (data: Buffer) => {
+ const output = data.toString();
+ stderr += output;
+ // Show real-time stderr in the output channel
+ this.outputChannel.appendLine(`[STDERR] ${output.trim()}`);
+ });
+
+ process.on('close', (code: number) => {
+ if (code === 0) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_COMMAND_SUCCESS, code.toString()));
+ resolve(stdout);
+ } else {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_COMMAND_FAILED, code.toString()));
+ reject(new Error(vscode.l10n.t(Constants.Strings.CODEQL_COMMAND_FAILED_ERROR, code.toString(), stderr || 'Unknown error')));
+ }
+ });
+
+ process.on('error', (error: Error) => {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_PROCESS_ERROR, error.message));
+ reject(new Error(vscode.l10n.t(Constants.Strings.CODEQL_PROCESS_ERROR, error.message)));
+ });
+ });
+ }
+
+ private parseCommand(command: string): string[] {
+ // Simple command parsing that handles quoted arguments
+ const parts: string[] = [];
+ let current = '';
+ let inQuotes = false;
+ let quoteChar = '';
+
+ for (let i = 0; i < command.length; i++) {
+ const char = command[i];
+
+ if ((char === '"' || char === "'") && !inQuotes) {
+ inQuotes = true;
+ quoteChar = char;
+ } else if (char === quoteChar && inQuotes) {
+ inQuotes = false;
+ quoteChar = '';
+ } else if (char === ' ' && !inQuotes) {
+ if (current.trim()) {
+ parts.push(current.trim());
+ current = '';
+ }
+ } else {
+ current += char;
+ }
+ }
+
+ if (current.trim()) {
+ parts.push(current.trim());
+ }
+
+ return parts;
+ }
+
+ private async displayResults(resultsPath: string): Promise {
+ try {
+ const resultsContent = fs.readFileSync(resultsPath, 'utf8');
+ const results = JSON.parse(resultsContent);
+
+ let hasIssues = false;
+
+ if (results.runs && results.runs.length > 0) {
+ const run = results.runs[0];
+ if (run.results && run.results.length > 0) {
+ hasIssues = true;
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ISSUES_FOUND, run.results.length.toString()));
+
+ run.results.forEach((result: { message?: { text?: string }; locations?: Array<{ physicalLocation?: { artifactLocation?: { uri?: string }; region?: { startLine?: number } } }> }, index: number) => {
+ this.outputChannel.appendLine(`\n${index + 1}. ${result.message?.text || Constants.Strings.CODEQL_NO_MESSAGE}`);
+ if (result.locations && result.locations.length > 0) {
+ const location = result.locations[0];
+ if (location.physicalLocation) {
+ const file = location.physicalLocation.artifactLocation?.uri;
+ const startLine = location.physicalLocation.region?.startLine;
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_FILE_LABEL, file || ''));
+ if (startLine) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_LINE_LABEL, startLine.toString()));
+ }
+ }
+ }
+ });
+ } else {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_NO_ISSUES_FOUND);
+ }
+ } else {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_NO_ANALYSIS_RESULTS);
+ }
+
+ // Only open SARIF viewer if issues are found
+ if (hasIssues) {
+ await this.openWithSarifViewer(resultsPath);
+ } else {
+ // Show a simple success notification for clean results
+ vscode.window.showInformationMessage(Constants.Strings.CODEQL_ANALYSIS_SUCCESS_NO_ISSUES);
+ }
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ERROR_READING_RESULTS, errorMessage));
+ }
+ }
+
+ private async openWithSarifViewer(resultsPath: string): Promise {
+ try {
+ const sarifExt = vscode.extensions.getExtension('MS-SarifVSCode.sarif-viewer');
+
+ if (!sarifExt) {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_SARIF_VIEWER_NOT_FOUND);
+
+ const installExtension = await vscode.window.showInformationMessage(
+ Constants.Strings.SARIF_VIEWER_NOT_INSTALLED,
+ Constants.Strings.INSTALL,
+ Constants.Strings.OPEN_AS_TEXT,
+ Constants.Strings.CANCEL
+ );
+
+ if (installExtension === Constants.Strings.INSTALL) {
+ try {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_INSTALLING_SARIF_VIEWER);
+ await vscode.commands.executeCommand('workbench.extensions.installExtension', 'MS-SarifVSCode.sarif-viewer');
+
+ // Wait a moment for the extension to be available
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // Try to get the extension again after installation
+ const newSarifExt = vscode.extensions.getExtension('MS-SarifVSCode.sarif-viewer');
+ if (newSarifExt) {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_SARIF_VIEWER_INSTALLED);
+ await newSarifExt.activate();
+
+ if (newSarifExt.exports && typeof newSarifExt.exports.openLogs === 'function') {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_OPENING_WITH_NEW_SARIF_VIEWER);
+ await newSarifExt.exports.openLogs([vscode.Uri.file(resultsPath)]);
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_SARIF_VIEWER_OPENED);
+ return;
+ }
+ }
+
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_SARIF_VIEWER_API_NOT_AVAILABLE);
+ await this.fallbackToTextEditor(resultsPath);
+
+ } catch (installError) {
+ const errorMessage = installError instanceof Error ? installError.message : String(installError);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_SARIF_VIEWER_INSTALL_ERROR, errorMessage));
+ vscode.window.showErrorMessage(Constants.Strings.SARIF_VIEWER_INSTALL_FAILED);
+ await this.fallbackToTextEditor(resultsPath);
+ }
+ } else if (installExtension === Constants.Strings.OPEN_AS_TEXT) {
+ await this.fallbackToTextEditor(resultsPath);
+ } else {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_USER_CANCELLED);
+ }
+ return;
+ }
+
+ if (!sarifExt.isActive) {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_ACTIVATING_SARIF_VIEWER);
+ await sarifExt.activate();
+ }
+
+ if (sarifExt.exports && typeof sarifExt.exports.openLogs === 'function') {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_OPENING_WITH_SARIF_VIEWER);
+ await sarifExt.exports.openLogs([vscode.Uri.file(resultsPath)]);
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_SARIF_VIEWER_OPENED);
+ } else {
+ this.outputChannel.appendLine(Constants.Strings.CODEQL_SARIF_VIEWER_API_FALLBACK);
+ await this.fallbackToTextEditor(resultsPath);
+ }
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_SARIF_VIEWER_ERROR, errorMessage));
+ await this.fallbackToTextEditor(resultsPath);
+ }
+ } private async fallbackToTextEditor(resultsPath: string): Promise {
+ try {
+ // Offer to open the full SARIF file as text
+ const openFile = await vscode.window.showInformationMessage(
+ Constants.Strings.CODEQL_ANALYSIS_COMPLETED_OPEN_FILE,
+ Constants.Strings.CODEQL_OPEN_RESULTS,
+ Constants.Strings.CODEQL_CLOSE
+ );
+
+ if (openFile === Constants.Strings.CODEQL_OPEN_RESULTS) {
+ const document = await vscode.workspace.openTextDocument(resultsPath);
+ await vscode.window.showTextDocument(document);
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ERROR_OPENING_RESULTS, errorMessage));
+ }
+ }
+
+ private async getCodeQLQuerySuite(sitePath: string): Promise {
+ const configPath = path.join(sitePath, 'powerpages.config.json');
+ const defaultQuerySuite = 'codeql/javascript-queries:codeql-suites/javascript-security-and-quality.qls';
+
+ try {
+ if (fs.existsSync(configPath)) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CONFIG_FILE_FOUND, configPath));
+
+ const configContent = fs.readFileSync(configPath, 'utf8');
+ const config: PowerPagesConfig = JSON.parse(configContent);
+
+ if (config.codeQlQuery) {
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_USING_CUSTOM_QUERY, config.codeQlQuery));
+ return config.codeQlQuery;
+ } else {
+ // Add the default query suite to the existing config
+ config.codeQlQuery = defaultQuerySuite;
+ await this.updateConfigFile(configPath, config);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_ADDED_DEFAULT_QUERY_TO_CONFIG, defaultQuerySuite));
+ return defaultQuerySuite;
+ }
+ } else {
+ // Create new config file with default query suite
+ const config: PowerPagesConfig = {
+ codeQlQuery: defaultQuerySuite
+ };
+ await this.createConfigFile(configPath, config);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CREATED_CONFIG_FILE, configPath, defaultQuerySuite));
+ return defaultQuerySuite;
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CONFIG_ERROR, errorMessage));
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_USING_DEFAULT_QUERY, defaultQuerySuite));
+ return defaultQuerySuite;
+ }
+ }
+
+ private async createConfigFile(configPath: string, config: PowerPagesConfig): Promise {
+ try {
+ const configContent = JSON.stringify(config, null, 2);
+ fs.writeFileSync(configPath, configContent, 'utf8');
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CONFIG_FILE_CREATED_SUCCESSFULLY, configPath));
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CONFIG_FILE_CREATE_ERROR, errorMessage));
+ throw error;
+ }
+ }
+
+ private async updateConfigFile(configPath: string, config: PowerPagesConfig): Promise {
+ try {
+ const configContent = JSON.stringify(config, null, 2);
+ fs.writeFileSync(configPath, configContent, 'utf8');
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CONFIG_FILE_UPDATED_SUCCESSFULLY, configPath));
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ this.outputChannel.appendLine(vscode.l10n.t(Constants.Strings.CODEQL_CONFIG_FILE_UPDATE_ERROR, errorMessage));
+ throw error;
+ }
+ }
+
+ public dispose(): void {
+ this.outputChannel.dispose();
+ }
+}
diff --git a/src/client/test/Integration/power-pages/actions-hub/ActionsHubCommandHandlers.test.ts b/src/client/test/Integration/power-pages/actions-hub/ActionsHubCommandHandlers.test.ts
index 110e62117..da784523f 100644
--- a/src/client/test/Integration/power-pages/actions-hub/ActionsHubCommandHandlers.test.ts
+++ b/src/client/test/Integration/power-pages/actions-hub/ActionsHubCommandHandlers.test.ts
@@ -6,7 +6,7 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
-import { showEnvironmentDetails, refreshEnvironment, switchEnvironment, openActiveSitesInStudio, openInactiveSitesInStudio, createNewAuthProfile, previewSite, fetchWebsites, revealInOS, uploadSite, createKnownSiteIdsSet, findOtherSites, showSiteDetails, openSiteManagement, downloadSite, openInStudio } from '../../../../power-pages/actions-hub/ActionsHubCommandHandlers';
+import { showEnvironmentDetails, refreshEnvironment, switchEnvironment, openActiveSitesInStudio, openInactiveSitesInStudio, createNewAuthProfile, previewSite, fetchWebsites, revealInOS, uploadSite, createKnownSiteIdsSet, findOtherSites, showSiteDetails, openSiteManagement, downloadSite, openInStudio, runCodeQLScreening } from '../../../../power-pages/actions-hub/ActionsHubCommandHandlers';
import { Constants } from '../../../../power-pages/actions-hub/Constants';
import * as CommonUtils from '../../../../power-pages/commonUtility';
import { AuthInfo, CloudInstance, EnvironmentType, OrgInfo } from '../../../../pac/PacTypes';
@@ -1496,4 +1496,86 @@ describe('ActionsHubCommandHandlers', () => {
expect(mockOpenUrl.called).to.be.false;
});
});
+
+ describe('runCodeQLScreening', () => {
+ let mockShowErrorMessage: sinon.SinonStub;
+ let mockShowWarningMessage: sinon.SinonStub;
+ let mockGetExtension: sinon.SinonStub;
+ let mockShowProgressNotification: sinon.SinonStub;
+ let mockSiteTreeItem: SiteTreeItem;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ let mockHasPowerPagesSiteFolder: sinon.SinonStub;
+
+ beforeEach(() => {
+ mockShowErrorMessage = sandbox.stub(vscode.window, 'showErrorMessage');
+ mockShowWarningMessage = sandbox.stub(vscode.window, 'showWarningMessage');
+ mockGetExtension = sandbox.stub(vscode.extensions, 'getExtension');
+ mockShowProgressNotification = sandbox.stub(Utils, 'showProgressWithNotification').callsFake(async (title: string, task: (progress: vscode.Progress<{
+ message?: string;
+ increment?: number;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ }>) => Promise) => await task({} as unknown as vscode.Progress<{ message?: string; increment?: number }>));
+
+ mockHasPowerPagesSiteFolder = sandbox.stub(WorkspaceInfoFinderUtil, 'hasPowerPagesSiteFolder').returns(true);
+
+ mockSiteTreeItem = new SiteTreeItem({
+ name: "Test Site",
+ websiteId: "test-id",
+ dataModelVersion: 1,
+ status: WebsiteStatus.Active,
+ websiteUrl: 'https://test-site.com',
+ isCurrent: false,
+ siteVisibility: SiteVisibility.Private,
+ siteManagementUrl: "https://test-site-management.com",
+ createdOn: "2025-03-20",
+ creator: "Test Creator",
+ isCodeSite: false
+ });
+ });
+
+ it('should prompt to install CodeQL extension when not installed', async () => {
+ mockGetExtension.returns(undefined);
+ mockShowWarningMessage.resolves(Constants.Strings.INSTALL);
+ sandbox.stub(CurrentSiteContext, 'currentSiteFolderPath').get(() => 'C:\\test\\site\\path');
+
+ await runCodeQLScreening(mockSiteTreeItem);
+
+ expect(mockGetExtension.calledWith('github.vscode-codeql')).to.be.true;
+ expect(mockShowWarningMessage.calledWith(
+ Constants.Strings.CODEQL_EXTENSION_NOT_INSTALLED,
+ Constants.Strings.INSTALL,
+ Constants.Strings.CANCEL
+ )).to.be.true;
+ });
+
+ it('should show error when current site path not found', async () => {
+ mockGetExtension.returns({ id: 'github.vscode-codeql' });
+ sandbox.stub(CurrentSiteContext, 'currentSiteFolderPath').get(() => null);
+
+ await runCodeQLScreening(mockSiteTreeItem);
+
+ expect(mockShowErrorMessage.calledWith(Constants.Strings.CODEQL_CURRENT_SITE_PATH_NOT_FOUND)).to.be.true;
+ });
+
+ it('should create CodeQL database for current site when extension is installed', async () => {
+ mockGetExtension.returns({ id: 'github.vscode-codeql' });
+ sandbox.stub(CurrentSiteContext, 'currentSiteFolderPath').get(() => 'C:\\test\\site\\path');
+
+ await runCodeQLScreening(mockSiteTreeItem);
+
+ expect(mockShowProgressNotification.calledWith(Constants.Strings.CODEQL_SCREENING_STARTED)).to.be.true;
+ });
+
+ it('should handle errors gracefully', async () => {
+ mockGetExtension.returns({ id: 'github.vscode-codeql' });
+ sandbox.stub(CurrentSiteContext, 'currentSiteFolderPath').get(() => 'C:\\test\\site\\path');
+ mockShowProgressNotification.rejects(new Error('Test error'));
+
+ await runCodeQLScreening(mockSiteTreeItem);
+
+ expect(mockShowErrorMessage.calledWith(Constants.Strings.CODEQL_SCREENING_FAILED)).to.be.true;
+ expect(traceErrorStub.calledOnce).to.be.true;
+ expect(traceErrorStub.firstCall.args[0]).to.equal(Constants.EventNames.ACTIONS_HUB_CODEQL_SCREENING_FAILED);
+ });
+ });
});
diff --git a/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts b/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts
index b1925fea0..16f926082 100644
--- a/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts
+++ b/src/client/test/Integration/power-pages/actions-hub/ActionsHubTreeDataProvider.test.ts
@@ -63,7 +63,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register refresh command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'refreshEnvironment');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.refresh")).to.be.true;
@@ -75,7 +75,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register switchEnvironment command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'switchEnvironment');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.switchEnvironment")).to.be.true;
@@ -87,7 +87,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register showEnvironmentDetails command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'showEnvironmentDetails');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.showEnvironmentDetails")).to.be.true;
@@ -99,7 +99,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register openActiveSitesInStudio command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'openActiveSitesInStudio');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.openActiveSitesInStudio")).to.be.true;
@@ -111,7 +111,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register openInactiveSitesInStudio command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'openInactiveSitesInStudio');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.openInactiveSitesInStudio")).to.be.true;
@@ -123,7 +123,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register preview command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'previewSite');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.activeSite.preview")).to.be.true;
@@ -135,7 +135,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register newAuthProfile command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'createNewAuthProfile');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.newAuthProfile")).to.be.true;
@@ -147,7 +147,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register revealInOS commands", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'revealInOS');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.currentActiveSite.revealInOS.windows")).to.be.true;
@@ -163,7 +163,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register openSiteManagement commands", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'openSiteManagement');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.inactiveSite.openSiteManagement")).to.be.true;
@@ -176,7 +176,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register uploadSite command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'uploadSite');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite")).to.be.true;
@@ -188,7 +188,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register siteDetails command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'showSiteDetails');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.siteDetails")).to.be.true;
@@ -200,7 +200,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should register downloadSite command", async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'downloadSite');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.activeSite.downloadSite")).to.be.true;
@@ -212,7 +212,7 @@ describe("ActionsHubTreeDataProvider", () => {
it('should register openSiteInStudio command', async () => {
const mockCommandHandler = sinon.stub(CommandHandlers, 'openInStudio');
mockCommandHandler.resolves();
- const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
actionsHubTreeDataProvider["registerPanel"](pacTerminal);
expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.activeSite.openInStudio")).to.be.true;
@@ -220,12 +220,24 @@ describe("ActionsHubTreeDataProvider", () => {
await registerCommandStub.getCall(14).args[1]();
expect(mockCommandHandler.calledOnce).to.be.true;
});
+
+ it('should register runCodeQLScreening command', async () => {
+ const mockCommandHandler = sinon.stub(CommandHandlers, 'runCodeQLScreening');
+ mockCommandHandler.resolves();
+ const actionsHubTreeDataProvider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, true);
+ actionsHubTreeDataProvider["registerPanel"](pacTerminal);
+
+ expect(registerCommandStub.calledWith("microsoft.powerplatform.pages.actionsHub.currentActiveSite.runCodeQLScreening")).to.be.true;
+
+ await registerCommandStub.getCall(16).args[1]();
+ expect(mockCommandHandler.calledOnce).to.be.true;
+ });
});
describe('getTreeItem', () => {
it("should return the element in getTreeItem", () => {
const element = {} as ActionsHubTreeItem;
- const result = ActionsHubTreeDataProvider.initialize(context, pacTerminal).getTreeItem(element);
+ const result = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false).getTreeItem(element);
expect(result).to.equal(element);
});
});
@@ -244,7 +256,7 @@ describe("ActionsHubTreeDataProvider", () => {
sinon.stub(CommandHandlers, 'fetchWebsites').resolves({ activeSites: mockActiveSites, inactiveSites: mockInactiveSites, otherSites: otherSites });
PacContext['_authInfo'] = null;
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
await provider.getChildren();
expect(traceInfoStub.calledOnce).to.be.true;
@@ -265,7 +277,7 @@ describe("ActionsHubTreeDataProvider", () => {
sinon.stub(CommandHandlers, 'fetchWebsites').resolves({ activeSites: mockActiveSites, inactiveSites: mockInactiveSites, otherSites: otherSites });
PacContext['_authInfo'] = null;
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
const result = await provider.getChildren();
expect(result).to.not.be.null;
@@ -309,7 +321,7 @@ describe("ActionsHubTreeDataProvider", () => {
EnvironmentId: "test-env-id"
}));
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
const result = await provider.getChildren();
@@ -328,7 +340,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should return empty array when auth info is not available", async () => {
sinon.stub(PacContext, "AuthInfo").get(() => null);
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
provider["_loadWebsites"] = false;
const result = await provider.getChildren();
@@ -339,7 +351,7 @@ describe("ActionsHubTreeDataProvider", () => {
sinon.stub(PacContext, "AuthInfo").get(() => ({ OrganizationFriendlyName: 'Foo Bar' }));
sinon.stub(vscode.authentication, 'getSession').resolves({ accessToken: '' } as vscode.AuthenticationSession);
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
provider["_loadWebsites"] = false;
const result = await provider.getChildren();
@@ -348,7 +360,7 @@ describe("ActionsHubTreeDataProvider", () => {
it("should call element.getChildren when an element is passed", async () => {
const element = new SiteTreeItem({} as IWebsiteInfo);
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
sinon.stub(CommandHandlers, 'fetchWebsites').resolves({ activeSites: [], inactiveSites: [], otherSites: [] });
provider["_loadWebsites"] = false;
const getChildrenStub = sinon.stub(element, "getChildren").resolves([]);
@@ -390,7 +402,7 @@ describe("ActionsHubTreeDataProvider", () => {
OrganizationId: "",
OrganizationUniqueName: ""
};
- const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal);
+ const provider = ActionsHubTreeDataProvider.initialize(context, pacTerminal, false);
provider["_loadWebsites"] = true;
await provider.getChildren();
diff --git a/src/client/test/Integration/power-pages/actions-hub/actions/codeQLAction.test.ts b/src/client/test/Integration/power-pages/actions-hub/actions/codeQLAction.test.ts
new file mode 100644
index 000000000..cafe2f28f
--- /dev/null
+++ b/src/client/test/Integration/power-pages/actions-hub/actions/codeQLAction.test.ts
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import * as assert from 'assert';
+import * as sinon from 'sinon';
+import * as vscode from 'vscode';
+import { CodeQLAction } from '../../../../../power-pages/actions-hub/actions/codeQLAction';
+
+describe('CodeQLAction', () => {
+ let sandbox: sinon.SinonSandbox;
+ let codeqlAction: CodeQLAction;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+
+ // Mock the output channel
+ const mockOutputChannel = {
+ show: sandbox.stub(),
+ appendLine: sandbox.stub(),
+ dispose: sandbox.stub()
+ };
+
+ sandbox.stub(vscode.window, 'createOutputChannel').returns(mockOutputChannel as never);
+ codeqlAction = new CodeQLAction();
+ });
+
+ afterEach(() => {
+ codeqlAction.dispose();
+ sandbox.restore();
+ });
+
+ describe('constructor', () => {
+ it('should create output channel with correct name', () => {
+ const action = new CodeQLAction();
+ assert.ok(action);
+ action.dispose();
+ });
+ });
+
+ describe('dispose', () => {
+ it('should dispose output channel when dispose is called', () => {
+ const action = new CodeQLAction();
+ action.dispose();
+ // Should not throw when disposing
+ });
+ });
+
+ describe('executeCodeQLAnalysisWithCustomPath', () => {
+ it('should handle missing CodeQL extension gracefully', async () => {
+ const sitePath = '/test/site/path';
+ const databaseLocation = '/test/db/location';
+
+ // Mock VS Code API to simulate missing CodeQL extension
+ sandbox.stub(vscode.extensions, 'getExtension').returns(undefined);
+ const showErrorStub = sandbox.stub(vscode.window, 'showErrorMessage').resolves();
+
+ try {
+ await codeqlAction.executeCodeQLAnalysisWithCustomPath(sitePath, databaseLocation);
+ assert.ok(showErrorStub.called, 'Should show error when CodeQL extension is missing');
+ } catch (error) {
+ // Expected error for missing extension
+ assert.ok(true, 'Should handle missing extension gracefully');
+ }
+ });
+
+ it('should create output channel and show it during analysis', async () => {
+ const sitePath = '/test/site/path';
+ const databaseLocation = '/test/db/location';
+
+ // Mock VS Code API to avoid actual extension checks
+ sandbox.stub(vscode.extensions, 'getExtension').returns(undefined);
+ sandbox.stub(vscode.window, 'showErrorMessage').resolves();
+ const createChannelStub = vscode.window.createOutputChannel as sinon.SinonStub;
+
+ await codeqlAction.executeCodeQLAnalysisWithCustomPath(sitePath, databaseLocation);
+
+ assert.ok(createChannelStub.called, 'Should create output channel');
+ });
+
+ it('should handle CodeQL extension activation', async () => {
+ const sitePath = '/test/site/path';
+ const databaseLocation = '/test/db/location';
+
+ const mockExtension = {
+ isActive: false,
+ activate: sandbox.stub().resolves(),
+ exports: null
+ };
+
+ sandbox.stub(vscode.extensions, 'getExtension').returns(mockExtension as never);
+ sandbox.stub(vscode.window, 'showErrorMessage').resolves();
+
+ try {
+ await codeqlAction.executeCodeQLAnalysisWithCustomPath(sitePath, databaseLocation);
+ assert.ok(mockExtension.activate.called, 'Should attempt to activate CodeQL extension');
+ } catch (error) {
+ // Handle expected errors during test execution
+ assert.ok(true, 'Test completed');
+ }
+ });
+
+ });
+
+ describe('error handling', () => {
+ it('should handle exceptions gracefully during analysis', async () => {
+ // This test verifies that the method handles errors without crashing
+ const sitePath = '/test/site/path';
+ const databaseLocation = '/test/db/location';
+
+ // Mock extensions.getExtension to return undefined (no CodeQL extension)
+ sandbox.stub(vscode.extensions, 'getExtension').returns(undefined);
+ sandbox.stub(vscode.window, 'showErrorMessage').resolves();
+
+ // This should complete without throwing an unhandled error
+ await codeqlAction.executeCodeQLAnalysisWithCustomPath(sitePath, databaseLocation);
+
+ // Test passes if we reach this point
+ assert.ok(true, 'Should handle missing extension gracefully');
+ });
+
+ it('should handle null or undefined inputs gracefully', async () => {
+ // Test with null paths
+ sandbox.stub(vscode.extensions, 'getExtension').returns(undefined);
+ sandbox.stub(vscode.window, 'showErrorMessage').resolves();
+
+ // Should not throw with null/undefined inputs
+ await codeqlAction.executeCodeQLAnalysisWithCustomPath('', '');
+
+ assert.ok(true, 'Should handle empty string inputs without throwing');
+ });
+ });
+
+ describe('integration behavior', () => {
+ it('should work with basic mocked VS Code environment', () => {
+ // Test that the class can be instantiated and used in a basic VS Code mock environment
+ const action = new CodeQLAction();
+
+ assert.ok(action, 'Should create CodeQLAction instance');
+ assert.ok(typeof action.executeCodeQLAnalysisWithCustomPath === 'function', 'Should have main execution method');
+ assert.ok(typeof action.dispose === 'function', 'Should have dispose method');
+
+ action.dispose();
+ });
+
+ it('should handle VS Code API changes gracefully', async () => {
+ // Test behavior when VS Code APIs return unexpected values
+ const sitePath = '/test/site/path';
+ const databaseLocation = '/test/db/location';
+
+ sandbox.stub(vscode.extensions, 'getExtension').returns(null as never);
+ sandbox.stub(vscode.window, 'showErrorMessage').resolves();
+
+ // Should handle null return from getExtension
+ await codeqlAction.executeCodeQLAnalysisWithCustomPath(sitePath, databaseLocation);
+
+ assert.ok(true, 'Should handle null extension return gracefully');
+ });
+
+ });
+});
diff --git a/src/common/constants.ts b/src/common/constants.ts
index 7f78ffe46..bc7ac7e33 100644
--- a/src/common/constants.ts
+++ b/src/common/constants.ts
@@ -95,6 +95,7 @@ export const ADX_WEBPAGE = 'adx_webpage'
export const HTML_FILE_EXTENSION = '.html';
export const UTF8_ENCODING = 'utf8';
export const EDGE_TOOLS_EXTENSION_ID = 'ms-edgedevtools.vscode-edge-devtools';
+export const CODEQL_EXTENSION_ID = 'github.vscode-codeql';
export const ADX_WEBSITE_RECORDS_API_PATH = 'api/data/v9.2/adx_websites?$select=*&$expand=owninguser($select=systemuserid,fullname)';
export const POWERPAGES_SITE_RECORDS_API_PATH = 'api/data/v9.2/powerpagesites?$select=*&$expand=owninguser($select=systemuserid,fullname)';
export const POWERPAGES_SITE_SETTINGS_API_PATH = 'api/data/v9.2/mspp_sitesettings';
diff --git a/src/common/ecs-features/ecsFeatureGates.ts b/src/common/ecs-features/ecsFeatureGates.ts
index e40a4e4d9..d2a663264 100644
--- a/src/common/ecs-features/ecsFeatureGates.ts
+++ b/src/common/ecs-features/ecsFeatureGates.ts
@@ -89,3 +89,13 @@ export const {
enableBLChanges: false,
},
})
+
+export const {
+ feature: EnableCodeQlScan
+} = getFeatureConfigs({
+ teamName: PowerPagesClientName,
+ description: 'Enable CodeQl Scan in VSCode Desktop',
+ fallback: {
+ enableCodeQlScan: false,
+ }
+})
diff --git a/src/common/utilities/WorkspaceInfoFinderUtil.ts b/src/common/utilities/WorkspaceInfoFinderUtil.ts
index 2977df9ae..ae5545da1 100644
--- a/src/common/utilities/WorkspaceInfoFinderUtil.ts
+++ b/src/common/utilities/WorkspaceInfoFinderUtil.ts
@@ -98,3 +98,40 @@ export function findPowerPagesSiteFolder(startPath: string): string | null {
}
return null;
}
+
+/**
+ * Checks if a website.yml file exists in the specified directory
+ * @param directoryPath The directory path to check
+ * @returns True if website.yml exists, false otherwise
+ */
+export function hasWebsiteYaml(directoryPath: string): boolean {
+ const websiteYamlPath = path.join(directoryPath, WEBSITE_YML);
+ return fs.existsSync(websiteYamlPath);
+}
+
+/**
+ * Checks if a .powerpages-site folder exists in the specified directory
+ * @param directoryPath The directory path to check
+ * @returns True if .powerpages-site folder exists, false otherwise
+ */
+export function hasPowerPagesSiteFolder(directoryPath: string): boolean {
+ return fs.existsSync(directoryPath);
+}
+
+/**
+ * Gets the path to the website.yml file in the specified directory
+ * @param directoryPath The directory path
+ * @returns The full path to website.yml
+ */
+export function getWebsiteYamlPath(directoryPath: string): string {
+ return path.join(directoryPath, WEBSITE_YML);
+}
+
+/**
+ * Gets the path to the .powerpages-site folder in the specified directory
+ * @param directoryPath The directory path
+ * @returns The full path to .powerpages-site folder
+ */
+export function getPowerPagesSiteFolderPath(directoryPath: string): string {
+ return path.join(directoryPath, POWERPAGES_SITE_FOLDER);
+}