├── User-Interface
├── .env
├── src
│ ├── services
│ │ ├── index.ts
│ │ ├── deviceService.ts
│ │ └── pawService.ts
│ ├── components
│ │ ├── PawActions
│ │ │ ├── index.ts
│ │ │ └── PawActions.tsx
│ │ ├── Home
│ │ │ ├── Home.css
│ │ │ ├── index.ts
│ │ │ └── Home.tsx
│ │ ├── DeviceDetails
│ │ │ ├── DeviceDetails.css
│ │ │ ├── index.ts
│ │ │ └── DeviceDetails.tsx
│ │ ├── ComingSoon
│ │ │ ├── index.ts
│ │ │ └── ComingSoon.tsx
│ │ ├── DeviceContainer
│ │ │ ├── index.ts
│ │ │ └── DeviceContainer.tsx
│ │ ├── CommissionPawsPanel
│ │ │ ├── index.ts
│ │ │ ├── CommissionPawsPanel.tsx
│ │ │ ├── CommissionPawsPanelFooter
│ │ │ │ └── CommissionPawsPanelFooter.tsx
│ │ │ └── CommissionPawsPanelContent
│ │ │ │ ├── SelectedItems
│ │ │ │ └── SelectedItems.tsx
│ │ │ │ ├── CommissionPawsPanelContent.tsx
│ │ │ │ └── FilteredItems
│ │ │ │ └── FilteredItems.tsx
│ │ ├── DeviceItemList
│ │ │ ├── DeviceItemList.types.ts
│ │ │ └── DeviceItemList.tsx
│ │ ├── SearchBox
│ │ │ └── AzureSearchBox.tsx
│ │ ├── LeftNav
│ │ │ ├── LeftNav.css
│ │ │ └── LeftNav.tsx
│ │ └── Header
│ │ │ ├── Header.tsx
│ │ │ └── Header.css
│ ├── models
│ │ ├── mocks
│ │ │ ├── index.ts
│ │ │ └── autopilotDeviceMock.ts
│ │ ├── index.ts
│ │ └── device.ts
│ ├── Assets
│ │ ├── Change.ico
│ │ ├── Change.png
│ │ ├── Info.ico
│ │ ├── Info.png
│ │ ├── Trash.ico
│ │ ├── Trash.png
│ │ ├── Wrench.ico
│ │ ├── Wrench.png
│ │ ├── Warning.ico
│ │ ├── Warning.png
│ │ ├── Cloud PAW Logo - Icon.ico
│ │ ├── Cloud PAW Logo - 4k Raster.png
│ │ └── groups.svg
│ ├── features
│ │ ├── ui
│ │ │ ├── index.ts
│ │ │ └── slicer.ts
│ │ └── device
│ │ │ ├── index.ts
│ │ │ └── slicer.ts
│ ├── store
│ │ ├── selectors
│ │ │ ├── index.ts
│ │ │ └── deviceSelectors
│ │ │ │ ├── index.ts
│ │ │ │ └── filterById.ts
│ │ ├── slice
│ │ │ ├── index.ts
│ │ │ └── pawAssignment.ts
│ │ ├── actions
│ │ │ ├── deviceActions
│ │ │ │ ├── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── gettingDeviceActions.ts
│ │ │ └── pawActions
│ │ │ │ ├── index.ts
│ │ │ │ ├── gettingPawsActions.ts
│ │ │ │ ├── types.ts
│ │ │ │ ├── commissionPawActions.ts
│ │ │ │ └── assignmentPawActions.ts
│ │ ├── reducers
│ │ │ ├── deviceReducers
│ │ │ │ ├── index.ts
│ │ │ │ └── gettingDevicesReducer.ts
│ │ │ ├── appReducer.ts
│ │ │ └── pawReducers
│ │ │ │ ├── index.ts
│ │ │ │ ├── gettingPawsReducer.ts
│ │ │ │ ├── assignmentPawReducer.ts
│ │ │ │ └── commissionPawReducer.ts
│ │ ├── configureStore.ts
│ │ └── store.ts
│ ├── react-app-env.d.ts
│ ├── setupTests.ts
│ ├── index.css
│ ├── reportWebVitals.ts
│ ├── index.tsx
│ ├── App.css
│ ├── App.tsx
│ └── themes.ts
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── .vscode
│ ├── extensions.json
│ ├── settings.json
│ ├── express.code-snippets
│ └── launch.json
├── .gitignore
├── tsconfig.json
├── package.json
└── README.md
├── Server
├── .deployment
├── .gitignore
├── .mocharc.json
├── test
│ ├── indexTest.ts
│ └── UtilityTest.ts
├── src
│ ├── Utility
│ │ ├── i18n.ts
│ │ ├── index.ts
│ │ ├── Utility.ts
│ │ ├── types.ts
│ │ └── RequestGenerator.ts
│ ├── Routes
│ │ ├── OpenAPI.ts
│ │ └── DeploymentEngine.ts
│ ├── index.ts
│ └── Startup
│ │ └── Authentication.ts
├── .vscode
│ ├── extensions.json
│ ├── settings.json
│ ├── launch.json
│ └── server.code-snippets
├── package.json
├── disableeslintrc.json
└── tsconfig.json
├── Privileged Security Management.code-workspace
├── .github
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── UnitTest-Server.js.yml
│ ├── UnitTest-UI.js.yml
│ ├── codeql-analysis.yml
│ └── Build-Binaries.yml
├── azure-pipelines.yml
├── CODE_OF_CONDUCT.md
├── SUPPORT.md
├── PRIVACY
├── LICENSE
├── .gitignore
├── SECURITY.md
└── README.md
/User-Interface/.env:
--------------------------------------------------------------------------------
1 | BUILD_PATH='../Server/bin/src/UI'
--------------------------------------------------------------------------------
/Server/.deployment:
--------------------------------------------------------------------------------
1 | [config]
2 | SCM_DO_BUILD_DURING_DEPLOYMENT=true
--------------------------------------------------------------------------------
/User-Interface/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './pawService';
2 |
--------------------------------------------------------------------------------
/Server/.gitignore:
--------------------------------------------------------------------------------
1 | # dotenv environment variables file
2 | .env
3 | .env.test
--------------------------------------------------------------------------------
/User-Interface/src/components/PawActions/index.ts:
--------------------------------------------------------------------------------
1 | export { PawActions } from './PawActions';
2 |
--------------------------------------------------------------------------------
/Server/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "recursive": true,
3 | "spec": "bin/test/",
4 | "color": true
5 | }
--------------------------------------------------------------------------------
/User-Interface/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/User-Interface/src/models/mocks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./autopilotDeviceMock"
2 | export * from "./psmDeviceMock"
--------------------------------------------------------------------------------
/Server/test/indexTest.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // Coming soon!
--------------------------------------------------------------------------------
/User-Interface/src/components/Home/Home.css:
--------------------------------------------------------------------------------
1 | /* // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license. */
--------------------------------------------------------------------------------
/Server/src/Utility/i18n.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // Coming soon!
--------------------------------------------------------------------------------
/User-Interface/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/public/favicon.ico
--------------------------------------------------------------------------------
/User-Interface/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/public/logo192.png
--------------------------------------------------------------------------------
/User-Interface/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/public/logo512.png
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Change.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Change.ico
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Change.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Change.png
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Info.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Info.ico
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Info.png
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Trash.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Trash.ico
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Trash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Trash.png
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Wrench.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Wrench.ico
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Wrench.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Wrench.png
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Warning.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Warning.ico
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Warning.png
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceDetails/DeviceDetails.css:
--------------------------------------------------------------------------------
1 | /* Copyright (c) Microsoft Corporation. */
2 | /* Licensed under the MIT license. */
--------------------------------------------------------------------------------
/User-Interface/src/models/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from './device';
--------------------------------------------------------------------------------
/User-Interface/src/features/ui/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from "./slicer"
--------------------------------------------------------------------------------
/User-Interface/src/components/Home/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { Home } from "./Home"
--------------------------------------------------------------------------------
/User-Interface/src/features/device/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from "./slicer"
5 |
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Cloud PAW Logo - Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Cloud PAW Logo - Icon.ico
--------------------------------------------------------------------------------
/User-Interface/src/store/selectors/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from './deviceSelectors';
--------------------------------------------------------------------------------
/User-Interface/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | ///
5 |
--------------------------------------------------------------------------------
/User-Interface/src/Assets/Cloud PAW Logo - 4k Raster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/Cloud-PAW-Management/HEAD/User-Interface/src/Assets/Cloud PAW Logo - 4k Raster.png
--------------------------------------------------------------------------------
/User-Interface/src/components/ComingSoon/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { ComingSoon } from "./ComingSoon";
--------------------------------------------------------------------------------
/User-Interface/src/store/slice/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { pawAssignmentSlice } from "./pawAssignment"
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceDetails/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { DeviceDetails } from "./DeviceDetails";
--------------------------------------------------------------------------------
/User-Interface/src/store/selectors/deviceSelectors/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { filterById } from './filterById';
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceContainer/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { DeviceContainer } from './DeviceContainer';
--------------------------------------------------------------------------------
/User-Interface/src/components/CommissionPawsPanel/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export { CommissionPawsPanel } from './CommissionPawsPanel';
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/deviceActions/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from './types';
5 | export * from './gettingDeviceActions';
--------------------------------------------------------------------------------
/Privileged Security Management.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "name": "Server",
5 | "path": "Server"
6 | },
7 | {
8 | "name": "User-Interface",
9 | "path": "User-Interface"
10 | }
11 | ],
12 | "settings": {}
13 | }
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceItemList/DeviceItemList.types.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { IPsmDevice } from '../../models';
5 |
6 | export interface IPawItemListProps {
7 | items: IPsmDevice[];
8 | };
--------------------------------------------------------------------------------
/User-Interface/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "streetsidesoftware.code-spell-checker",
4 | "dbaeumer.vscode-eslint",
5 | "ms-azuretools.vscode-azureappservice",
6 | "ms-vscode.azure-account",
7 | "eamodio.gitlens"
8 | ]
9 | }
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/pawActions/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from './types';
5 | export * from './commissionPawActions';
6 | export * from './gettingPawsActions';
7 | export * from './assignmentPawActions';
--------------------------------------------------------------------------------
/Server/src/Utility/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export * from "./GraphClient"
5 | // export * from "./i18n"
6 | export * from "./RequestGenerator"
7 | export * from "./types"
8 | export * from "./Utility"
9 | export * from "./Validators"
--------------------------------------------------------------------------------
/User-Interface/src/components/ComingSoon/ComingSoon.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // Render a splash screen stating that the feature is coming soon!
5 | export function ComingSoon() {
6 | return (
7 |
Coming Soon!
8 | )
9 | }
--------------------------------------------------------------------------------
/Server/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "streetsidesoftware.code-spell-checker",
4 | "dbaeumer.vscode-eslint",
5 | "ms-azuretools.vscode-azureappservice",
6 | "ms-vscode.azure-account",
7 | "eamodio.gitlens",
8 | "gruntfuggly.todo-tree"
9 | ]
10 | }
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/deviceReducers/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { combineReducers } from 'redux';
5 | import { getDevices } from './gettingDevicesReducer';
6 |
7 | export const devices = combineReducers({
8 | getDevices,
9 | });
10 |
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/deviceActions/types.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export const GETTING_DEVICE_REQUEST = 'GETTING_DEVICE_REQUEST';
5 | export const GETTING_DEVICE_SUCCESS = 'GETTING_DEVICE_SUCCESS';
6 | export const GETTING_DEVICE_FAILURE = 'GETTING_DEVICE_FAILURE'
7 |
--------------------------------------------------------------------------------
/User-Interface/src/store/selectors/deviceSelectors/filterById.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export const filterById = (state, id) => {
5 | const filteredById = state.devices.getDevices.devices.filter(device => device.azureAdDeviceId.indexOf(id)>=0);
6 | return filteredById;
7 | };
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/User-Interface"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 | - package-ecosystem: npm
10 | directory: "/Server"
11 | schedule:
12 | interval: daily
13 | time: "10:00"
14 | open-pull-requests-limit: 10
--------------------------------------------------------------------------------
/User-Interface/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Intune",
4 | "defaultuser",
5 | "keyvault"
6 | ],
7 | "appService.zipIgnorePattern": [
8 | "node_modules{,/**}",
9 | ".vscode{,/**}"
10 | ],
11 | "appService.deploySubpath": ".",
12 | "appService.defaultWebAppToDeploy": "None"
13 | }
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/appReducer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { combineReducers } from 'redux';
5 | import { paw } from './pawReducers';
6 | import { devices } from './deviceReducers';
7 |
8 | export const appReducer = combineReducers({
9 | paw,
10 | devices
11 | });
12 |
13 |
--------------------------------------------------------------------------------
/User-Interface/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
5 | // allows you to do things like:
6 | // expect(element).toHaveTextContent(/react/i)
7 | // learn more: https://github.com/testing-library/jest-dom
8 | import '@testing-library/jest-dom';
9 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Run the MS Internal component governance process and report the data internally.
2 |
3 | trigger:
4 | - main
5 |
6 | pool:
7 | vmImage: ubuntu-latest
8 |
9 | steps:
10 | - task: ComponentGovernanceComponentDetection@0
11 | inputs:
12 | scanType: 'Register'
13 | verbosity: 'Verbose'
14 | alertWarningLevel: 'High'
15 | displayName: 'Component Detection'
--------------------------------------------------------------------------------
/User-Interface/src/store/configureStore.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { createStore, applyMiddleware } from 'redux';
5 | import thunk from 'redux-thunk';
6 | import { appReducer } from './reducers/appReducer';
7 |
8 | // TODO: Legacy implementation to be migrated to the toolkit
9 | export const store = createStore(appReducer, applyMiddleware(thunk));
--------------------------------------------------------------------------------
/Server/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "defaultuser",
4 | "Intune",
5 | "keyvault",
6 | "odata"
7 | ],
8 | "appService.zipIgnorePattern": [
9 | "node_modules{,/**}",
10 | ".vscode{,/**}"
11 | ],
12 | "appService.deploySubpath": ".",
13 | "appService.defaultWebAppToDeploy": "None",
14 | "editor.bracketPairColorization.enabled": true
15 | }
--------------------------------------------------------------------------------
/User-Interface/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/User-Interface/src/features/ui/slicer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { createSlice } from "@reduxjs/toolkit";
5 |
6 | export const uiSlice = createSlice({
7 | // Domain of slicer
8 | name: 'ui',
9 | // Initially empty
10 | initialState: { commissionMode: false },
11 | // Actions that can be performed on this data structure
12 | reducers: {}
13 | });
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/pawReducers/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { combineReducers } from 'redux';
5 | import { commissionPaws } from './commissionPawReducer';
6 | import { getPaws } from './gettingPawsReducer';
7 | import { assignPaw } from "./assignmentPawReducer";
8 |
9 | export const paw = combineReducers({
10 | assignPaw,
11 | commissionPaws,
12 | getPaws
13 | });
14 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/User-Interface/src/index.css:
--------------------------------------------------------------------------------
1 | /* Copyright (c) Microsoft Corporation. */
2 | /* Licensed under the MIT license. */
3 |
4 | body {
5 | margin: 0;
6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
8 | sans-serif;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | }
12 |
13 | code {
14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
15 | monospace;
16 | }
17 |
--------------------------------------------------------------------------------
/User-Interface/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { ReportHandler } from 'web-vitals';
5 |
6 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
7 | if (onPerfEntry && onPerfEntry instanceof Function) {
8 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
9 | getCLS(onPerfEntry);
10 | getFID(onPerfEntry);
11 | getFCP(onPerfEntry);
12 | getLCP(onPerfEntry);
13 | getTTFB(onPerfEntry);
14 | });
15 | }
16 | };
17 |
18 | export default reportWebVitals;
19 |
--------------------------------------------------------------------------------
/User-Interface/src/store/store.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { configureStore } from "@reduxjs/toolkit"
5 | import { deviceSlice } from "../features/device"
6 |
7 | // Configure the default store for the app
8 | export const store = configureStore({
9 | reducer: {
10 | device: deviceSlice.reducer
11 | }
12 | });
13 |
14 | // Export the compiled RootState from the store
15 | export type RootState = ReturnType
16 |
17 | // Export the compiled dispatch data
18 | export type AppDispatch = typeof store.dispatch
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support
2 |
3 | ## How to file issues and get help
4 |
5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing
6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or
7 | feature request as a new Issue.
8 |
9 | For help and questions about using this project, please contact your MCS or CSU/Premier/Unified Customer Success Account Manager (CSAM/TAM) and have them email "the-a-team@microsoft.com" (internal only email).
10 |
11 | ## Microsoft Support Policy
12 |
13 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
14 |
--------------------------------------------------------------------------------
/User-Interface/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Privileged Security Management",
3 | "name": "Automated Privileged Security Management",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceDetails/DeviceDetails.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import React from "react";
5 | import { useParams, useNavigate } from "react-router-dom"
6 |
7 | export function DeviceDetails() {
8 | const navigate = useNavigate();
9 | const { id } = useParams();
10 |
11 | // If no ID was specified, redirect the user to the device list
12 | if (id === undefined) {
13 | navigate("/");
14 | };
15 |
16 | return (
17 |
18 | Individual PAW device!
19 | DeviceID: { id }
20 |
21 | )
22 | }
--------------------------------------------------------------------------------
/User-Interface/src/index.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import { Provider } from 'react-redux';
7 | import './index.css';
8 | import App from './App';
9 | import { store } from './store/configureStore';
10 |
11 | // Start the react renderer with strict mode enabled for the whole app
12 | // Also adds a redux store to the root of the app so that a single global store is available
13 | ReactDOM.render(
14 |
15 |
16 |
17 |
18 | ,
19 | document.getElementById('root')
20 | );
--------------------------------------------------------------------------------
/User-Interface/src/App.css:
--------------------------------------------------------------------------------
1 | /* Copyright (c) Microsoft Corporation. */
2 | /* Licensed under the MIT license. */
3 |
4 | .App {
5 | text-align: center;
6 | }
7 |
8 | .App-logo {
9 | height: 40vmin;
10 | pointer-events: none;
11 | }
12 |
13 | .App-header {
14 | background-color: #282c34;
15 | min-height: 100vh;
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | justify-content: center;
20 | font-size: calc(10px + 2vmin);
21 | color: white;
22 | }
23 |
24 | .App-link {
25 | color: #61dafb;
26 | }
27 |
28 | @keyframes App-logo-spin {
29 | from {
30 | transform: rotate(0deg);
31 | }
32 | to {
33 | transform: rotate(360deg);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/User-Interface/src/models/device.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // Define the PSM processed autopilot device object structure
5 | export interface IPsmAutopilotDevice {
6 | displayName?: string,
7 | azureActiveDirectoryDeviceId: string,
8 | azureAdDeviceId: string,
9 | serialNumber: string
10 | }
11 |
12 | // Define the PSM's device object structure
13 | export interface IPsmDevice {
14 | Type: "Privileged" | "Developer" | "Tactical"
15 | ParentGroup: string
16 | ParentDevice?: string
17 | DisplayName: string
18 | id: string
19 | GroupAssignment: string
20 | UserAssignment: string
21 | CommissionedDate: string //UTC Time in ISO format
22 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'Feature Request: '
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/User-Interface/src/App.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { Home } from "./components/Home";
5 | import { DeviceContainer } from "./components/DeviceContainer";
6 | import { DeviceDetails } from "./components/DeviceDetails";
7 | import { BrowserRouter, Routes, Route } from "react-router-dom";
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 | } />
15 | } />
16 | } />
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | export default App;
--------------------------------------------------------------------------------
/User-Interface/src/components/SearchBox/AzureSearchBox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SearchBox, ISearchBoxStyles } from '@fluentui/react/lib/SearchBox';
3 |
4 | const searchBoxStyles: Partial = { root: { width: 500,
5 | margin: 8 } };
6 |
7 | /* eslint-disable react/jsx-no-bind */
8 | export const AzureSearchBox = () => (
9 | {
13 | console.log('Custom onEscape Called');
14 | }}
15 | onClear={ev => {
16 | console.log('Custom onClear Called');
17 | }}
18 | onChange={(_, newValue) => console.log('SearchBox onChange fired: ' + newValue)}
19 | onSearch={newValue => console.log('SearchBox onSearch fired: ' + newValue)}
20 | />
21 | );
--------------------------------------------------------------------------------
/PRIVACY:
--------------------------------------------------------------------------------
1 | The software may collect information about you and your use of the software and send it to Microsoft.
2 | Microsoft may use this information to provide services and improve our products and services.
3 | You may turn off the telemetry as described in the repository.
4 | There are also some features in the software that may enable you and Microsoft to collect data from users of your applications.
5 | If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement.
6 | Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704.
7 | You can learn more about data collection and use in the help documentation and our privacy statement.
8 | Your use of the software operates as your consent to these practices.
9 |
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/pawActions/gettingPawsActions.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { IPsmDevice } from '../../../models';
5 | import { PawService } from '../../../services';
6 | import {
7 | GETTING_PAWS_REQUEST,
8 | GETTING_PAWS_SUCCESS,
9 | GETTING_PAWS_FAILURE
10 | } from './types';
11 |
12 | const gettingPawsRequest = () => ({
13 | type: GETTING_PAWS_REQUEST,
14 | });
15 | const gettingPawsSuccess = (paws: IPsmDevice[]) => ({
16 | type: GETTING_PAWS_SUCCESS,
17 | payload: paws
18 | });
19 | const gettingPawsFailure = (error: Error) => ({
20 | type: GETTING_PAWS_FAILURE,
21 | payload: error
22 | });
23 |
24 | export const getPaws = () => {
25 | return async (dispatch) => {
26 | dispatch(gettingPawsRequest());
27 | PawService.getPaws()
28 | .then(paws => dispatch(gettingPawsSuccess(paws)))
29 | .catch(error => dispatch(gettingPawsFailure(error)))
30 | };
31 | }
--------------------------------------------------------------------------------
/User-Interface/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "src"
4 | ],
5 | "compilerOptions": {
6 | "target": "ES2019",
7 | "lib": [
8 | "dom",
9 | "dom.iterable",
10 | "esnext"
11 | ],
12 | "jsx": "react-jsx",
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "sourceMap": true,
17 | "outDir": "./bin",
18 | "removeComments": true,
19 | "noEmit": true,
20 | "isolatedModules": true,
21 | "esModuleInterop": true,
22 | "forceConsistentCasingInFileNames": true,
23 | "strict": true,
24 | "noImplicitAny": false,
25 | "strictNullChecks": true,
26 | "strictFunctionTypes": true,
27 | "strictBindCallApply": true,
28 | "strictPropertyInitialization": true,
29 | "noImplicitThis": true,
30 | "alwaysStrict": true,
31 | "noFallthroughCasesInSwitch": true,
32 | "skipLibCheck": true,
33 | "allowJs": true,
34 | "allowSyntheticDefaultImports": true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'Bug Report: '
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/deviceActions/gettingDeviceActions.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { IPsmAutopilotDevice } from '../../../models';
5 | import { DeviceService } from '../../../services/deviceService';
6 | import {
7 | GETTING_DEVICE_REQUEST,
8 | GETTING_DEVICE_SUCCESS,
9 | GETTING_DEVICE_FAILURE
10 | } from './types';
11 |
12 | const gettingDeviceRequest = () => ({
13 | type: GETTING_DEVICE_REQUEST,
14 | });
15 | const gettingDevicesSuccess = (devices: IPsmAutopilotDevice[]) => ({
16 | type: GETTING_DEVICE_SUCCESS,
17 | payload: devices
18 | });
19 | const gettingDevicesFailure = (error: Error) => ({
20 | type: GETTING_DEVICE_FAILURE,
21 | payload: error
22 | });
23 |
24 | export const getDevices = () => {
25 | return async (dispatch) => {
26 | dispatch(gettingDeviceRequest());
27 | DeviceService.getDevices()
28 | .then(devices => dispatch(gettingDevicesSuccess(devices)))
29 | .catch(error => dispatch(gettingDevicesFailure(error)))
30 | };
31 | }
--------------------------------------------------------------------------------
/.github/workflows/UnitTest-Server.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Unit Test - Server
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | paths:
10 | - Server/**
11 | pull_request:
12 | branches: [ main ]
13 | paths:
14 | - Server/**
15 | workflow_dispatch:
16 |
17 | jobs:
18 | build:
19 |
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | matrix:
24 | node-version: [16.x, 17.x]
25 |
26 | steps:
27 | - uses: actions/checkout@v2
28 | - name: Use Node.js ${{ matrix.node-version }}
29 | uses: actions/setup-node@v2
30 | with:
31 | node-version: ${{ matrix.node-version }}
32 | cache: 'npm'
33 | cache-dependency-path: Server/package-lock.json
34 | - run: npm ci
35 | working-directory: Server
36 | - run: npm run-script build --if-present
37 | working-directory: Server
38 | - run: npm run-script test
39 | working-directory: Server
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/pawReducers/gettingPawsReducer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import {
5 | GETTING_PAWS_REQUEST,
6 | GETTING_PAWS_SUCCESS,
7 | GETTING_PAWS_FAILURE,
8 | } from '../../actions/pawActions';
9 |
10 | const initialState = {
11 | paws: [],
12 | isGettingPaws: false,
13 | error: undefined,
14 | };
15 | export const getPaws = (state = initialState, action: any) => {
16 | switch(action.type) {
17 | case GETTING_PAWS_REQUEST:
18 | return {
19 | ...state,
20 | isGettingPaws: true
21 | };
22 | case GETTING_PAWS_SUCCESS:
23 | return {
24 | ...state,
25 | isGettingPaws: false,
26 | paws: action.payload,
27 | error: undefined,
28 | };
29 | case GETTING_PAWS_FAILURE:
30 | return {
31 | ...state,
32 | isGettingPaws: false,
33 | error: action.payload
34 | };
35 | default:
36 | return state;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/User-Interface/src/components/LeftNav/LeftNav.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html, body {
6 | margin: 0;
7 | padding: 0;
8 | width: 100%;
9 | width: 100%;
10 | }
11 |
12 | #wrapper {
13 | display: block;
14 | position: fixed;
15 | width: 100%;
16 | height: 100%;
17 | }
18 |
19 | #leftWrapper {
20 | display: inline-block;
21 | position: absolute;
22 | left: 0;
23 | margin: 0;
24 | padding: 0;
25 | width: 15em;
26 | height: 100%;
27 |
28 | background-color: #1e1e1e;
29 | }
30 |
31 | #listView {
32 | display: block;
33 | position: relative;
34 | }
35 |
36 | #listView li {
37 | display: block;
38 | list-style: none;
39 | }
40 |
41 | #listView li a {
42 | display: block;
43 | padding: 20px;
44 |
45 | color: #fff;
46 | font-family: sans-serif;
47 | font-size: 1.1em;
48 | text-decoration: none;
49 |
50 | border-top: 1px solid rgba(0,0,0,0.5);
51 | border-bottom:1px solid rgba(255,255,255,0.04);
52 |
53 | -webkit-transition: all .20s ease;
54 | -moz-transition: all .20s ease;
55 | -o-transition: all .20s ease;
56 | transition: all .20s ease;
57 | }
58 |
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/deviceReducers/gettingDevicesReducer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import {
5 | GETTING_DEVICE_REQUEST,
6 | GETTING_DEVICE_SUCCESS,
7 | GETTING_DEVICE_FAILURE
8 | } from '../../actions/deviceActions';
9 |
10 | const initialState = {
11 | devices: [],
12 | isGettingDevices: false,
13 | error: undefined,
14 | };
15 | export const getDevices = (state = initialState, action: any) => {
16 | switch(action.type) {
17 | case GETTING_DEVICE_REQUEST:
18 | return {
19 | ...state,
20 | isGettingDevices: true
21 | };
22 | case GETTING_DEVICE_SUCCESS:
23 | return {
24 | ...state,
25 | isGettingDevices: false,
26 | devices: action.payload,
27 | error: undefined,
28 | };
29 | case GETTING_DEVICE_FAILURE:
30 | return {
31 | ...state,
32 | isGettingDevices: false,
33 | error: action.payload
34 | };
35 | default:
36 | return state;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/.github/workflows/UnitTest-UI.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Unit Test - User Interface
5 |
6 | on:
7 | # push:
8 | # branches: [ main ]
9 | # paths:
10 | # - User-Interface/**
11 | # pull_request:
12 | # branches: [ main ]
13 | # paths:
14 | # - User-Interface/**
15 | workflow_dispatch:
16 |
17 | jobs:
18 | build:
19 |
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | matrix:
24 | node-version: [16.x, 17.x]
25 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
26 |
27 | steps:
28 | - uses: actions/checkout@v2
29 | - name: Use Node.js ${{ matrix.node-version }}
30 | uses: actions/setup-node@v2
31 | with:
32 | node-version: ${{ matrix.node-version }}
33 | cache: 'npm'
34 | cache-dependency-path: User-Interface/package-lock.json
35 | - run: npm ci
36 | working-directory: User-Interface
37 | - run: npm run-script build --if-present
38 | working-directory: User-Interface
39 | - run: npm run-script test
40 | working-directory: User-Interface
41 |
--------------------------------------------------------------------------------
/User-Interface/src/components/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { PrimaryButton } from "@fluentui/react";
5 | import React from "react";
6 | import { useNavigate } from "react-router-dom";
7 |
8 | // Initial landing page
9 | export function Home() {
10 |
11 | // Instantiate the Page Navigator
12 | const navigator = useNavigate();
13 |
14 | // Define the on-click event function
15 | function onClickPageNavDevice(): void {
16 | // Navigate to the devices page
17 | navigator("/devices");
18 | };
19 |
20 | return (
21 |
22 | Welcome to Privileged Security Management
23 | Please select a module to administer
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
--------------------------------------------------------------------------------
/User-Interface/.vscode/express.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | // Place your Cloud-PAW-Management workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
7 | // Placeholders with the same ids are connected.
8 | // Example:
9 | // "Print to console": {
10 | // "scope": "javascript,typescript",
11 | // "prefix": "log",
12 | // "body": [
13 | // "console.log('$1');",
14 | // "$2"
15 | // ],
16 | // "description": "Log output to console"
17 | // }
18 | "Route Callback": {
19 | "prefix": "route-callback",
20 | "description": "Pre-built callback with error handling",
21 | "scope": "javascript,typescript",
22 | "body": [
23 | "async (request, response, next) => {",
24 | " \/\/ Catch execution errors",
25 | " try {",
26 | " \/\/ ${2:Describe the action}",
27 | " ${1:\/\/Action to execute};",
28 | " } catch (error) {",
29 | " \/\/ Send the error details if something goes wrong",
30 | " next(error);",
31 | " };",
32 | "}"
33 | ]
34 | }
35 | }
--------------------------------------------------------------------------------
/User-Interface/src/Assets/groups.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/User-Interface/src/components/CommissionPawsPanel/CommissionPawsPanel.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { Panel, PanelType } from "@fluentui/react";
5 | import React, { useEffect } from "react";
6 | import { useDispatch } from "react-redux";
7 | import { getDevices } from "../../store/actions/deviceActions";
8 | import { CommissionPawsPanelContent } from "./CommissionPawsPanelContent/CommissionPawsPanelContent";
9 | import { CommissionPawsPanelFooter } from "./CommissionPawsPanelFooter/CommissionPawsPanelFooter";
10 |
11 | interface ICommissionPawsPanelProps {
12 | isOpen: boolean;
13 | onDismissPanel?: () => void;
14 | }
15 | export const CommissionPawsPanel = ({ isOpen, onDismissPanel}: ICommissionPawsPanelProps) => {
16 | const dispatch = useDispatch();
17 |
18 | useEffect(() => {
19 | dispatch(getDevices());
20 | }, [dispatch]);
21 |
22 | const onRenderFooterContent = React.useCallback(
23 | () => ,
24 | [onDismissPanel],
25 | );
26 | return (
27 |
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/User-Interface/src/components/CommissionPawsPanel/CommissionPawsPanelFooter/CommissionPawsPanelFooter.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { DefaultButton, IStackTokens, PrimaryButton, Stack } from '@fluentui/react';
5 | import React, { useCallback } from 'react';
6 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
7 | import { commissionPaws } from '../../../store/actions/pawActions';
8 |
9 | export const CommissionPawsPanelFooter = (props) => {
10 | const stackTokens: Partial = { childrenGap: 20 };
11 | const selectedItems = useSelector((state: RootStateOrAny) => state.paw.commissionPaws.devicesToCommission);
12 | const pawTypeToCommission = useSelector((state: RootStateOrAny) => state.paw.commissionPaws.pawTypeToCommission);
13 |
14 | const dispatch = useDispatch();
15 |
16 | const dismissPanel = () => {
17 | props.onDismissPanel();
18 | };
19 | const onCommissionPaws = useCallback(() => {
20 | dispatch(commissionPaws(selectedItems, pawTypeToCommission));
21 | props.onDismissPanel();
22 | }, [dispatch, selectedItems, pawTypeToCommission, props]);
23 |
24 | return (
25 |
26 |
30 | Commission PAW
31 |
32 | Cancel
33 |
34 | );
35 | };
--------------------------------------------------------------------------------
/User-Interface/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | /* eslint-disable jsx-a11y/anchor-is-valid */
5 | import './Header.css';
6 | // import groups from '../../Assets/groups.svg';
7 | // import {AzureSearchBox} from '../SearchBox/AzureSearchBox';
8 | import * as React from 'react';
9 | // import { FontIcon } from '@fluentui/react/lib/Icon';
10 | // import { mergeStyles } from '@fluentui/react/lib/Styling';
11 |
12 | // const iconClass = mergeStyles({
13 | // fontSize: 50,
14 | // height: 50,
15 | // width: 50,
16 | // margin: '0 25px',
17 | // });
18 |
19 | export const Header = () => {
20 | return (
21 |
22 |
26 | {/*
*/}
31 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/Server/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Build and Run",
9 | "request": "launch",
10 | "runtimeArgs": [
11 | "run-script",
12 | "build_run"
13 | ],
14 | "runtimeExecutable": "npm",
15 | "skipFiles": [
16 | "/**"
17 | ],
18 | "type": "pwa-node"
19 | },
20 | {
21 | "name": "Build and Test",
22 | "request": "launch",
23 | "runtimeArgs": [
24 | "run-script",
25 | "build_test"
26 | ],
27 | "runtimeExecutable": "npm",
28 | "skipFiles": [
29 | "/**"
30 | ],
31 | "type": "pwa-node"
32 | },
33 | {
34 | "name": "Build",
35 | "request": "launch",
36 | "runtimeArgs": [
37 | "run-script",
38 | "build"
39 | ],
40 | "runtimeExecutable": "npm",
41 | "skipFiles": [
42 | "/**"
43 | ],
44 | "type": "pwa-node"
45 | },
46 | {
47 | "name": "Run Tests",
48 | "request": "launch",
49 | "runtimeArgs": [
50 | "run-script",
51 | "test"
52 | ],
53 | "runtimeExecutable": "npm",
54 | "skipFiles": [
55 | "/**"
56 | ],
57 | "type": "pwa-node"
58 | }
59 | ]
60 | }
--------------------------------------------------------------------------------
/User-Interface/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "privileged-security-management-ui",
3 | "version": "0.0.1",
4 | "private": "true",
5 | "keywords": [
6 | "Intune",
7 | "Endpoint Manager",
8 | "Microsoft",
9 | "PAW",
10 | "Web app",
11 | "interactive login"
12 | ],
13 | "author": "Elliot Huffman",
14 | "license": "MIT",
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/microsoft/Privileged-Security-Management"
18 | },
19 | "dependencies": {
20 | "@fluentui/react": "^8.86.4",
21 | "@reduxjs/toolkit": "^1.8.3",
22 | "react": "^17.0.2",
23 | "react-dom": "^17.0.2",
24 | "react-redux": "^7.2.8",
25 | "react-router-dom": "^6.3.0"
26 | },
27 | "devDependencies": {
28 | "@microsoft/microsoft-graph-types-beta": "^0.29.0-preview",
29 | "@testing-library/jest-dom": "^5.16.4",
30 | "@testing-library/react": "^12.1.4",
31 | "@testing-library/user-event": "^13.5.0",
32 | "@types/jest": "^28.1.1",
33 | "@types/node": "^18.6.4",
34 | "@types/react": "^17.0.43",
35 | "@types/react-dom": "^17.0.14",
36 | "@types/react-redux": "^7.1.23",
37 | "@types/redux": "^3.6.31",
38 | "react-scripts": "^5.0.1",
39 | "typescript": "^4.7.4",
40 | "web-vitals": "^2.1.4"
41 | },
42 | "scripts": {
43 | "start": "react-scripts start",
44 | "build": "react-scripts build",
45 | "test": "react-scripts test",
46 | "eject": "react-scripts eject"
47 | },
48 | "eslintConfig": {
49 | "extends": [
50 | "react-app",
51 | "react-app/jest"
52 | ]
53 | },
54 | "browserslist": {
55 | "production": [
56 | ">0.2%",
57 | "not dead",
58 | "not op_mini all"
59 | ],
60 | "development": [
61 | "last 1 chrome version",
62 | "last 1 firefox version",
63 | "last 1 safari version"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/User-Interface/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Build and Run",
9 | "request": "launch",
10 | "runtimeArgs": [
11 | "run-script",
12 | "build_run"
13 | ],
14 | "runtimeExecutable": "npm",
15 | "skipFiles": [
16 | "/**"
17 | ],
18 | "type": "pwa-node"
19 | },
20 | {
21 | "name": "Build and Test",
22 | "request": "launch",
23 | "runtimeArgs": [
24 | "run-script",
25 | "build_test"
26 | ],
27 | "runtimeExecutable": "npm",
28 | "skipFiles": [
29 | "/**"
30 | ],
31 | "type": "pwa-node"
32 | },
33 | {
34 | "name": "Build",
35 | "request": "launch",
36 | "runtimeArgs": [
37 | "run-script",
38 | "build"
39 | ],
40 | "runtimeExecutable": "npm",
41 | "skipFiles": [
42 | "/**"
43 | ],
44 | "type": "pwa-node"
45 | },
46 | {
47 | "name": "Run Tests",
48 | "request": "launch",
49 | "runtimeArgs": [
50 | "run-script",
51 | "test"
52 | ],
53 | "runtimeExecutable": "npm",
54 | "skipFiles": [
55 | "/**"
56 | ],
57 | "type": "pwa-node"
58 | }
59 | ]
60 | }
--------------------------------------------------------------------------------
/Server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "privileged-security-management-server",
3 | "version": "1.1.0",
4 | "description": "A utility to easily manage privileged security via a web app.",
5 | "main": "bin/src/index.js",
6 | "bin": "bin/src/index.js",
7 | "private": "true",
8 | "scripts": {
9 | "start": "node ./bin/src/index.js",
10 | "test": "mocha",
11 | "build": "tsc",
12 | "build_run": "tsc && node ./bin/src/index.js",
13 | "build_test": "tsc && mocha"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/microsoft/Privileged-Security-Management"
18 | },
19 | "keywords": [
20 | "Intune",
21 | "Endpoint Manager",
22 | "Microsoft",
23 | "PAW",
24 | "Web app",
25 | "interactive login"
26 | ],
27 | "author": "Elliot Huffman",
28 | "license": "MIT",
29 | "devDependencies": {
30 | "@microsoft/microsoft-graph-types-beta": "^0.29.0-preview",
31 | "@types/chai": "^4.3.1",
32 | "@types/express": "^4.17.13",
33 | "@types/mocha": "^9.1.1",
34 | "@types/swagger-ui-express": "^4.1.3",
35 | "@typescript-eslint/eslint-plugin": "^5.32.0",
36 | "@typescript-eslint/parser": "^5.32.0",
37 | "chai": "^4.3.6",
38 | "eslint": "^8.21.0",
39 | "mocha": "^10.0.0",
40 | "typescript": "^4.7.4"
41 | },
42 | "dependencies": {
43 | "@azure/identity": "^2.1.0",
44 | "@azure/keyvault-secrets": "^4.4.0",
45 | "@microsoft/microsoft-graph-client": "^3.0.2",
46 | "express": "^4.18.1",
47 | "helmet": "^5.1.1",
48 | "isomorphic-fetch": "^3.0.0",
49 | "swagger-ui-express": "^4.5.0"
50 | },
51 | "pkg": {
52 | "scripts": "bin/src/**/*.js",
53 | "assets": [
54 | "node_modules/**/*",
55 | "bin/src/UI/**/*"
56 | ],
57 | "targets": [
58 | "node16-win-x64",
59 | "node16-linux-x64"
60 | ],
61 | "outputPath": "dist"
62 | }
63 | }
--------------------------------------------------------------------------------
/User-Interface/src/themes.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export const lightTheme = {
5 | palette: {
6 | themePrimary: '#058ef7',
7 | themeLighterAlt: '#00060a',
8 | themeLighter: '#011728',
9 | themeLight: '#012b4a',
10 | themeTertiary: '#035594',
11 | themeSecondary: '#047dda',
12 | themeDarkAlt: '#1d99f8',
13 | themeDark: '#40a9f9',
14 | themeDarker: '#71bffb',
15 | neutralLighterAlt: '#302f2f',
16 | neutralLighter: '#383737',
17 | neutralLight: '#464444',
18 | neutralQuaternaryAlt: '#4e4d4d',
19 | neutralQuaternary: '#555454',
20 | neutralTertiaryAlt: '#727070',
21 | neutralTertiary: '#fffefd',
22 | neutralSecondary: '#fffefe',
23 | neutralPrimaryAlt: '#fffefe',
24 | neutralPrimary: '#fffefc',
25 | neutralDark: '#fffffe',
26 | black: '#ffffff',
27 | white: '#262525',
28 | }
29 | };
30 | export const darkTheme = {
31 | palette: {
32 | themePrimary: '#058ef7',
33 | themeLighterAlt: '#00060a',
34 | themeLighter: '#011728',
35 | themeLight: '#012b4a',
36 | themeTertiary: '#035594',
37 | themeSecondary: '#047dda',
38 | themeDarkAlt: '#1d99f8',
39 | themeDark: '#40a9f9',
40 | themeDarker: '#71bffb',
41 | neutralLighterAlt: '#302f2f',
42 | neutralLighter: '#383737',
43 | neutralLight: '#464444',
44 | neutralQuaternaryAlt: '#4e4d4d',
45 | neutralQuaternary: '#555454',
46 | neutralTertiaryAlt: '#727070',
47 | neutralTertiary: '#fffefd',
48 | neutralSecondary: '#fffefe',
49 | neutralPrimaryAlt: '#fffefe',
50 | neutralPrimary: '#fffefc',
51 | neutralDark: '#fffffe',
52 | black: '#ffffff',
53 | white: '#262525',
54 | }
55 | };
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/pawActions/types.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | export const GETTING_PAWS_REQUEST = 'GETTING_PAWS_REQUEST';
5 | export const GETTING_PAWS_SUCCESS = 'GETTING_PAWS_SUCCESS';
6 | export const GETTING_PAWS_FAILURE = 'GETTING_PAWS_FAILURE';
7 |
8 | export const COMMISSIONING_PAWS_REQUEST = 'COMMISSIONING_PAWS_REQUEST';
9 | export const COMMISSIONING_PAWS_SUCCESS = 'COMMISSIONING_PAWS_SUCCESS';
10 | export const COMMISSIONING_PAWS_FAILURE = 'COMMISSIONING_PAWS_FAILURE';
11 |
12 | export const DECOMMISSIONING_PAW_SELECTED = 'DECOMMISSIONING_PAW_SELECTED';
13 | export const DECOMMISSIONING_PAWS_REQUEST = 'DECOMMISSIONING_PAWS_REQUEST';
14 | export const DECOMMISSIONING_PAWS_SUCCESS = 'DECOMMISSIONING_PAWS_SUCCESS';
15 | export const DECOMMISSIONING_PAWS_FAILURE = 'DECOMMISSIONING_PAWS_FAILURE';
16 |
17 | export const SELECT_DEVICES_TO_COMMISSION_PAW = 'SELECTED_DEVICES_TO_COMMISSION_PAW';
18 | export const SELECT_PAW_TYPE_TO_COMMISSION = 'SELECT_PAW_TYPE_TO_COMMISSION';
19 | export const REMOVE_DEVICE_FROM_COMMISSION_PAW_LIST = 'REMOVE_DEVICE_FROM_COMMISSION_PAW_LIST';
20 |
21 | export const PAW_LIST_REFRESH_SUCCESS = 'PAW_LIST_REFRESH_SUCCESS';
22 |
23 | export const GETTING_PAW_ASSIGNMENT_REQUEST = 'GETTING_PAW_ASSIGNMENT_REQUEST';
24 | export const GETTING_PAW_ASSIGNMENT_SUCCESS = 'GETTING_PAW_ASSIGNMENT_SUCCESS';
25 | export const GETTING_PAW_ASSIGNMENT_FAILURE = 'GETTING_PAW_ASSIGNMENT_FAILURE';
26 |
27 | export const ASSIGN_PAW_REQUEST = 'ASSIGN_PAW_REQUEST';
28 | export const ASSIGN_PAW_SUCCESS = 'ASSIGN_PAW_SUCCESS';
29 | export const ASSIGN_PAW_FAILURE = 'ASSIGN_PAW_FAILURE';
30 |
31 | export const UNASSIGN_PAW_REQUEST = 'UNASSIGN_PAW_REQUEST';
32 | export const UNASSIGN_PAW_SUCCESS = 'UNASSIGN_PAW_SUCCESS';
33 | export const UNASSIGN_PAW_FAILURE = 'UNASSIGN_PAW_FAILURE';
34 |
35 | export const SELECT_PAW_ASSIGN = 'SELECT_PAW_ASSIGN';
36 | export const SELECT_PAW_UNASSIGN = 'SELECT_PAW_UNASSIGN';
--------------------------------------------------------------------------------
/Server/src/Routes/OpenAPI.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { hostname } from "os";
5 | import * as swaggerUI from "swagger-ui-express"
6 | import { appVersion } from "../Startup/ConfigEngine";
7 | import * as openAPIDoc from "../JSON/openAPI.json";
8 | import type express from "express";
9 |
10 | // Define the Swagger UI class that
11 | export class SwaggerUI {
12 | // Define the properties that will be available to the class
13 | private webServer: express.Express;
14 |
15 | // Define how the class should be instantiated
16 | constructor(webServer: express.Express) {
17 |
18 | // Make the express instance available to the class
19 | this.webServer = webServer;
20 |
21 | // Initialize the route
22 | this.initSwaggerUI();
23 | };
24 |
25 | // Initialize the Swagger UI middleware
26 | initSwaggerUI(): void {
27 |
28 | // Import environmental variables
29 | const port = process.env.PSM_PORT || 3000;
30 |
31 | // Set the app version in the API Doc
32 | openAPIDoc.info.version = appVersion;
33 |
34 | // Set the two host names in the API Doc
35 | openAPIDoc.servers[0].url = "https://" + hostname + ":" + port + "/"
36 | openAPIDoc.servers[1].url = "http://" + hostname + ":" + port + "/"
37 |
38 | // Initialize the Swagger UI middleware on the API endpoint
39 | this.webServer.use('/Docs', swaggerUI.serve);
40 |
41 | // Set the Swagger UI engine's default options
42 | const swaggerOptions: swaggerUI.SwaggerUiOptions = {
43 | // customCss: '.swagger-ui .topbar { display: none }',
44 | customSiteTitle: "Privileged Security Management - API Docs"
45 | };
46 |
47 | // Specify the document to be served up on the SwaggerUI endpoint using the specified options
48 | this.webServer.get('/Docs', swaggerUI.setup(openAPIDoc, swaggerOptions));
49 | }
50 | }
--------------------------------------------------------------------------------
/User-Interface/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
19 |
20 |
29 | Privileged Security Management (Private Preview)
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Server/src/Utility/Utility.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // TODO: enhance debug console output with time stamps and other valuable data
5 | // Write debug data to the console if debug mode is turned on
6 | export function writeDebugInfo(object: any, message?: any): void {
7 | // Gather the debug mode setting from the current environmental variable set
8 | const debugMode = process.env.PSM_Debug || "false";
9 |
10 | // If the debug mode value is "true" write to the console
11 | if (debugMode === "true") {
12 | // If the message parameter is not left blank, write it
13 | if (typeof message !== "undefined") {
14 | // Write the specified message to the console
15 | console.log("\n" + message);
16 | } else {
17 | // If no message was specified, write a whitespace to separate the object from the line above it
18 | console.log("\n");
19 | };
20 | // The the specified object to the console
21 | console.log(object);
22 | };
23 | };
24 |
25 | // Define the custom error structure for the app so that error handling can be well structured and in the future, automated.
26 | export class InternalAppError extends Error {
27 | // Define the initialization code for the class
28 | constructor(message: string, name?: string, trace?: string) {
29 | // Satisfy the requirements of the parent class by passing the error message to it upon initialization
30 | super(message)
31 |
32 | // If present, set the values
33 | if (typeof name === "string") {this.name = name};
34 | if (typeof trace === "string") {this.stack = trace};
35 |
36 | // Log the error on error creation/instantiation
37 | this.logError();
38 | };
39 |
40 | // TODO: Add an error reporting engine
41 | private reportError() {};
42 |
43 | // TODO: Write the error logging logic (console/disk/wherever)
44 | private logError() {
45 | console.error(this.message);
46 | };
47 | };
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ main ]
9 | schedule:
10 | - cron: '44 18 * * 1'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | language: [ 'javascript' ]
21 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
22 | # Learn more:
23 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v3
28 |
29 | # Initializes the CodeQL tools for scanning.
30 | - name: Initialize CodeQL
31 | uses: github/codeql-action/init@v2
32 | with:
33 | languages: ${{ matrix.language }}
34 | # If you wish to specify custom queries, you can do so here or in a config file.
35 | # By default, queries listed here will override any specified in a config file.
36 | # Prefix the list here with "+" to use these queries and those in the config file.
37 | queries: +security-and-quality
38 |
39 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
40 | # If this step fails, then you should remove it and run the build manually (see below)
41 | - name: Autobuild
42 | uses: github/codeql-action/autobuild@v2
43 |
44 | # ℹ️ Command-line programs to run using the OS shell.
45 | # 📚 https://git.io/JvXDl
46 |
47 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
48 | # and modify them (or add more) to build your code if your project
49 | # uses a compiled language
50 |
51 | #- run: |
52 | # make bootstrap
53 | # make release
54 |
55 | - name: Perform CodeQL Analysis
56 | uses: github/codeql-action/analyze@v2
57 |
--------------------------------------------------------------------------------
/User-Interface/src/components/LeftNav/LeftNav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Nav, INavStyles, INavLinkGroup } from '@fluentui/react/lib/Nav';
3 |
4 | const navStyles: Partial = { root: { width: 300 } };
5 |
6 | const navLinkGroups: INavLinkGroup[] = [
7 | {
8 | name: 'Main',
9 | expandAriaLabel: 'Expand Main section',
10 | collapseAriaLabel: 'Collapse Main section',
11 | links: [
12 | {
13 | key: 'Home',
14 | name: 'Home',
15 | url: '/',
16 | },
17 | {
18 | key: 'All PAWs',
19 | name: 'All PAWs',
20 | url: '/devices',
21 | },
22 | {
23 | key: 'User Management',
24 | name: 'User Management',
25 | url: '#',
26 | },
27 | {
28 | key: 'Silo Management',
29 | name: 'Silo Management',
30 | url: '#',
31 | },
32 | {
33 | key: 'Privileged Secure Score',
34 | name: 'Privileged Secure Score',
35 | url: '#',
36 | }
37 | ],
38 | },
39 | {
40 | name: 'Settings',
41 | expandAriaLabel: 'Expand Settings section',
42 | collapseAriaLabel: 'Collapse Settings section',
43 | links: [
44 | {
45 | key: 'General',
46 | name: 'General',
47 | url: '#',
48 | },
49 | {
50 | key: "Access control (IAM)",
51 | name: "Access control (IAM)",
52 | url: "#"
53 | },
54 | {
55 | key: 'Naming Format',
56 | name: 'Naming Format',
57 | url: '#',
58 | }
59 | ],
60 | },
61 | {
62 | name: 'Support + troubleshooting',
63 | expandAriaLabel: 'Expand Troubleshooting + Support section',
64 | collapseAriaLabel: 'Collapse Troubleshooting + Support section',
65 | links: [
66 | {
67 | key: 'New support request',
68 | name: 'New support request',
69 | url: 'mailto:elliot.huffman@microsoft.com',
70 | }
71 | ],
72 | },
73 | ];
74 |
75 | export const LeftNav: React.FunctionComponent = () => {
76 | return (
77 |
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built Executables/Installers
2 | dist/
3 |
4 | # typescript compiled output
5 | bin/
6 |
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # Diagnostic reports (https://nodejs.org/api/report.html)
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage
29 | *.lcov
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (https://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 | jspm_packages/
49 |
50 | # TypeScript v1 declaration files
51 | typings/
52 |
53 | # TypeScript cache
54 | *.tsbuildinfo
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional eslint cache
60 | .eslintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 |
80 | # Next.js build output
81 | .next
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/pawActions/commissionPawActions.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { getPaws } from '.';
5 | import type { IPsmAutopilotDevice, IPsmDevice } from '../../../models';
6 | import { PawService } from '../../../services';
7 | import {
8 | COMMISSIONING_PAWS_REQUEST,
9 | COMMISSIONING_PAWS_SUCCESS,
10 | COMMISSIONING_PAWS_FAILURE,
11 | DECOMMISSIONING_PAWS_SUCCESS,
12 | DECOMMISSIONING_PAWS_FAILURE,
13 | DECOMMISSIONING_PAWS_REQUEST
14 | } from './types';
15 |
16 | const commissioningPawsRequest = () => ({
17 | type: COMMISSIONING_PAWS_REQUEST,
18 | });
19 | const commissioningPawsSuccess = (paws: IPsmDevice[]) => ({
20 | type: COMMISSIONING_PAWS_SUCCESS,
21 | payload: paws
22 | });
23 | const commissioningPawsFailure = (error: Error) => ({
24 | type: COMMISSIONING_PAWS_FAILURE,
25 | payload: error
26 | });
27 |
28 | export const commissionPaws = (paws: IPsmAutopilotDevice[], pawTypeToCommission: string) => {
29 | return async (dispatch) => {
30 | dispatch(commissioningPawsRequest());
31 | PawService.commissionPaw(paws, pawTypeToCommission)
32 | .then(paws => {
33 | dispatch(commissioningPawsSuccess([]));
34 | dispatch(getPaws());
35 | })
36 | .catch(error => dispatch(commissioningPawsFailure(error)))
37 | };
38 | }
39 |
40 | const decommissioningPawsRequest = () => ({
41 | type: DECOMMISSIONING_PAWS_REQUEST,
42 | });
43 |
44 | const decommissioningPawsSuccess = (paws: IPsmDevice[]) => ({
45 | type: DECOMMISSIONING_PAWS_SUCCESS,
46 | payload: paws
47 | });
48 |
49 | const decommissioningPawsFailure = (error: Error) => ({
50 | type: DECOMMISSIONING_PAWS_FAILURE,
51 | payload: error
52 | });
53 |
54 | export const decommissionPaws = (paws: IPsmDevice[]) => {
55 | return async (dispatch) => {
56 | dispatch(decommissioningPawsRequest())
57 | PawService.decommissionPaw(paws)
58 | .then(paws => {
59 | dispatch(decommissioningPawsSuccess([]));
60 | dispatch(getPaws())
61 | })
62 | .catch(error => dispatch(decommissioningPawsFailure(error)))
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/User-Interface/src/features/device/slicer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { createSlice } from "@reduxjs/toolkit";
5 | import type { PayloadAction } from "@reduxjs/toolkit"
6 | import { PsmDeviceList } from "../../models/mocks";
7 | import type { RootState } from "../../store/store"
8 | import type { IPsmDevice } from "../../models"
9 |
10 | // Create an empty device list to satisfy the type checker
11 | const emptyDeviceList: IPsmDevice[] = [];
12 |
13 | // Create the device slicer
14 | export const deviceSlice = createSlice({
15 | // Domain of device
16 | name: 'device',
17 | // Initially empty
18 | initialState: { deviceList: emptyDeviceList },
19 | // Actions that can be performed on this data structure
20 | reducers: {
21 | // Set the list of devices, this is usually called when the get devices API is called. Can completly replace all devices
22 | setDeviceList: (state, action: PayloadAction) => {
23 | state.deviceList = action.payload;
24 | },
25 | // Create a new device from an existing autopilot device
26 | commissionDevice: (state, action: PayloadAction) => {
27 | // Add a new device to the beginning of the array
28 | state.deviceList.unshift(action.payload);
29 | },
30 | // Remove the specified device
31 | decommissionDevice: (state, action: PayloadAction) => {
32 | // Return the specified device by ID
33 | state.deviceList.filter((device) => { return device.id !== action.payload });
34 | },
35 | }
36 | })
37 |
38 | // Expose the reducer's actions
39 | export const { setDeviceList, commissionDevice, decommissionDevice } = deviceSlice.actions
40 |
41 | // Get the list of devices from the server's API
42 | // TODO: finish async retrieval later
43 | export function getDeviceList() {
44 | setTimeout(
45 | (dispatch) => {
46 | dispatch(setDeviceList(PsmDeviceList))
47 | },
48 | 1000
49 | );
50 | }
51 |
52 | // Configure a selector for the device list
53 | export function selectDevice(state: RootState) {
54 | return state.device.deviceList
55 | }
--------------------------------------------------------------------------------
/User-Interface/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/User-Interface/src/services/deviceService.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import type { IPsmAutopilotDevice } from "../models";
5 | // import { autopilotDeviceList } from '../models/mocks/autopilotDeviceMock';
6 |
7 | export interface IDeviceService {
8 | getDevices: () => Promise,
9 | }
10 | export class DeviceService {
11 | public static API_BASE_URL = document.location.origin;
12 | public static async getDevices(): Promise {
13 |
14 | /*
15 | Comment the following code to work with device mocks
16 | */
17 |
18 | // Define the URL to query against
19 | const getDevicesUrl = `${this.API_BASE_URL}/API/Lifecycle/AutopilotDevice`
20 |
21 | // Grab a list of autopilot devices
22 | const response = await fetch(getDevicesUrl);
23 |
24 | // Parse the JSON return of the API call
25 | const result = await response.json();
26 |
27 | // Return the specified
28 | return result.map((device: IPsmAutopilotDevice) => {
29 | let computedName = "";
30 | computedName = device.displayName === undefined || device.displayName === "" ? device.serialNumber : device.displayName
31 | return {
32 | displayName: computedName,
33 | azureActiveDirectoryDeviceId: device.azureActiveDirectoryDeviceId,
34 | azureAdDeviceId: device.azureActiveDirectoryDeviceId,
35 | serialNumber: device.serialNumber
36 | };
37 | });
38 |
39 | /*
40 | uncomment the following return to work with device mocks, also uncomment devices mock import above
41 | */
42 |
43 | // Map the mock data to the redux store's device list format
44 | // return autopilotDeviceList.map((device) => {
45 | // if (device.displayName === undefined) { device.displayName = device.serialNumber }
46 | // return {
47 | // displayName: device.displayName,
48 | // azureActiveDirectoryDeviceId: device.azureActiveDirectoryDeviceId,
49 | // azureAdDeviceId: device.azureAdDeviceId,
50 | // serialNumber: device.serialNumber
51 | // };
52 | // });
53 | }
54 | }
--------------------------------------------------------------------------------
/User-Interface/src/components/CommissionPawsPanel/CommissionPawsPanelContent/SelectedItems/SelectedItems.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import React from "react";
5 | import { DetailsList, IColumn, IconButton, Label, Stack } from "@fluentui/react";
6 | import { RootStateOrAny, useDispatch, useSelector } from "react-redux";
7 | import { IPsmAutopilotDevice } from "../../../../models";
8 | import { REMOVE_DEVICE_FROM_COMMISSION_PAW_LIST } from "../../../../store/actions/pawActions";
9 |
10 | export const SelectedItems = () => {
11 | const selectedItems = useSelector((state: RootStateOrAny) => state.paw.commissionPaws.devicesToCommission);
12 | const dispatch = useDispatch();
13 |
14 | const onRemoveSelectedDevice = (deviceItem: IPsmAutopilotDevice) => {
15 | dispatch({
16 | type: REMOVE_DEVICE_FROM_COMMISSION_PAW_LIST,
17 | payload: deviceItem
18 | })
19 | };
20 |
21 | const deviceSummaryColumn: IColumn = {
22 | key: 'deviceId',
23 | name: 'Selected Items',
24 | fieldName: 'deviceId',
25 | minWidth: 150,
26 | maxWidth: 200,
27 | isRowHeader: true,
28 | isResizable: true,
29 | data: 'string',
30 | onRender: (item: IPsmAutopilotDevice) => {
31 | return (
32 |
33 |
34 |
35 |
36 | {item?.azureAdDeviceId}
37 |
38 |
39 | onRemoveSelectedDevice(item)}
43 | />
44 |
45 |
46 | );
47 | },
48 | isPadded: false,
49 | };
50 | return (
51 |
52 | {(selectedItems && selectedItems.length > 0)
53 | ?
54 | :
55 | }
56 |
57 | )
58 | };
59 |
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceContainer/DeviceContainer.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { DefaultPalette, IStackStyles, Stack, ThemeProvider } from '@fluentui/react';
5 | import { useBoolean } from '@fluentui/react-hooks';
6 | import { initializeIcons } from '@fluentui/react/lib/Icons';
7 | import React, { useEffect } from 'react';
8 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
9 | import { getPaws } from '../../store/actions/pawActions';
10 | import { darkTheme } from '../../themes';
11 | import { CommissionPawsPanel } from '../CommissionPawsPanel';
12 | import { DeviceItemList } from '../DeviceItemList/DeviceItemList';
13 | import { Header } from '../Header/Header';
14 | import { LeftNav } from '../LeftNav/LeftNav';
15 | import { PawActions } from '../PawActions';
16 |
17 | initializeIcons(/* optional base url */);
18 |
19 | export const DeviceContainer = () => {
20 | const dispatch = useDispatch();
21 | const paws = useSelector((state: RootStateOrAny) => state.paw.getPaws.paws);
22 | const [isCommissionPawsPanelOpen, { setTrue: openCommissionPawsPanel, setFalse: dismissCommissionPawsPanel }] = useBoolean(false);
23 |
24 | useEffect(() => {
25 | dispatch(getPaws())
26 | }, [dispatch]);
27 | const stackStyles: IStackStyles = {
28 | root: {
29 |
30 | },
31 | };
32 | const leftNavStyles: IStackStyles = {
33 | root: {
34 | //background: DefaultPalette.themeDark,
35 | color: DefaultPalette.white,
36 | minWidth: '300px',
37 | display: 'grid'
38 | },
39 | };
40 | const contentStyles: IStackStyles = {
41 | root: {
42 | //color: DefaultPalette.white,
43 | width: '100%',
44 | minWidth: '900px',
45 | display: 'grid'
46 | },
47 | };
48 | return (
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/User-Interface/src/components/CommissionPawsPanel/CommissionPawsPanelContent/CommissionPawsPanelContent.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { FocusZone, FocusZoneDirection, IStackStyles, IStackTokens, SearchBox, Selection, Stack } from '@fluentui/react';
5 | import React, { useMemo, useState } from 'react';
6 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
7 | import { IPsmAutopilotDevice } from '../../../models';
8 | import { SELECT_DEVICES_TO_COMMISSION_PAW } from '../../../store/actions/pawActions';
9 | import { FilteredItems } from './FilteredItems/FilteredItems';
10 | import { SelectedItems } from './SelectedItems/SelectedItems';
11 |
12 | export const CommissionPawsPanelContent = () => {
13 | const dispatch = useDispatch();
14 | const [searchTerm, setSearchTerm] = useState('');
15 | const devices: IPsmAutopilotDevice[] = useSelector((state: RootStateOrAny) => state.devices.getDevices.devices);
16 | const stackTokens: Partial = { childrenGap: 0 };
17 |
18 | const selection = new Selection({
19 | onSelectionChanged: () => {
20 | onDeviceSelected(selection.getSelection() as IPsmAutopilotDevice[])
21 | },
22 | });
23 |
24 | const onSearch = (newValue: string) => {
25 | setSearchTerm(newValue.toLowerCase());
26 | };
27 |
28 | const containerStyles: IStackStyles = {
29 | root: {
30 | marginTop: 20
31 | },
32 | };
33 |
34 |
35 | const onDeviceSelected = (deviceItems: IPsmAutopilotDevice[]) => {
36 | dispatch({
37 | type: SELECT_DEVICES_TO_COMMISSION_PAW,
38 | payload: deviceItems,
39 | });
40 | };
41 |
42 | const FilteredItemsMemo = useMemo(
43 | () => {
44 | if (searchTerm === "") {
45 | var filteredDevices = devices;
46 | } else {
47 | filteredDevices = devices.filter((device) => {
48 | if (device.azureAdDeviceId.toLowerCase().includes(searchTerm) || device.displayName?.toLowerCase().includes(searchTerm)) {
49 | return true
50 | } else {
51 | return false
52 | }
53 | });
54 | }
55 | return ;
56 | },
57 | [devices, searchTerm]
58 | );
59 |
60 | return (
61 | <>
62 |
63 |
64 |
65 | {
66 | FilteredItemsMemo
67 | }
68 |
69 |
70 |
71 | >
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/User-Interface/src/components/Header/Header.css:
--------------------------------------------------------------------------------
1 | /* Copyright (c) Microsoft Corporation. */
2 | /* Licensed under the MIT license. */
3 |
4 | /* Add a black background color to the top navigation */
5 | .topnav {
6 | background-color: #333;
7 | overflow: hidden;
8 | }
9 |
10 | /* Style the links inside the navigation bar */
11 | .topnav a {
12 | float: left;
13 | color: #f2f2f2;
14 | text-align: center;
15 | padding: 14px 16px;
16 | text-decoration: none;
17 | font-size: 13px;
18 | font-weight: bold;
19 | }
20 |
21 | .titleheader{
22 | /* background-color: #333; */
23 | overflow: hidden;
24 | }
25 |
26 | .titleheader a {
27 | float: left;
28 | color: #f2f2f2;
29 | text-align: center;
30 | padding: 6px 15px 1px 15px;
31 | text-decoration: none;
32 | font-weight: 600;
33 | font-size: 24px;
34 | line-height: 28px;
35 | flex: 0 1 auto;
36 | margin-right: 16px;
37 | }
38 |
39 | .titleheaderspan {
40 | float: left;
41 | color: #a1a0a3;
42 | font-size: 10px;
43 | line-height: 28px;
44 | flex: 0 1 auto;
45 | margin-left: 17px;
46 | margin-top: 1px;
47 | }
48 |
49 | /* Style the search box inside the navigation bar */
50 | .topnav input[type=text] {
51 |
52 | padding: 3px;
53 | border: none;
54 | margin-top: 8px;
55 | margin-right: 16px;
56 | font-size: 17px;
57 | letter-spacing: normal;
58 | word-spacing: normal;
59 | line-height: normal;
60 | text-transform: none;
61 | text-indent: 0px;
62 | text-shadow: none;
63 | display: inline-block;
64 | text-align: start;
65 | appearance: auto;
66 | -webkit-rtl-ordering: logical;
67 | cursor: text;
68 | width: 343px;
69 | }
70 |
71 |
72 | /* Style the list */
73 | ul.breadcrumb {
74 | padding: 10px 16px;
75 | list-style: none;
76 | background-color: #525252;
77 | margin: auto;
78 | font-size: small;
79 | }
80 |
81 | /* Display list items side by side */
82 | ul.breadcrumb li {
83 | display: inline;
84 | font-size: 18px;
85 | }
86 |
87 | /* Add a slash symbol (/) before/behind each list item */
88 | ul.breadcrumb li+li:before {
89 | padding: 8px;
90 | color: black;
91 | content: "/\00a0";
92 | }
93 |
94 | /* Add a color to all links inside the list */
95 | ul.breadcrumb li a {
96 | color: #0275d8;
97 | text-decoration: none;
98 | }
99 |
100 | /* Add a color on mouse-over */
101 | ul.breadcrumb li a:hover {
102 | color: #01447e;
103 | text-decoration: underline;
104 | }
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/pawReducers/assignmentPawReducer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import {
5 | GETTING_PAW_ASSIGNMENT_REQUEST,
6 | GETTING_PAW_ASSIGNMENT_SUCCESS,
7 | GETTING_PAW_ASSIGNMENT_FAILURE,
8 | ASSIGN_PAW_REQUEST,
9 | ASSIGN_PAW_SUCCESS,
10 | ASSIGN_PAW_FAILURE,
11 | UNASSIGN_PAW_REQUEST,
12 | UNASSIGN_PAW_SUCCESS,
13 | UNASSIGN_PAW_FAILURE
14 | } from "../../actions/pawActions";
15 |
16 | // Define the initial state of the assignment store
17 | const initialState = {
18 | isGettingAssignmentList: false,
19 | isAssigning: false,
20 | isRemovingAssignment: false,
21 | assignedUserList: [],
22 | addUpnList: [],
23 | removeUpnList: [],
24 | message: undefined,
25 | error: undefined
26 | };
27 |
28 | // TODO: write docs
29 | export function assignPaw(state = initialState, action: any) {
30 | switch (action.type) {
31 | case GETTING_PAW_ASSIGNMENT_REQUEST:
32 | return {
33 | ...state,
34 | isGettingAssignmentList: true,
35 | message: "Getting User Assignments..."
36 | };
37 | case GETTING_PAW_ASSIGNMENT_SUCCESS:
38 | return {
39 | ...state,
40 | isGettingAssignmentList: false,
41 | message: undefined,
42 | assignedUserList: action.payload,
43 | error: undefined
44 | };
45 | case GETTING_PAW_ASSIGNMENT_FAILURE:
46 | return {
47 | ...state,
48 | isGettingAssignmentList: false,
49 | error: action.payload,
50 | message: undefined,
51 | };
52 | case ASSIGN_PAW_REQUEST:
53 | return {
54 | ...state,
55 | // do stuff
56 | };
57 | case ASSIGN_PAW_SUCCESS:
58 | return {
59 | ...state,
60 | // do stuff
61 | // Put banner at top for success
62 | };
63 | case ASSIGN_PAW_FAILURE:
64 | return {
65 | ...state,
66 | // do stuff
67 | };
68 | case UNASSIGN_PAW_REQUEST:
69 | return {
70 | ...state,
71 | // do stuff
72 | };
73 | case UNASSIGN_PAW_SUCCESS:
74 | return {
75 | ...state,
76 | // do stuff
77 | // Put a banner at the top for success
78 | };
79 | case UNASSIGN_PAW_FAILURE:
80 | return {
81 | ...state,
82 | // do stuff
83 | };
84 | default:
85 | return state;
86 | };
87 | };
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
40 |
41 |
--------------------------------------------------------------------------------
/User-Interface/src/components/CommissionPawsPanel/CommissionPawsPanelContent/FilteredItems/FilteredItems.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import React, { useCallback, useMemo } from "react";
5 | import { DetailsList, IColumn, IconButton, Label, Stack, Selection, Dropdown, FocusZone } from "@fluentui/react";
6 | import { useDispatch } from "react-redux";
7 | import { IPsmAutopilotDevice } from "../../../../models";
8 | import { SELECT_DEVICES_TO_COMMISSION_PAW, SELECT_PAW_TYPE_TO_COMMISSION } from "../../../../store/actions/pawActions";
9 |
10 | interface IFilteredItemsProps {
11 | items: IPsmAutopilotDevice[];
12 | }
13 |
14 | export const FilteredItems = ({ items }: IFilteredItemsProps) => {
15 | const dispatch = useDispatch();
16 |
17 | const getKey = (item: IPsmAutopilotDevice, index?: number): string => {
18 | return item.azureAdDeviceId;
19 | };
20 |
21 | const onDeviceSelected = (deviceItems: IPsmAutopilotDevice[]) => {
22 | dispatch({
23 | type: SELECT_DEVICES_TO_COMMISSION_PAW,
24 | payload: deviceItems,
25 | });
26 | };
27 |
28 | const selection = new Selection({
29 | onSelectionChanged: () => {
30 | onDeviceSelected(selection.getSelection() as IPsmAutopilotDevice[])
31 | },
32 | });
33 |
34 | const onPawTypeChange = useCallback((event, option) => {
35 | dispatch({
36 | type: SELECT_PAW_TYPE_TO_COMMISSION,
37 | payload: option.key
38 | })
39 | }, [dispatch]);
40 |
41 | const PawTypeDropDown = useMemo(() => {
42 | const dropdownStyles = { dropdown: { marginTop: 20 } };
43 | return ;
53 | }, [onPawTypeChange])
54 | const deviceSummaryColumn: IColumn = {
55 | key: 'azureAdDeviceId',
56 | name: 'Select to commission',
57 | fieldName: 'azureAdDeviceId',
58 | minWidth: 150,
59 | maxWidth: 200,
60 | isRowHeader: true,
61 | isResizable: true,
62 | data: 'string',
63 | onRender: (item: IPsmAutopilotDevice) => {
64 | return (
65 |
66 |
67 |
68 |
69 | {item?.azureAdDeviceId}
70 |
71 |
72 | );
73 | },
74 | isPadded: false,
75 | };
76 | // show max of 4 search result
77 | return (
78 |
79 |
87 | {PawTypeDropDown}
88 |
89 | )
90 | };
91 |
--------------------------------------------------------------------------------
/Server/src/Routes/DeploymentEngine.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import type express from "express";
5 | import type { ConfigurationEngine } from "../Startup/ConfigEngine";
6 | import type { AppGraphClient } from "../Utility";
7 | import { InternalAppError, writeDebugInfo } from "../Utility";
8 |
9 | class DeploymentEngineRouter {
10 | // Define the properties that will be available to the class
11 | private webServer: express.Express;
12 | private graphClient: AppGraphClient;
13 | private configEngine: ConfigurationEngine;
14 |
15 | // Define how the class should be instantiated
16 | constructor(webServer: express.Express, graphClient: AppGraphClient, configEngine: ConfigurationEngine) {
17 |
18 | // Make the express instance available to the class
19 | this.webServer = webServer;
20 |
21 | // Make the graph client instance available to the class
22 | this.graphClient = graphClient;
23 |
24 | // Make the config engine instance available to the class
25 | this.configEngine = configEngine;
26 |
27 | // Initialize the routes
28 | this.initRoutes();
29 | };
30 |
31 | // Initialize the web server's deployment routes
32 | private initRoutes(): void {
33 | // Trigger the core security group deployment
34 | this.webServer.post("/API/Deploy", async (request, response, next) => {
35 | try {
36 | // Send the boolean response of the deployment operation
37 | response.send(await this.configEngine.deployConfigTag(request.body.userConcent));
38 | } catch (error) {
39 | // Check to see if the error is an error
40 | if (error instanceof InternalAppError) {
41 | if (error.name === "Invalid Input") {
42 | // Write debug info.
43 | writeDebugInfo(error, "Input Validation Error:");
44 |
45 | // Send a note back to the client about input.
46 | response.send("The expected input is not valid: " + error.message);
47 | // Since our errors are safe, return it to the caller
48 | } else {
49 | // Write debug info
50 | writeDebugInfo(error, "One of our other errors:");
51 |
52 | // Send the error details back
53 | response.send("Unhandled custom internal error: " + error.name);
54 | }
55 | // If the error is not our error, it is unhandled as we didn't control the error
56 | } else {
57 | // Write debug info
58 | writeDebugInfo(error, "Unhandled error:");
59 |
60 | // Send a sanitized generic error back to the caller.
61 | response.send("An unknown error occurred!");
62 | };
63 | };
64 | });
65 |
66 | // Place holder for the core infrastructure single click to deploy method
67 | this.webServer.post("/deploy", async (request, response, next) => {});
68 | };
69 | };
--------------------------------------------------------------------------------
/User-Interface/src/store/reducers/pawReducers/commissionPawReducer.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import type { IPsmAutopilotDevice } from '../../../models';
5 |
6 | import {
7 | DECOMMISSIONING_PAW_SELECTED,
8 | DECOMMISSIONING_PAWS_REQUEST,
9 | DECOMMISSIONING_PAWS_SUCCESS,
10 | DECOMMISSIONING_PAWS_FAILURE,
11 | SELECT_DEVICES_TO_COMMISSION_PAW,
12 | REMOVE_DEVICE_FROM_COMMISSION_PAW_LIST,
13 | SELECT_PAW_TYPE_TO_COMMISSION,
14 | COMMISSIONING_PAWS_REQUEST,
15 | COMMISSIONING_PAWS_SUCCESS,
16 | COMMISSIONING_PAWS_FAILURE
17 | } from '../../actions/pawActions';
18 |
19 | const initialState = {
20 | pawsToDecommission: [],
21 | devicesToCommission: [],
22 | pawTypeToCommission: undefined,
23 | isPawCommissioning: false,
24 | isPawDecommissioning: false,
25 | message: undefined,
26 | error: undefined,
27 | };
28 |
29 | export const commissionPaws = (state = initialState, action: any) => {
30 | switch (action.type) {
31 | case DECOMMISSIONING_PAW_SELECTED:
32 | return {
33 | ...state,
34 | pawsToDecommission: [...action.payload],
35 | };
36 | case DECOMMISSIONING_PAWS_REQUEST:
37 | return {
38 | ...state,
39 | isPawDecommissioning: true,
40 | };
41 | case DECOMMISSIONING_PAWS_SUCCESS:
42 | return {
43 | ...state,
44 | message: 'Decomissioning success',
45 | isPawDecommissioning: false,
46 | };
47 | case DECOMMISSIONING_PAWS_FAILURE:
48 | return {
49 | ...state,
50 | isPawDecommissioning: false,
51 | error: action.payload,
52 | message: undefined,
53 | };
54 | case SELECT_DEVICES_TO_COMMISSION_PAW:
55 | return {
56 | ...state,
57 | devicesToCommission: [...action.payload]
58 | };
59 | case SELECT_PAW_TYPE_TO_COMMISSION:
60 | return {
61 | ...state,
62 | pawTypeToCommission: action.payload
63 | };
64 | case COMMISSIONING_PAWS_REQUEST:
65 | return {
66 | ...state,
67 | isPawCommissioning: true,
68 | };
69 | case COMMISSIONING_PAWS_SUCCESS:
70 | return {
71 | ...state,
72 | message: 'Commissioning success',
73 | isPawCommissioning: false,
74 | };
75 | case COMMISSIONING_PAWS_FAILURE:
76 | return {
77 | ...state,
78 | isPawCommissioning: false,
79 | error: action.payload,
80 | message: undefined,
81 | };
82 | case REMOVE_DEVICE_FROM_COMMISSION_PAW_LIST:
83 | return {
84 | ...state,
85 | devicesToCommission: state.devicesToCommission.filter((device: IPsmAutopilotDevice) => device.azureAdDeviceId !== action.payload.azureAdDeviceId)
86 | };
87 | default:
88 | return state;
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/Server/.vscode/server.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | // Place your Cloud-PAW-Management workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
7 | // Placeholders with the same ids are connected.
8 | // Example:
9 | // "Print to console": {
10 | // "scope": "javascript,typescript",
11 | // "prefix": "log",
12 | // "body": [
13 | // "console.log('$1');",
14 | // "$2"
15 | // ],
16 | // "description": "Log output to console"
17 | // }
18 | "Route Callback (Custom, Insecure)": {
19 | "prefix": "route-callback",
20 | "description": "Pre-built callback with error handling",
21 | "scope": "javascript,typescript",
22 | "body": [
23 | "async (request, response, next) => {",
24 | " \/\/ Catch execution errors",
25 | " try {",
26 | " \/\/ ${2:Describe the action}",
27 | " ${1:\/\/Action to execute};",
28 | " } catch (error) {",
29 | " \/\/ Send the error details if something goes wrong",
30 | " next(error);",
31 | " };",
32 | "}"
33 | ]
34 | },
35 | "Write Debug (Custom)": {
36 | "prefix": "writeDebugInfo",
37 | "description": "A prebuilt write debug info snippet that includes the necessary comment.",
38 | "scope": "javascript,typescript",
39 | "body": [
40 | "\/\/ Write debug info",
41 | "writeDebugInfo(${1:Data to log}, \"${2:Message header, optional}\");",
42 | ""
43 | ]
44 | },
45 | "Throw Internal App Error (Custom)": {
46 | "prefix": "throw InternalAppError",
47 | "description": "Throw a new internal app error using the error handler built into the project's utility file.",
48 | "scope": "javascript,typescript",
49 | "body": [
50 | "\/\/ Throw an error",
51 | "throw new InternalAppError(\"${1:Error Message}\", \"${2:Error Type}\", \"${3:Stack Trace}\");"
52 | ]
53 | },
54 | "Try Catch - Internal Edition": {
55 | "prefix": "trycatch - internal",
56 | "description": "A best practice pre-built try catch block",
57 | "scope": "javascript,typescript",
58 | "body": [
59 | "// Catch execution errors",
60 | "try {",
61 | " ${1:code to execute;}",
62 | "} catch (error) { // If an error happens",
63 | " // Check if error is internal and pass it directly if it is.",
64 | " if (error instanceof InternalAppError) {",
65 | " // Send the current error instance up since it is an internal error.",
66 | " throw error;",
67 | " } else {",
68 | " // Throw an error",
69 | " throw new InternalAppError(\"${2:Error Message}\", \"Unknown Error\", \"${3:Scope}\");",
70 | " };",
71 | "};",
72 | ]
73 | },
74 | "Unit Test - Valid Test": {
75 | "prefix": "UTValid",
76 | "description": "A template for creating a valid unit test.",
77 | "scope": "javascript,typescript",
78 | "body": [
79 | "\/\/ ",
80 | "\/\/"
81 | ]
82 | }
83 | }
--------------------------------------------------------------------------------
/Server/src/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import express from "express";
5 | import helmet from "helmet";
6 | import * as path from "path";
7 | import { DebugRouter } from "./Routes/DebugRoute";
8 | import { LifecycleRouter } from "./Routes/LifecycleManagement";
9 | import { SwaggerUI } from "./Routes/OpenAPI";
10 | import { MSAzureAccessCredential } from "./Startup/Authentication";
11 | import { ConfigurationEngine } from "./Startup/ConfigEngine";
12 | import { AppGraphClient, writeDebugInfo } from "./Utility";
13 |
14 | // Import environmental variables
15 | const port = process.env.PORT || 3000;
16 | const debugMode = process.env.PSM_Debug || "false"
17 | const headlessOperation = process.env.PSM_Headless || "false"
18 |
19 | // Generate an authentication session that can create access tokens.
20 | // This will automatically use available credentials available in Managed Identity, Key Vault or environmental vars.
21 | const azureAuthSession = new MSAzureAccessCredential();
22 |
23 | // Initialize the graph client
24 | const graphClient = new AppGraphClient(azureAuthSession.credential);
25 |
26 | // Initialize the configuration engine
27 | const configEngine = new ConfigurationEngine(graphClient);
28 |
29 | // Initialize Express
30 | const webServer = express();
31 |
32 | // Parse the request bodies so that they can be used as objects instead of raw text
33 | webServer.use(express.json());
34 |
35 | // TODO: Properly config the CSP settings so they are react compatible to bring more security
36 | // Quick configure Express to be more secure, disabling the CSP because it breaks react
37 | webServer.use(helmet({ "contentSecurityPolicy": false }));
38 |
39 | // Check to see if the UI has been suppressed.
40 | if (headlessOperation !== "true") {
41 | // Serve up the UI directory
42 | webServer.use(express.static(path.join(__dirname, "UI")));
43 | webServer.use("/devices", express.static(path.join(__dirname, "UI")));
44 | webServer.use("/devices/:DeviceId", express.static(path.join(__dirname, "UI")));
45 |
46 | // Write the info about the static files being served
47 | writeDebugInfo(path.join(__dirname, "UI"), "Static file path:");
48 | };
49 |
50 | // If debug mode is enabled, enable the debug routes
51 | if (debugMode === "true") {
52 | // Instantiate an instance of the debug router which will add of the debugging routes
53 | new DebugRouter(webServer, graphClient, configEngine, azureAuthSession.credential);
54 |
55 | // Stop the server if the stop command is issued
56 | // This can't be in the debug routes as the server instance can't be exposed there.
57 | webServer.get('/stop', (request, response) => {
58 | // Notify the caller
59 | response.send("Stopping Server...");
60 |
61 | // Log to console the server stop status
62 | console.log("Stopping Server...");
63 |
64 | // Stop the server
65 | serverInstance.close();
66 | });
67 |
68 | // Instantiate an instance of the OpenAPI docs engine
69 | new SwaggerUI(webServer);
70 | };
71 |
72 | // Initialize the core business logic routes
73 | new LifecycleRouter(webServer, graphClient, configEngine);
74 |
75 | // Set the startup indicator as false to indicate that the app is no longer starting up
76 | configEngine.startup = false;
77 |
78 | // Start the web server
79 | const serverInstance = webServer.listen(port, () => {
80 | writeDebugInfo("Running on port: " + port, "Server Started");
81 | });
--------------------------------------------------------------------------------
/Server/src/Utility/types.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | // Define the structure of a Infrastructure Setting
5 | /*
6 | Name = Unique display name of the configuration being set.
7 | Description = Human readable description of what the setting is, who manages it and why it is needed.
8 | An exception to this is meta-data storage for the core system to avoid the need for a database.
9 | HostType = The type of host that the management/IDP systems are hosted on. Different Architectures are required dor different host types.
10 | HostProvider = The host provider is the type of host that the security orchestrator will be hosted on. This will be used for platform specific configuration strategies.
11 | Id = A unique identifier for the setting to be set. Not usually used.
12 | Path = A path in the configuration management system where a setting is located so that the orchestrator can set the setting.
13 | Setting = An object that contains all the required configurations for the specified host provider.
14 | Signature = A cryptographic signature of this object, minus the signature property. This prevents tamper of settings delivery.
15 | */
16 | export interface IInfrastructureSetting {
17 | "Name": string,
18 | "Description": string,
19 | "HostType": "Cloud" | "Government" | "Air-Gap" | "On-Prem",
20 | "HostProvider": "Azure" | "AWS" | "GCP" | "SCCM" | "AD DS" | "MS Endpoint Manager" | "Tanium" | "Mobile Iron",
21 | "Id"?: string,
22 | "Path": string,
23 | "Setting": {},
24 | "Signature": string
25 | }
26 |
27 | // Define the structure of an Architecture
28 | export interface IArchitectureSpecification {
29 | "SettingList": IInfrastructureSetting[],
30 | "Signature": string
31 | }
32 |
33 | // Define the Endpoint Manager Role Scope Tag data format.
34 | export interface ICloudSecConfigIncomplete {
35 | "PAWSecGrp"?: string,
36 | "UsrSecGrp"?: string,
37 | "SiloRootGrp"?: string,
38 | "BrkGls"?: string,
39 | "UsrTag"?: string,
40 | "ScopeTagID"?: string
41 | };
42 |
43 | export interface ICloudSecConfig {
44 | "PAWSecGrp": string,
45 | "UsrSecGrp": string,
46 | "SiloRootGrp": string,
47 | "BrkGls": string,
48 | "UsrTag": string,
49 | "ScopeTagID": string
50 | };
51 |
52 | /*
53 | * CommissionedDate = is the ISO 8601 string format of the time representing the commission date of the PAW.
54 | * GroupAssignment = This is the ID of the Custom CSP Device Configuration that configures the local admin and local hyper-v group memberships.
55 | * Type = Is the commission type of PAW.
56 | * UserAssignment = The ID of the Settings Catalog that contains the user rights assignment of the specified PAW device.
57 | */
58 | // Define the PAW Configuration Spec
59 | export interface IDeviceGroupConfig {
60 | CommissionedDate: Date,
61 | GroupAssignment: string,
62 | Type: "Privileged" | "Developer" | "Tactical",
63 | UserAssignment: string
64 | };
65 |
66 | /*
67 | id = DeviceID of the Managed Device
68 | DisplayName = The computer name of the device according to AAD.
69 | ParentGroup = the ObjectID of the unique device group that the managed device is a member of
70 | ParentDevice = is an optional property that is the DeviceID of the parent managed device
71 | */
72 | // Define the structure of the PAW device object
73 | export interface IDeviceObject extends IDeviceGroupConfig {
74 | id: string,
75 | DisplayName: string,
76 | ParentDevice?: string,
77 | ParentGroup: string
78 | };
--------------------------------------------------------------------------------
/User-Interface/src/components/DeviceItemList/DeviceItemList.tsx:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { CheckboxVisibility, DetailsList, Selection, DetailsListLayoutMode, IColumn, SelectionMode } from '@fluentui/react';
5 | import { useState } from 'react';
6 | import { useDispatch } from 'react-redux';
7 | import type { IPsmAutopilotDevice, IPsmDevice } from '../../models';
8 | import { IPawItemListProps } from './DeviceItemList.types';
9 | import { DECOMMISSIONING_PAW_SELECTED } from '../../store/actions/pawActions';
10 | export const DeviceItemList = (props: IPawItemListProps) => {
11 | const dispatch = useDispatch();
12 | const [isCompactMode, ] = useState(false);
13 | const onColumnClick = (ev: React.MouseEvent, column: IColumn): void => {
14 | };
15 | const pawDisplayNameColumn: IColumn = {
16 | key: 'pawDisplayName',
17 | name: 'Display Name',
18 | fieldName: 'DisplayName',
19 | minWidth: 100,
20 | maxWidth: 120,
21 | isRowHeader: true,
22 | isResizable: true,
23 | onColumnClick: onColumnClick,
24 | data: 'string',
25 | isPadded: true,
26 | };
27 | const pawIdColumn: IColumn = {
28 | key: 'pawId',
29 | name: 'PAW ID',
30 | fieldName: 'id',
31 | minWidth: 275,
32 | maxWidth: 350,
33 | isRowHeader: true,
34 | isResizable: true,
35 | onColumnClick: onColumnClick,
36 | data: 'string',
37 | isPadded: true,
38 | };
39 |
40 | const pawTypeColumn: IColumn = {
41 | key: 'pawType',
42 | name: 'PAW Type',
43 | fieldName: 'Type',
44 | minWidth: 100,
45 | maxWidth: 100,
46 | isRowHeader: true,
47 | isResizable: true,
48 | onColumnClick: onColumnClick,
49 | data: 'string',
50 | isPadded: true,
51 | };
52 |
53 | const commissionDateColumn: IColumn = {
54 | key: 'commissionDate',
55 | name: 'Commission Date',
56 | fieldName: 'CommissionedDate',
57 | minWidth: 210,
58 | isRowHeader: true,
59 | isResizable: true,
60 | onColumnClick: onColumnClick,
61 | data: 'string',
62 | isPadded: true,
63 | };
64 |
65 | const parentDeviceIdColumn: IColumn = {
66 | key: 'parentDeviceId',
67 | name: 'Parent Device',
68 | fieldName: 'ParentDevice',
69 | minWidth: 275,
70 | // maxWidth: 350,
71 | isRowHeader: true,
72 | isResizable: true,
73 | onColumnClick: onColumnClick,
74 | data: 'string',
75 | isPadded: true,
76 | };
77 |
78 | const columns: IColumn[] = [pawDisplayNameColumn, pawIdColumn, pawTypeColumn, commissionDateColumn, parentDeviceIdColumn];
79 |
80 | const onPawSelected = (item: IPsmDevice[]) => {
81 | dispatch({
82 | type: DECOMMISSIONING_PAW_SELECTED,
83 | payload: item
84 | })
85 | };
86 |
87 | const selection = new Selection({
88 | onSelectionChanged: () => {
89 | onPawSelected(selection.getSelection() as IPsmDevice[])
90 | },
91 | });
92 |
93 | const getKey = (item: IPsmAutopilotDevice, index?: number): string => {
94 | return item.azureActiveDirectoryDeviceId;
95 | };
96 |
97 | return ;
109 | };
--------------------------------------------------------------------------------
/User-Interface/src/components/PawActions/PawActions.tsx:
--------------------------------------------------------------------------------
1 | import { CommandBar, DefaultButton, Dialog, DialogFooter, DialogType, ICommandBarItemProps, PrimaryButton, Spinner, Stack } from '@fluentui/react';
2 | import { useBoolean } from '@fluentui/react-hooks';
3 | import React, { useCallback, useMemo } from 'react';
4 | import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
5 | import { decommissionPaws, getPaws } from '../../store/actions/pawActions';
6 |
7 | interface IPawActionsProps {
8 | onCommissionPaws: () => void;
9 | onDecommissionPaws?: () => void;
10 | }
11 | export const PawActions = (props: IPawActionsProps) => {
12 | const pawsToDecommission = useSelector((state: RootStateOrAny) => state.paw.commissionPaws.pawsToDecommission);
13 | const isPawDecommissioning = useSelector((state: RootStateOrAny) => state.paw.commissionPaws.isPawDecommissioning);
14 | const isPawCommissioning = useSelector((state: RootStateOrAny) => state.paw.commissionPaws.isPawCommissioning);
15 | const isGettingPaws = useSelector((state: RootStateOrAny) => state.paw.getPaws.isGettingPaws);
16 |
17 | const [hideDialog, { toggle: toggleHideDialog }] = useBoolean(true);
18 | const dispatch = useDispatch();
19 |
20 | const decommissionSelectedPaw = useCallback(() => {
21 | dispatch(decommissionPaws(pawsToDecommission));
22 | toggleHideDialog()
23 | }, [dispatch, pawsToDecommission, toggleHideDialog]);
24 |
25 | const onRefershPaws = useCallback(() => {
26 | dispatch(getPaws())
27 | }, [dispatch]);
28 |
29 | const _items: ICommandBarItemProps[] = [
30 | {
31 | key: 'addGroup',
32 | text: 'Commission PAW',
33 | iconProps: { iconName: 'AddGroup' },
34 | onClick: () => props.onCommissionPaws(),
35 | },
36 | {
37 | key: 'delete',
38 | text: 'Decommission Selected PAW',
39 | iconProps: { iconName: 'Delete' },
40 | disabled: !(pawsToDecommission?.length > 0),
41 | onClick: () => { toggleHideDialog() },
42 | },
43 | {
44 | key: 'refresh',
45 | text: 'Refresh',
46 | iconProps: { iconName: 'Refresh' },
47 | onClick: () => { onRefershPaws() },
48 | },
49 | ];
50 |
51 |
52 | const modalProps = React.useMemo(
53 | () => ({
54 | isBlocking: true,
55 | styles: { main: { maxWidth: 450 } },
56 | }),
57 | [],
58 | );
59 |
60 | const DecommissionPawDialog = useMemo(() => {
61 | const dialogContentProps = {
62 | type: DialogType.normal,
63 | title: 'Decommissioning PAW(s)',
64 | subText: `Do you want to decommission selected(${pawsToDecommission.length}) PAWs?`,
65 | };
66 | return (
67 |
80 | );
81 | }, [decommissionSelectedPaw, hideDialog, modalProps, pawsToDecommission.length, toggleHideDialog]);
82 |
83 | return (
84 |
85 |
94 | {DecommissionPawDialog}
95 | {isPawDecommissioning && }
96 | {isPawCommissioning && }
97 | {isGettingPaws && }
98 |
99 | );
100 | };
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/microsoft/Privileged-Security-Management/actions/workflows/codeql-analysis.yml) [](https://github.com/microsoft/Privileged-Security-Management/actions/workflows/UnitTest-Server.js.yml) [](https://github.com/microsoft/Privileged-Security-Management/actions/workflows/UnitTest-UI.js.yml) [](https://bestpractices.coreinfrastructure.org/projects/5021) [](https://github.com/microsoft/Privileged-Security-Management/blob/main/LICENSE)
2 |
3 | # Introduction
4 | Privileged Access Workstation ([PAW](https://aka.ms/paw)) and Securing Privileged Access ([SPA](https://aka.ms/spa)) may be the gold standard of administrative security, but the complexity of architecture and associated price point deter most administrators from implementing this in their environments. To lower the barrier of implementation, this application automates processes to reduce human error and simplify the required security expertise to deploy and manage PAWs and [SPA](https://aka.ms/spa) architectures, specifically from deployment to lifecycle management (on-board/decommission) in addition to SILO management.
5 |
6 | This application is designed to operate with a managed identity but supports multiple authentication methods to access the Microsoft Graph API. The Graph API is used to manage the various aspects of the tenant, from the Conditional Access to the Device Configurations in Endpoint Manager.
7 |
8 | # Deployment Guide
9 | The App can be deployed in a variety of ways to support your diverse hosting environment.
10 | Check out our deployment guides here:
11 | - [Azure](https://github.com/microsoft/Privileged-Security-Management/wiki/Deploy-to-Azure)
12 | - [Container](https://github.com/microsoft/Privileged-Security-Management/wiki/Deploy-to-Container)
13 | - [Linux](https://github.com/microsoft/Privileged-Security-Management/wiki/Deploy-to-Linux)
14 | - [Windows](https://github.com/microsoft/Privileged-Security-Management/wiki/Deploy-to-Windows)
15 | - [Deploy/Run from Source Code](https://github.com/microsoft/Privileged-Security-Management/wiki/Build-and-Run-from-Source)
16 |
17 | # Documentation
18 | The application's docs can be found in the GitHub wiki!
19 | https://github.com/microsoft/Privileged-Security-Management/wiki
20 |
21 | # Roadmap
22 | This is also found on the wiki!
23 | https://github.com/microsoft/Privileged-Security-Management/wiki/Version-Roadmap
24 |
25 | ## Contributing
26 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
27 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
28 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
29 |
30 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
31 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
32 | provided by the bot. You will only need to do this once across all repos using our CLA.
33 |
34 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
35 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
36 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
37 |
38 | ## Trademarks
39 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
40 | trademarks or logos is subject to and must follow
41 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
42 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
43 | Any use of third-party trademarks or logos are subject to those third-party's policies.
44 |
--------------------------------------------------------------------------------
/User-Interface/src/store/slice/pawAssignment.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
5 | import type { PayloadAction } from "@reduxjs/toolkit";
6 | import type { User } from "@microsoft/microsoft-graph-types-beta";
7 |
8 | // Define the structure of the initial state and its metadata
9 | export interface PawAssignmentState {
10 | isGettingAssignmentList: boolean,
11 | isAssigning: boolean,
12 | isRemovingAssignment: boolean,
13 | assignedUserList: User[],
14 | addUpnList: User[],
15 | removeUpnList: string[],
16 | message: string | undefined,
17 | error: any | undefined
18 | }
19 |
20 | // Define the initial state of the assignment store
21 | const initialState: PawAssignmentState = {
22 | isGettingAssignmentList: false,
23 | isAssigning: false,
24 | isRemovingAssignment: false,
25 | assignedUserList: [],
26 | addUpnList: [],
27 | removeUpnList: [],
28 | message: undefined,
29 | error: undefined
30 | };
31 |
32 | // Define the thunk for the PAW Assignment List action
33 | export const getPawAssignmentListThunk = createAsyncThunk(
34 | 'pawAssignment/getAssignmentList',
35 | async (pawId, thunkAPI): Promise => {
36 | // Catch errors during execution
37 | try {
38 | // Do something here
39 | const response = await fetch(`${document.location.origin}/API/Lifecycle/PAW/${pawId}/Assign`);
40 |
41 | // Parse the response body into JSON (the API always returns an array, an empty one if no PAWs are commissioned)
42 | const result: User[] = await response.json();
43 |
44 | // Return the results
45 | return result
46 | } catch (error) { // if an error was thrown
47 | // Reject the thunk and return the execution of the
48 | thunkAPI.rejectWithValue(error);
49 | }
50 | }
51 | );
52 |
53 | // Create the slice and export it to the caller
54 | export const pawAssignmentSlice = createSlice({
55 | // Name the slice internally in redux
56 | name: "pawAssignment",
57 |
58 | // Define the initial state of the slice
59 | initialState,
60 |
61 | // Define the reducers for the slice
62 | reducers: {},
63 | extraReducers: (builder) => {
64 | // Operate on the reducer builder parameter
65 | builder
66 | // Create a thunk reducer to set the state when a request starts
67 | .addCase(getPawAssignmentListThunk.pending, (state) => {
68 | // Set the state to indicate that a request is in progress
69 | state.isGettingAssignmentList = true;
70 |
71 | // Set the status message for the state
72 | state.message = "Getting User Assignments...";
73 | })
74 | // Create a thunk reducer to set the state when a request finishes successfully
75 | .addCase(getPawAssignmentListThunk.fulfilled, (state, action: PayloadAction) => {
76 | /// Set the state to indicate that a request is not in progress
77 | state.isGettingAssignmentList = false;
78 |
79 | // Clear the status message
80 | state.message = undefined;
81 |
82 | // Populate the PAW's assigned user list state with the results of the action
83 | state.assignedUserList = action.payload;
84 |
85 | // Because the operation was successful, clear the error state
86 | state.error = undefined;
87 | })
88 | // Create a thunk reducer to set the state when a request fails
89 | .addCase(getPawAssignmentListThunk.rejected, (state, action: PayloadAction) => {
90 | // Set the state to indicate that a request is not in progress
91 | state.isGettingAssignmentList = false;
92 |
93 | // Set the state to contain the results of the error
94 | state.error = action.payload;
95 |
96 | // Clear the current status message
97 | state.message = undefined;
98 | })
99 | }
100 | });
101 |
102 | // Export the actions of the slice
103 | // export const { } = pawAssignmentSlice.actions;
104 |
105 | // Export the reducers by default
106 | export default pawAssignmentSlice.reducer;
--------------------------------------------------------------------------------
/User-Interface/src/store/actions/pawActions/assignmentPawActions.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import {
5 | GETTING_PAW_ASSIGNMENT_REQUEST,
6 | GETTING_PAW_ASSIGNMENT_SUCCESS,
7 | GETTING_PAW_ASSIGNMENT_FAILURE,
8 | ASSIGN_PAW_REQUEST,
9 | ASSIGN_PAW_SUCCESS,
10 | ASSIGN_PAW_FAILURE,
11 | UNASSIGN_PAW_REQUEST,
12 | UNASSIGN_PAW_SUCCESS,
13 | UNASSIGN_PAW_FAILURE
14 | } from "./types";
15 | import { PawService } from "../../../services";
16 | import type { IPsmDevice } from "../../../models"
17 | import type { User } from "@microsoft/microsoft-graph-types-beta";
18 |
19 | // Define the redux store action for the start of the request
20 | function getAssignedUserRequest() {
21 | return {
22 | type: GETTING_PAW_ASSIGNMENT_REQUEST,
23 | };
24 | };
25 |
26 | // Define the redux store action for the successful completion of the request
27 | function getAssignedUserSuccess(userList: User[]) {
28 | return {
29 | type: GETTING_PAW_ASSIGNMENT_SUCCESS,
30 | payload: userList
31 | };
32 | };
33 |
34 | // Define the redux store action for the unsuccessful completion of the request
35 | function getAssignedUserFailure(error: Error) {
36 | // Return the redux action object
37 | return {
38 | type: GETTING_PAW_ASSIGNMENT_FAILURE,
39 | payload: error
40 | };
41 | };
42 |
43 | // Execute the retrieval command async while updating the UI's state in redux when the command resolves
44 | export function getPawAssignedUserList(pawDevice: IPsmDevice) {
45 | // Return a thunk object on execution
46 | return async function(dispatch): Promise {
47 | // Set the UI state to be in the retrieval mode
48 | dispatch(getAssignedUserRequest());
49 | // Execute the get assignment command
50 | PawService.getPawAssignment(pawDevice)
51 | // After successful execution, update the UI's state with the new data
52 | .then(userList => dispatch(getAssignedUserSuccess(userList)))
53 | // After unsuccessful execution, update the UI's state with the error details
54 | .catch(error => dispatch(getAssignedUserFailure(error)))
55 | };
56 | };
57 |
58 | // Define the redux store action for the start of the request
59 | function setAssignedUserRequest() {
60 | return {
61 | type: ASSIGN_PAW_REQUEST,
62 | };
63 | };
64 |
65 | // Define the redux store action for the successful completion of the request
66 | function setAssignedUserSuccess(userList: User[]) {
67 | return {
68 | type: ASSIGN_PAW_SUCCESS,
69 | payload: userList
70 | };
71 | };
72 |
73 | // Define the redux store action for the unsuccessful completion of the request
74 | function setAssignedUserFailure(error: Error) {
75 | // Return the redux action object
76 | return {
77 | type: ASSIGN_PAW_FAILURE,
78 | payload: error
79 | };
80 | };
81 |
82 | // Execute the add assignment command async while updating the UI's state in redux when the command resolves
83 | export function setPawAssignedUserList(pawDevice: IPsmDevice, upnList: string[]) {
84 | // Return a thunk object on execution
85 | return async function(dispatch): Promise {
86 | // Set the UI state to be in the addition mode
87 | dispatch(setAssignedUserRequest());
88 | // Execute the add assignment command
89 | PawService.setPawAssignment(pawDevice, upnList)
90 | // After successful execution, update the UI's state with the new data
91 | .then(userList => dispatch(setAssignedUserSuccess(userList)))
92 | // After unsuccessful execution, update the UI's state with the error details
93 | .catch(error => dispatch(setAssignedUserFailure(error)))
94 | };
95 | };
96 |
97 | // Define the redux store action for the start of the request
98 | function removeAssignedUserRequest() {
99 | return {
100 | type: UNASSIGN_PAW_REQUEST,
101 | };
102 | };
103 |
104 | // Define the redux store action for the successful completion of the request
105 | function removeAssignedUserSuccess(userList: User[]) {
106 | return {
107 | type: UNASSIGN_PAW_SUCCESS,
108 | payload: userList
109 | };
110 | };
111 |
112 | // Define the redux store action for the unsuccessful completion of the request
113 | function removeAssignedUserFailure(error: Error) {
114 | // Return the redux action object
115 | return {
116 | type: UNASSIGN_PAW_FAILURE,
117 | payload: error
118 | };
119 | };
120 |
121 | // Execute the un-assignment command async while updating the UI's state in redux when the command resolves
122 | export function removePawAssignedUserList(pawDevice: IPsmDevice, upnList: string[]) {
123 | // Return a thunk object on execution
124 | return async function(dispatch): Promise {
125 | // Set the UI state to be in the removal mode
126 | dispatch(removeAssignedUserRequest());
127 | // Execute the remove assignment command
128 | PawService.removePawAssignment(pawDevice, upnList)
129 | // After successful execution, update the UI's state with the new data
130 | .then(userList => dispatch(removeAssignedUserSuccess(userList)))
131 | // After unsuccessful execution, update the UI's state with the error details
132 | .catch(error => dispatch(removeAssignedUserFailure(error)))
133 | };
134 | };
--------------------------------------------------------------------------------
/.github/workflows/Build-Binaries.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Build Binaries
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Allows you to run this workflow manually from the Actions tab
8 | workflow_dispatch:
9 |
10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
11 | jobs:
12 | Generate-Bins:
13 | # The type of runner that the job will run on
14 | runs-on: ubuntu-latest
15 |
16 | # Steps represent a sequence of tasks that will be executed as part of the job
17 | steps:
18 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
19 | - uses: actions/checkout@v2
20 |
21 | # Set up NodeJS on the build host
22 | - name: Setup Node.js environment
23 | uses: actions/setup-node@v2
24 | with:
25 | node-version: 16.x
26 |
27 | # Install the PKG package to allow for building binaries
28 | - name: Install PKG
29 | run: npm install -g pkg
30 |
31 | # Install the UI's dependencies
32 | - name: Install the UI project
33 | run: npm install
34 | working-directory: User-Interface
35 |
36 | # Install the servers dependencies
37 | - name: Install the Server project
38 | run: npm install
39 | working-directory: Server
40 |
41 | # Compile the Typescript files to JS
42 | - name: Build Server
43 | run: npm run-script build
44 | working-directory: Server
45 |
46 | # Build the UI static files and place them into the server's compiled folder.
47 | - name: Build the UI
48 | run: npm run-script build
49 | working-directory: User-Interface
50 |
51 | # Build the native binaries to be included as artifacts for publish
52 | - name: Build native binaries
53 | run: npx pkg .
54 | working-directory: Server
55 |
56 | # Upload the Windows EXE Arctifact to make them available to other processes
57 | - name: Upload Raw Windows EXE Artifact
58 | uses: actions/upload-artifact@v2
59 | with:
60 | name: WindowsRawExe
61 | path: Server/dist/privileged-security-management-server-win.exe
62 |
63 | # Upload the Windows EXE Arctifact to make them available to other processes
64 | - name: Upload Raw Linux bin Artifact
65 | uses: actions/upload-artifact@v2
66 | with:
67 | name: LinuxRawBin
68 | path: Server/dist/privileged-security-management-server-linux
69 | # Build-And-Publish-Snap:
70 | # # Ensure that the binaries are generated before executing this part of the build process.
71 | # needs: Generate-Bins
72 |
73 | # # Run the snap build command on the latest Ubuntu OS available
74 | # runs-on: ubuntu-latest
75 |
76 | # # Steps represent a sequence of tasks that will be executed as part of the job
77 | # steps:
78 | # # Download the project's current source code
79 | # - name: Download project
80 | # uses: actions/checkout@v2
81 |
82 | # # Grab the bin file that was just generated by the BIN Build step
83 | # - name: Download Linux Bin
84 | # uses: actions/download-artifact@v3
85 | # with:
86 | # name: LinuxRawBin
87 | # path: Server/dist/
88 |
89 | # # Package the snap packaged based on the snapcraft.yaml file in the root of the source code repo
90 | # - name: Build Snap Package
91 | # uses: snapcore/action-build@v1
92 |
93 | # # Upload the built snap package to the store in edge mode
94 | # - uses: snapcore/action-publish@v1
95 | # with:
96 | # store_login: ${{ secrets.SNAP_STORE_EDGE_LOGIN }}
97 | # snap: ${{ steps.build.outputs.snap }}
98 | # release: edge
99 |
100 | # Authenticode-Sign:
101 | # # This job can't procede if the EXE isn't present to sign
102 | # needs: Generate-Bins
103 |
104 | # # The type of runner that the job will run on
105 | # runs-on: windows-latest
106 |
107 | # # Steps represent a sequence of tasks that will be executed as part of the job
108 | # steps:
109 | # - name: Download the Windows Raw un-packaged exe Artifact
110 | # uses: actions/download-artifact@v2
111 | # with:
112 | # # Artifact name
113 | # name: WindowsRawExe
114 | # # Destination path
115 | # path: Downloads
116 |
117 | # # Runs a single command using the runners shell
118 | # - name: Set up .Net CLI
119 | # uses: actions/setup-dotnet@v1
120 | # with:
121 | # dotnet-version: "6.0.x"
122 |
123 | # # Install the Azure Sign Tool for authenticode signing
124 | # - name: Install Azure Sign Tool
125 | # run: dotnet tool install --global AzureSignTool
126 |
127 | Package-WindowsInstallers:
128 | # This job can't procede if the EXE hasn't been generated or signed
129 | needs: [Generate-Bins]
130 |
131 | # The type of runner that the job will run on
132 | runs-on: windows-latest
133 |
134 | # Steps represent a sequence of tasks that will be executed as part of the job
135 | steps:
136 | - name: Download project
137 | uses: actions/checkout@v2
138 |
139 | - name: Download EXE
140 | uses: actions/download-artifact@v3
141 | with:
142 | name: WindowsRawExe
143 | path: Server/dist/
144 |
145 | - name: Build MSI Installer
146 | uses: Caphyon/advinst-github-action@v1.0
147 | with:
148 | advinst-version: '19.6'
149 | advinst-license: ${{ secrets.ADVANCEDINSTALLER_KEY }}
150 | advinst-enable-automation: 'false'
151 | aip-path: 'Privileged Security Management.aip'
152 | aip-build-name: 'MSI'
153 |
154 | # Upload the Windows MSI Arctifact to make them available to other processes
155 | - name: Upload MSI Artifact
156 | uses: actions/upload-artifact@v2
157 | with:
158 | name: WindowsInstallerMSI
159 | path: Server/dist/PSM-Win-x64.msi
160 |
161 | - name: Build EXE Installer
162 | uses: Caphyon/advinst-github-action@v1.0
163 | with:
164 | advinst-version: '19.6'
165 | advinst-license: ${{ secrets.ADVANCEDINSTALLER_KEY }}
166 | advinst-enable-automation: 'false'
167 | aip-path: 'Privileged Security Management.aip'
168 | aip-build-name: 'EXE'
169 |
170 | # Upload the Windows EXE Arctifact to make them available to other processes
171 | - name: Upload EXE Artifact
172 | uses: actions/upload-artifact@v2
173 | with:
174 | name: WindowsInstallerEXE
175 | path: Server/dist/PSM-Win-x64.exe
176 |
--------------------------------------------------------------------------------
/User-Interface/src/services/pawService.ts:
--------------------------------------------------------------------------------
1 | import type { User } from "@microsoft/microsoft-graph-types-beta";
2 | import type { IPsmAutopilotDevice, IPsmDevice } from "../models";
3 | // import { PsmDeviceList } from '../models/mocks';
4 |
5 | export interface IPawService {
6 | getPaws: () => Promise,
7 | commissionPaw: (paw: IPsmAutopilotDevice) => Promise,
8 | decommissionPaw: (paws: IPsmDevice[]) => Promise, // return list of decommissioned Paws
9 | }
10 | export class PawService {
11 | // Get the current host name and port (if there is a port)
12 | public static API_BASE_URL = document.location.origin;
13 |
14 | // Get a list of PAW devices from the Server's Lifecycle API
15 | public static async getPaws(): Promise {
16 | // Build the URL to run the API request against
17 | const getPawsUrl = `${this.API_BASE_URL}/API/Lifecycle/PAW`;
18 |
19 | // Run the Get request against the specified endpoint
20 | const response = await fetch(getPawsUrl);
21 |
22 | // Parse the response body into JSON (the API always returns an array, an empty one if no PAWs are commissioned)
23 | const result: Array = await response.json();
24 |
25 | // rename the object's keys to match what is used throughout the rest of the app
26 | return result.map((device: IPsmDevice) => {
27 | // For each object in the array, replace it's contents with the below object structure
28 | return {
29 | DisplayName: device.DisplayName,
30 | id: device.id,
31 | Type: device.Type,
32 | CommissionedDate: new Date(device.CommissionedDate).toUTCString(),
33 | ParentDevice: device.ParentDevice,
34 | GroupAssignment: device.GroupAssignment,
35 | UserAssignment: device.UserAssignment,
36 | ParentGroup: device.ParentGroup
37 | };
38 | });
39 | /*
40 | Uncomment the below code to work with the mock, and also uncomment paws mock import
41 | */
42 | // return PsmDeviceList.map((device: IPsmDevice) => {
43 | // return {
44 | // DisplayName: device.DisplayName,
45 | // id: device.id,
46 | // Type: device.Type,
47 | // CommissionedDate: new Date(device.CommissionedDate).toUTCString(),
48 | // ParentDevice: device.ParentDevice,
49 | // GroupAssignment: device.GroupAssignment,
50 | // ParentGroup: device.ParentGroup,
51 | // UserAssignment: device.UserAssignment
52 | // };
53 | // });
54 | };
55 |
56 | // Commission the specified autopilot device by using the lifecycle API
57 | public static commissionPaw = async (deviceList: IPsmAutopilotDevice[], pawTypeToCommission: string) => {
58 | // Loop through each device in the list of autopilot devices
59 | for (const device of deviceList) {
60 | // Build the URL for the specified unique autopilot device
61 | const commissionPawUrl = `${this.API_BASE_URL}/API/Lifecycle/PAW/${device.azureAdDeviceId}/Commission`;
62 |
63 | // Build the post body
64 | const postBody = {
65 | type: pawTypeToCommission,
66 | };
67 |
68 | // make the web request with the required options and the previously built post body
69 | await fetch(commissionPawUrl, {
70 | method: 'POST',
71 | mode: 'same-origin',
72 | headers: {
73 | 'Content-Type': 'application/json'
74 | },
75 | body: JSON.stringify(postBody)
76 | });
77 | };
78 | };
79 |
80 | // Decommission the specified PAW device by using the lifecycle API
81 | public static decommissionPaw = async (deviceList: IPsmDevice[]) => {
82 | // Loop through all of the specified PAWs and decommission them
83 | for (const device of deviceList) {
84 | // Build the web request url dynamically
85 | const commissionPawUrl = `${this.API_BASE_URL}/API/Lifecycle/PAW/${device.id}/Commission`;
86 |
87 | // Make the request to decommission the PAW with the specified options
88 | await fetch(commissionPawUrl, {
89 | method: 'DELETE',
90 | mode: 'same-origin'
91 | });
92 | };
93 | };
94 |
95 | // Get the User Assignments for the specified PAW device
96 | public static async getPawAssignment(psmDevice: IPsmDevice): Promise {
97 | // Build the request url
98 | const getAssignmentURL = `${this.API_BASE_URL}/API/Lifecycle/PAW/${psmDevice.id}/Assign`;
99 |
100 | // Run the Get request against the specified endpoint
101 | const response = await fetch(getAssignmentURL);
102 |
103 | // Parse the response body into JSON (the API always returns an array, an empty one if no users are assigned)
104 | const result: User[] = await response.json();
105 |
106 | // Returned the processed results
107 | return result;
108 | };
109 |
110 | public static async setPawAssignment(psmDevice: IPsmDevice, upnList: string[]): Promise {
111 | // Build the request url
112 | const postAssignmentURL = `${this.API_BASE_URL}/API/Lifecycle/PAW/${psmDevice.id}/Assign`;
113 |
114 | // Build the post body to be used in the web request
115 | const postBody = {
116 | userList: upnList
117 | };
118 |
119 | // Run the Get request against the specified endpoint
120 | const response = await fetch(postAssignmentURL, {
121 | method: "POST",
122 | mode: "same-origin",
123 | headers: {
124 | 'Content-Type': 'application/json'
125 | },
126 | body: JSON.stringify(postBody)
127 | });
128 |
129 | // Parse the response body into JSON (the API always returns an array, an empty one if no users are assigned)
130 | const result: User[] = await response.json();
131 |
132 | // Returned the processed results
133 | return result;
134 | };
135 |
136 | public static async removePawAssignment(psmDevice: IPsmDevice, upnList: string[]): Promise {
137 | // Build the request url
138 | const deleteAssignmentURL = `${this.API_BASE_URL}/API/Lifecycle/PAW/${psmDevice.id}/Assign`;
139 |
140 | // Build the delete body to be used in the web request
141 | const deleteBody = {
142 | userList: upnList
143 | };
144 |
145 | // Run the Get request against the specified endpoint
146 | const response = await fetch(deleteAssignmentURL, {
147 | method: "DELETE",
148 | mode: "same-origin",
149 | headers: {
150 | 'Content-Type': 'application/json'
151 | },
152 | body: JSON.stringify(deleteBody)
153 | });
154 |
155 | // Parse the response body into JSON (the API always returns an array, an empty one if no users are assigned)
156 | const result: User[] = await response.json();
157 |
158 | // Returned the processed results
159 | return result;
160 | };
161 | };
--------------------------------------------------------------------------------
/Server/src/Startup/Authentication.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import { ChainedTokenCredential, ClientSecretCredential, ManagedIdentityCredential } from "@azure/identity";
5 | import type { KeyVaultSecret } from "@azure/keyvault-secrets";
6 | import { SecretClient } from "@azure/keyvault-secrets";
7 | import { validateGUID, validateKeyVaultName, validateKeyVaultSecretName, InternalAppError, writeDebugInfo } from "../Utility";
8 |
9 | // Create the MS Azure Access Credential handler class.
10 | export class MSAzureAccessCredential {
11 | // Define the class properties
12 | credential: Promise;
13 | private clientSecretCred: ClientSecretCredential | undefined;
14 | private managedIdentCred: ManagedIdentityCredential;
15 | private clientID: string
16 | private clientSecret: string | Promise
17 | private tenantID: string
18 | private managedIdentGUID: string
19 | private keyVaultName: string
20 | private kvSecretName: string
21 | private kvCloudSelection: string
22 |
23 | // Initialize the Access Credential class when instantiated
24 | constructor() {
25 | // Set keyvault to operate off teh Azure Public Cloud by default.
26 | this.kvCloudSelection = ".vault.azure.net"
27 |
28 | // Import environmental variables
29 | this.managedIdentGUID = process.env.PSM_Managed_ID_GUID || ""
30 | this.keyVaultName = process.env.PSM_KeyVault_Name || ""
31 | this.kvSecretName = process.env.PSM_KeyVault_Secret || ""
32 | this.clientID = process.env.PSM_Client_GUID || ""
33 | this.clientSecret = process.env.PSM_Client_Secret || ""
34 | this.tenantID = process.env.PSM_Tenant_ID || ""
35 |
36 | // Validate environmental variable input to ensure that the input is as expected and not an injection attempt.
37 | if (this.managedIdentGUID !== "" && !validateGUID(this.managedIdentGUID)) { throw new InternalAppError("The user assigned managed identity GUID is not a valid GUID!", "Invalid Input"); };
38 | if (this.keyVaultName !== "" && !validateKeyVaultName(this.keyVaultName)) { throw new InternalAppError("The key vault name must be a string and following naming constraints!", "Invalid Input", "Authentication - Constructor - Input Validation"); };
39 | if (this.kvSecretName !== "" && !validateKeyVaultSecretName(this.kvSecretName)) { throw new InternalAppError("The key vault secret name must be a string and following naming constraints!", "Invalid Input", "Authentication - Constructor - Input Validation"); };
40 | if (this.clientID !== "" && !validateGUID(this.clientID)) { throw new InternalAppError("The Client ID was specified but it isn't a string in the GUID format!", "Invalid Input", "Authentication - Constructor - Input Validation"); };
41 | if (this.clientSecret !== "" && typeof this.clientSecret !== "string") { throw new InternalAppError("The client secret was specified but isn't a string!", "Invalid Input", "Authentication - Constructor - Input Validation"); };
42 | if (this.tenantID !== "" && !validateGUID(this.tenantID)) { throw new InternalAppError("The Tenant ID was specified but it isn't a string in the GUID format!", "Invalid Input", "Authentication - Constructor - Input Validation"); };
43 |
44 | // Validate if a GUID is provided for a user assigned managed identity
45 | if (this.managedIdentGUID !== "") {
46 | // Write debug info
47 | writeDebugInfo("Initializing UA MI Credential");
48 |
49 | // Initialize the managed identity credential object for user assigned managed identity.
50 | this.managedIdentCred = new ManagedIdentityCredential(this.managedIdentGUID);
51 | } else {
52 | // Write debug info
53 | writeDebugInfo("Initializing SA MI Credential");
54 |
55 | // Initialize the managed identity credential object for system assigned managed identity.
56 | this.managedIdentCred = new ManagedIdentityCredential();
57 | };
58 |
59 | // Write debug info
60 | writeDebugInfo("Managed Identity Credential Initialization Complete");
61 |
62 | // Check if the keyvault name was specified
63 | if (this.keyVaultName !== "" || this.kvSecretName !== "") {
64 | // Validate that all of the properties are in the correct configuration.
65 | if (this.keyVaultName === "" || this.kvSecretName === "" || this.tenantID === "" || this.clientID === "" || this.clientSecret !== "") { throw new InternalAppError("The required configurations aren't present, please double check your MI KV based auth config.", "Invalid Config", "Authentication - MI KV App Reg - Config Validation"); };
66 |
67 | // Build the URL of the key vault
68 | const kvURL = "https://" + this.keyVaultName + this.kvCloudSelection;
69 |
70 | // Instantiate the key vault client
71 | const kvSecretClient = new SecretClient(kvURL, this.managedIdentCred);
72 |
73 | // Start the KV secret retrieval process
74 | this.clientSecret = kvSecretClient.getSecret(this.kvSecretName);
75 |
76 | // Build the chained token credential
77 | this.credential = this.getKvChainedCred();
78 | } else if (this.clientID !== "" || this.clientSecret !== "" || this.tenantID !== "") { // If no KV, then check for App Reg Auth
79 | // Validate that all of the properties are in the correct configuration.
80 | if (this.tenantID === "" || this.clientID === "" || this.clientSecret === "") { throw new InternalAppError("The required configurations aren't present, please double check your app reg based auth config.", "Invalid Config", "Authentication - App Reg/Local Vars - Config Validation"); };
81 |
82 | // Build a client secret credential
83 | this.clientSecretCred = new ClientSecretCredential(this.tenantID, this.clientID, this.clientSecret);
84 |
85 | // Build the chained token credential
86 | this.credential = Promise.resolve(new ChainedTokenCredential(this.clientSecretCred));
87 | } else { // Just Managed identity auth here
88 | // Build the chained token credential
89 | this.credential = Promise.resolve(new ChainedTokenCredential(this.managedIdentCred));
90 | };
91 | };
92 |
93 | // Define an asynchronous function that chains together a credential built from data in the key vault and managed identity.
94 | private async getKvChainedCred() {
95 | // Validate the client secret is defined correctly.
96 | if (typeof this.clientSecret === "string") { throw new InternalAppError("The client secret should not be manually set for key vault based auth!", "Invalid Input", "Authentication - getKvChainedCred - Input Validation"); };
97 | if (typeof this.clientSecret.then !== "function" || typeof this.clientSecret.catch !== "function") { throw new InternalAppError("The client secret should be Promise, the specified object is not a promise!", "Invalid Input", "Authentication - getKvChainedCred - Input Validation"); };
98 |
99 | // Isolate the value from the Key Vault secret
100 | const kvSecretValue = (await this.clientSecret).value
101 |
102 | // Validate that it contains data, if not, throw an error
103 | if (kvSecretValue === undefined) { throw new InternalAppError("KV secret value is undefined", "Invalid Input", "Authentication - getKvChainedCred - Secret value validation") };
104 |
105 | // Create the client secret object and place it into the instantiated class' properties
106 | this.clientSecretCred = new ClientSecretCredential(this.tenantID, this.clientID, kvSecretValue);
107 |
108 | // Return a chained credential
109 | return new ChainedTokenCredential(this.clientSecretCred);
110 | };
111 | };
--------------------------------------------------------------------------------
/Server/disableeslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true,
4 | "node": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | ],
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": [
16 | "@typescript-eslint"
17 | ],
18 | "rules": {
19 | "accessor-pairs": "error",
20 | "array-bracket-newline": "error",
21 | "array-bracket-spacing": "error",
22 | "array-callback-return": "error",
23 | "array-element-newline": "error",
24 | "arrow-body-style": "error",
25 | "arrow-parens": "error",
26 | "arrow-spacing": "error",
27 | "block-scoped-var": "error",
28 | "block-spacing": "error",
29 | "brace-style": "error",
30 | "camelcase": "error",
31 | "capitalized-comments": "error",
32 | "class-methods-use-this": "error",
33 | "comma-dangle": "error",
34 | "comma-spacing": "error",
35 | "comma-style": "error",
36 | "complexity": "error",
37 | "computed-property-spacing": "error",
38 | "consistent-return": "error",
39 | "consistent-this": "error",
40 | "curly": "error",
41 | "default-case": "error",
42 | "default-case-last": "error",
43 | "default-param-last": "error",
44 | "dot-location": "error",
45 | "dot-notation": "error",
46 | "eol-last": "error",
47 | "eqeqeq": "error",
48 | "func-call-spacing": "error",
49 | "func-name-matching": "error",
50 | "func-names": "error",
51 | "func-style": "error",
52 | "function-call-argument-newline": "error",
53 | "function-paren-newline": "error",
54 | "generator-star-spacing": "error",
55 | "grouped-accessor-pairs": "error",
56 | "guard-for-in": "error",
57 | "id-denylist": "error",
58 | "id-length": "error",
59 | "id-match": "error",
60 | "implicit-arrow-linebreak": "error",
61 | "indent": "error",
62 | "init-declarations": "error",
63 | "jsx-quotes": "error",
64 | "key-spacing": "error",
65 | "keyword-spacing": "error",
66 | "line-comment-position": "error",
67 | "lines-around-comment": "error",
68 | "lines-between-class-members": "error",
69 | "max-classes-per-file": "error",
70 | "max-depth": "error",
71 | "max-lines": "error",
72 | "max-lines-per-function": "error",
73 | "max-nested-callbacks": "error",
74 | "max-params": "error",
75 | "max-statements": "error",
76 | "max-statements-per-line": "error",
77 | "multiline-comment-style": "error",
78 | "multiline-ternary": "error",
79 | "new-cap": "error",
80 | "new-parens": "error",
81 | "newline-per-chained-call": "error",
82 | "no-alert": "error",
83 | "no-array-constructor": "error",
84 | "no-await-in-loop": "error",
85 | "no-bitwise": "error",
86 | "no-caller": "error",
87 | "no-confusing-arrow": "error",
88 | "no-console": "error",
89 | "no-constructor-return": "error",
90 | "no-continue": "error",
91 | "no-div-regex": "error",
92 | "no-duplicate-imports": "error",
93 | "no-else-return": "error",
94 | "no-empty-function": "error",
95 | "no-eq-null": "error",
96 | "no-eval": "error",
97 | "no-extend-native": "error",
98 | "no-extra-bind": "error",
99 | "no-extra-label": "error",
100 | "no-extra-parens": "error",
101 | "no-extra-semi": "off",
102 | "@typescript-eslint/no-extra-semi": ["error"],
103 | "no-floating-decimal": "error",
104 | "no-implicit-coercion": "error",
105 | "no-implicit-globals": "error",
106 | "no-implied-eval": "error",
107 | "no-inline-comments": "error",
108 | "no-invalid-this": "error",
109 | "no-iterator": "error",
110 | "no-label-var": "error",
111 | "no-labels": "error",
112 | "no-lone-blocks": "error",
113 | "no-lonely-if": "error",
114 | "no-loop-func": "error",
115 | "no-magic-numbers": "error",
116 | "no-mixed-operators": "error",
117 | "no-multi-assign": "error",
118 | "no-multi-spaces": "error",
119 | "no-multi-str": "error",
120 | "no-multiple-empty-lines": "error",
121 | "no-negated-condition": "error",
122 | "no-nested-ternary": "error",
123 | "no-new": "error",
124 | "no-new-func": "error",
125 | "no-new-object": "error",
126 | "no-new-wrappers": "error",
127 | "no-octal-escape": "error",
128 | "no-param-reassign": "error",
129 | "no-plusplus": "error",
130 | "no-promise-executor-return": "error",
131 | "no-proto": "error",
132 | "no-restricted-exports": "error",
133 | "no-restricted-globals": "error",
134 | "no-restricted-imports": "error",
135 | "no-restricted-properties": "error",
136 | "no-restricted-syntax": "error",
137 | "no-return-assign": "error",
138 | "no-return-await": "error",
139 | "no-script-url": "error",
140 | "no-self-compare": "error",
141 | "no-sequences": "error",
142 | "no-shadow": "error",
143 | "no-tabs": "error",
144 | "no-template-curly-in-string": "error",
145 | "no-ternary": "error",
146 | "no-throw-literal": "error",
147 | "no-trailing-spaces": "error",
148 | "no-undef-init": "error",
149 | "no-undefined": "error",
150 | "no-underscore-dangle": "error",
151 | "no-unmodified-loop-condition": "error",
152 | "no-unneeded-ternary": "error",
153 | "no-unreachable-loop": "error",
154 | "no-unused-expressions": "error",
155 | "no-use-before-define": "error",
156 | "no-useless-call": "error",
157 | "no-useless-computed-key": "error",
158 | "no-useless-concat": "error",
159 | "no-useless-constructor": "error",
160 | "no-useless-rename": "error",
161 | "no-useless-return": "error",
162 | "no-var": "error",
163 | "no-void": "error",
164 | "no-warning-comments": "error",
165 | "no-whitespace-before-property": "error",
166 | "nonblock-statement-body-position": "error",
167 | "object-curly-newline": "error",
168 | "object-curly-spacing": ["error", "always"],
169 | "object-property-newline": "error",
170 | "object-shorthand": "error",
171 | "one-var": "error",
172 | "one-var-declaration-per-line": "error",
173 | "operator-assignment": "error",
174 | "operator-linebreak": "error",
175 | "padded-blocks": "error",
176 | "padding-line-between-statements": "error",
177 | "prefer-arrow-callback": "error",
178 | "prefer-const": "error",
179 | "prefer-destructuring": "error",
180 | "prefer-exponentiation-operator": "error",
181 | "prefer-named-capture-group": "error",
182 | "prefer-numeric-literals": "error",
183 | "prefer-object-spread": "error",
184 | "prefer-promise-reject-errors": "error",
185 | "prefer-regex-literals": "error",
186 | "prefer-rest-params": "error",
187 | "prefer-spread": "error",
188 | "prefer-template": "error",
189 | "quote-props": "error",
190 | "quotes": "error",
191 | "radix": "error",
192 | "require-atomic-updates": "error",
193 | "require-await": "error",
194 | "require-unicode-regexp": "error",
195 | "rest-spread-spacing": "error",
196 | "semi": "error",
197 | "semi-spacing": "error",
198 | "semi-style": "error",
199 | "sort-imports": "error",
200 | "sort-keys": "error",
201 | "sort-vars": "error",
202 | "space-before-blocks": "error",
203 | "space-before-function-paren": ["error", "never"],
204 | "space-in-parens": "error",
205 | "space-infix-ops": "error",
206 | "space-unary-ops": "error",
207 | "spaced-comment": "error",
208 | "strict": "error",
209 | "switch-colon-spacing": "error",
210 | "symbol-description": "error",
211 | "template-curly-spacing": "error",
212 | "template-tag-spacing": "error",
213 | "unicode-bom": "error",
214 | "vars-on-top": "error",
215 | "wrap-iife": "error",
216 | "wrap-regex": "error",
217 | "yield-star-spacing": "error",
218 | "yoda": "error"
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/Server/src/Utility/RequestGenerator.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import type * as MicrosoftGraphBeta from "@microsoft/microsoft-graph-types-beta";
5 | import { InternalAppError, validateEmailArray, validateGUID, validateGUIDArray, validateStringArray } from "./";
6 |
7 | // Generate a settings object for the user rights assignment of a PAW.
8 | // Allows multiple users for potential shared PAW concept in the future.
9 | export function endpointPAWUserRightsSettings(userList: string[]) {
10 | // Validate input is a populated array of strings
11 | if (!(userList instanceof Array)) { throw new InternalAppError("The specified UserList is not an array!") };
12 | if (!validateStringArray(userList)) { throw new InternalAppError("The user list is not an array of strings!") };
13 |
14 | // Define object structures
15 | interface SettingsValueCollection {
16 | "@odata.type": string,
17 | settingInstance: {
18 | "@odata.type": string;
19 | settingDefinitionId: string;
20 | simpleSettingCollectionValue?: SettingsValueObject[]
21 | };
22 | };
23 |
24 | interface SettingsValueObject {
25 | "@odata.type": string;
26 | value: string;
27 | };
28 |
29 | // Build the initial settings object structure
30 | let settingsObject: SettingsValueCollection[] = [
31 | {
32 | "@odata.type": "#microsoft.graph.deviceManagementConfigurationSetting",
33 | "settingInstance": {
34 | "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance",
35 | "settingDefinitionId": "device_vendor_msft_policy_config_userrights_allowlocallogon",
36 | "simpleSettingCollectionValue": []
37 | }
38 | }
39 | ];
40 |
41 | // Loop through all of the usernames provided in the parameter
42 | for (let index = 0; index < userList.length; index++) {
43 | // Extract the username from the array
44 | const userName = userList[index];
45 |
46 | // Build the settings value with the username to be added to the settings object
47 | const computedValue: SettingsValueObject = {
48 | "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue",
49 | "value": userName
50 | };
51 |
52 | // Add the value object to the settings object value collection
53 | settingsObject[0].settingInstance.simpleSettingCollectionValue?.push(computedValue);
54 | };
55 |
56 | // Return the computed object to the caller
57 | return settingsObject;
58 | };
59 |
60 | // Generate an assignment object for Microsoft Endpoint Manager (MEM)
61 | export function endpointGroupAssignmentTarget(includeGUID?: string[], excludeGUID?: string[]) {
62 | // Validate inputs
63 | if (!(includeGUID instanceof Array) || !validateGUIDArray(includeGUID)) { throw new InternalAppError("The specified array of included group GUIDs is not valid!") };
64 | if (!(excludeGUID instanceof Array) || !validateGUIDArray(excludeGUID)) { throw new InternalAppError("The specified array of excluded group GUIDs is not valid!") };
65 |
66 | // Define the assignment structure object type interface
67 | interface AssignmentStructure {
68 | assignments: {
69 | target: {
70 | "@odata.type": string;
71 | groupId: string;
72 | };
73 | }[];
74 | }
75 |
76 | // Create an empty assignment(s) object
77 | const assignmentObject: AssignmentStructure = {
78 | "assignments": []
79 | }
80 |
81 | // If groups are included, add them to the assignment object
82 | if (typeof includeGUID !== "undefined") {
83 | // Loop over each of the included GUIDs
84 | for (let index = 0; index < includeGUID.length; index++) {
85 | // Extract one of the GUIDs
86 | const groupGUID = includeGUID[index];
87 |
88 | // Build the target object with the specified GUID
89 | const target = {
90 | "target": {
91 | "@odata.type": "#microsoft.graph.groupAssignmentTarget",
92 | "groupId": groupGUID
93 | }
94 | }
95 |
96 | // Add the target object to the assignment structure
97 | assignmentObject.assignments.push(target);
98 | }
99 | }
100 |
101 | // If group exclusions are specified, add them to the assignment object
102 | if (typeof excludeGUID !== "undefined") {
103 | // Loop over each of the excluded GUIDs
104 | for (let index = 0; index < excludeGUID.length; index++) {
105 | // Extract one of the GUIDs
106 | const groupGUID = excludeGUID[index];
107 |
108 | // Build the target object with the specified GUID
109 | const target = {
110 | "target": {
111 | "@odata.type": "#microsoft.graph.exclusionGroupAssignmentTarget",
112 | "groupId": groupGUID
113 | }
114 | }
115 |
116 | // Add the target object to the assignment structure
117 | assignmentObject.assignments.push(target);
118 | }
119 | }
120 |
121 | // Return the built assignment object
122 | return assignmentObject;
123 | };
124 |
125 | // Generate the object for conditional access policy to assign a specific user to a device
126 | export function conditionalAccessPAWUserAssignment(deviceID: string, deviceGroupGUID: string, userGroupListGUID: string[], breakGlassGroupGUID: string): MicrosoftGraphBeta.ConditionalAccessPolicy {
127 | // Validate input
128 | if (!validateGUID(deviceID) || typeof deviceID !== "string") { throw new InternalAppError("The Device ID specified is not a valid GUID!") };
129 | if (!validateGUID(deviceGroupGUID) || typeof deviceGroupGUID !== "string") { throw new InternalAppError("The device group is not a valid GUID!") };
130 | if (!validateGUIDArray(userGroupListGUID)) { throw new InternalAppError("The user group list array is not an array of GUID(s)!") };
131 | if (!validateGUID(breakGlassGroupGUID) || typeof breakGlassGroupGUID !== "string") { throw new InternalAppError("The Break Glass Group GUID specified is not a valid GUID!") };
132 |
133 | // Create the base object to return later
134 | let policyUserAssignment: MicrosoftGraphBeta.ConditionalAccessPolicy = {
135 | "conditions": {
136 | "users": {
137 | "includeGroups": [deviceGroupGUID],
138 | "excludeGroups": [breakGlassGroupGUID]
139 | },
140 | "applications": {
141 | "includeApplications": ["All"]
142 | },
143 | "clientAppTypes": ["all"],
144 | "devices": {
145 | "deviceFilter": {
146 | "mode": "exclude",
147 | "rule": "device.deviceId -in [\"" + deviceID + "\"]"
148 | }
149 | }
150 | },
151 | "grantControls": {
152 | "operator": "AND",
153 | "builtInControls": ["block"]
154 | }
155 | }
156 |
157 | // Silence error checker in TS. This check should not be necessary.
158 | if (typeof policyUserAssignment.conditions?.users?.includeGroups === "undefined") { throw new InternalAppError("If you get this error, I don't know how this happened. File a bug report with Node.JS. (CA PAW assignment)") };
159 |
160 | // Add the user group list GUID to the included groups in the policy assignment object
161 | policyUserAssignment.conditions.users.includeGroups.push.apply(policyUserAssignment.conditions.users.includeGroups, userGroupListGUID);
162 |
163 | // Return the computed results
164 | return policyUserAssignment;
165 | };
166 |
167 | // Generate the OMA Setting for MS Hyper-V, and local admin rights assignments. Assigned users are allowed to be Hyper-V admins but not local even if they are global admins.
168 | export function localGroupMembershipUserRights(upnList?: string[]) {
169 | // Validate Input
170 | if (typeof upnList !== "undefined" && !validateEmailArray(upnList)) { throw new InternalAppError("upnList is not a valid list of user principal names!", "Invalid Input", "RequestGenerator - localGroupMembershipUserRights - Input Validation") };
171 |
172 | // Build the initial XML configuration
173 | const settingStart = "";
174 | let settingMiddle = "";
175 | const settingEnd = "";
176 |
177 | // If the UPN List is specified, add the users to the list
178 | if (typeof upnList !== "undefined") {
179 | // Loop through all of the users in the user list
180 | for (const user of upnList) {
181 | // Add a user line to grant that user hyper-v admin rights
182 | settingMiddle += "";
183 | };
184 | };
185 | // If the UPN List is un-specified, don't add any users to the list.
186 | // This will force no users in that group, effectively eliminating control of hyper-v.
187 |
188 | // Build the settings object to return
189 | const settingsBody = {
190 | "@odata.type": "#microsoft.graph.omaSettingString",
191 | "displayName": "Admin Groups Config",
192 | "description": "Configures the Administrators and Hyper-V Admins groups",
193 | "omaUri": "./Device/Vendor/MSFT/Policy/Config/LocalUsersAndGroups/Configure",
194 | "value": settingStart + settingMiddle + settingEnd
195 | };
196 |
197 | // Return the compiled local groups permissions assignment XML string
198 | return settingsBody;
199 | };
200 |
201 | // TODO: Generate device configuration profiles for the MEM device config CRUD operations
202 | // Generate a Windows 10 device restriction post body for MEM
203 | export function win10DevRestriction() { }
--------------------------------------------------------------------------------
/Server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "react", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "commonjs", /* Specify what module code is generated. */
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
48 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
50 | "outDir": "./bin", /* Specify an output folder for all emitted files. */
51 | "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
67 |
68 | /* Interop Constraints */
69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
74 |
75 | /* Type Checking */
76 | "strict": true, /* Enable all strict type-checking options. */
77 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
78 | "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
79 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
80 | "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
81 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
82 | "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
84 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
95 |
96 | /* Completeness */
97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Server/test/UtilityTest.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import "mocha";
5 | import { expect } from "chai";
6 | import { validateGUID, validateEmail } from "../src/Utility/Validators";
7 |
8 | describe("GUID Validator", () => {
9 | describe("Validation of a Real GUID", () => {
10 | it("Validates the null GUID as a GUID", () => {
11 | // Attempt to validate a nil GUID
12 | const nullGUID = validateGUID("00000000-0000-0000-0000-000000000000");
13 |
14 | // A nil GUID is a real GUID, the GUID validator should return true.
15 | expect(nullGUID).to.equal(true);
16 | });
17 |
18 | it("Validates a GUID as a GUID", () => {
19 | // Attempt to validate a non nil GUID
20 | const normalGUID = validateGUID("123e4567-e89b-12d3-a456-426652340000");
21 |
22 | // A v1 GUID is a valid GUID. The GUID validator should return true.
23 | expect(normalGUID).to.equal(true);
24 | });
25 |
26 | it("Validates an array with a single GUID object as a GUID", () => {
27 | // Attempt to validate an array with a single index that is a proper GUID
28 | const arrayGUID = validateGUID(["123e4567-e89b-12d3-a456-426652340000"]);
29 |
30 | // An array with a single index that is a GUID is a proper GUID. The GUID validator should return true.
31 | expect(arrayGUID).to.equal(true);
32 | });
33 | });
34 |
35 | describe("Validation of non GUID", () => {
36 | it("Validates undefined input as not a GUID", () => {
37 | // An undefined value is not a valid GUID
38 | const undefinedData = validateGUID(undefined);
39 |
40 | // This should return false
41 | expect(undefinedData).to.equal(false);
42 | });
43 |
44 | it("Validates null input as not a GUID", () => {
45 | // The null value is not a valid GUID
46 | const nullData = validateGUID(null);
47 |
48 | // This should return false
49 | expect(nullData).to.equal(false);
50 | });
51 |
52 | it("Validates non GUID string input as not a GUID", () => {
53 | // GUIDs are structured very specifically, this is not the valid structure
54 | const stringData = validateGUID("Hello world!");
55 |
56 | // This should return false
57 | expect(stringData).to.equal(false);
58 | });
59 |
60 | it("Validates integer input as not a GUID", () => {
61 | // An integer can't be a valid GUID as GUIDs have special characters
62 | const integerData = validateGUID(1234234567890);
63 |
64 | // This should return false
65 | expect(integerData).to.equal(false);
66 | });
67 |
68 | });
69 | });
70 |
71 | describe("Email Validator", () => {
72 | describe("Validation of a correct single email address", () => {
73 | it("Validates a normal email address", () => {
74 | // Collect results of validateEmail using a normal email address
75 | const validNormalEmail = validateEmail("something@something.com");
76 |
77 | // A normal email address is a valid email address, the validNormalEmail should be true.
78 | expect(validNormalEmail).to.equal(true);
79 | });
80 |
81 | it("Validates a custom TLD email address", () => {
82 | // Collect results of validateEmail using a custom TLD email address
83 | const customTLDEmail = validateEmail("someone@localhost.localdomain");
84 |
85 | // A custom TLD email address is a valid email address, the customTLDEmail should be true.
86 | expect(customTLDEmail).to.equal(true);
87 | });
88 |
89 | // ***DOESN'T WORK WITH CURRENT REGEX***
90 | // TODO Fix REGEX to work with IP addresses, or remove this test
91 | // it("Validates a IP Address as host email address", () => {
92 | // // Collect results of validateEmail using a IP Address as host email address
93 | // const ipAddressAsHostEmail = validateEmail("someone@127.0.0.1");
94 |
95 | // // A IP Address as host email address is a valid email address, the ipAddressAsHostEmail should be true.
96 | // expect(ipAddressAsHostEmail).to.equal(true);
97 | // });
98 | });
99 | });
100 |
101 |
102 | // //TEMPLATE - VALID EMAIL TYPE
103 | // // it("Validates a email address", () => {
104 | // // // Collect results of validateEmail using a as host email address
105 | // // const = validateEmail("");
106 |
107 | // // // A IP Address as host email address is a valid email address, the should be true.
108 | // // expect(TYPEOFEMAILADDRESSVARIABLE).to.equal(true);
109 | // // });
110 |
111 | // });
112 |
113 | // describe("Validation of non GUID", () => {
114 | // it("Validates undefined input as not a GUID", () => {
115 | // // An undefined value is not a valid GUID
116 | // const undefinedData = validateEmail(undefined);
117 |
118 | // // This should return false
119 | // expect(undefinedData).to.equal(false);
120 | // });
121 |
122 | // it("Validates null input as not a GUID", () => {
123 | // // The null value is not a valid GUID
124 | // const nullData = validateEmail(null);
125 |
126 | // // This should return false
127 | // expect(nullData).to.equal(false);
128 | // });
129 |
130 | // it("Validates non GUID string input as not a GUID", () => {
131 | // // GUIDs are structured very specifically, this is not the valid structure
132 | // const stringData = validateEmail("Hello world!");
133 |
134 | // // This should return false
135 | // expect(stringData).to.equal(false);
136 | // });
137 |
138 | // it("Validates integer input as not a GUID", () => {
139 | // // An integer can't be a valid GUID as GUIDs have special characters
140 | // const integerData = validateEmail(1234234567890);
141 |
142 | // // This should return false
143 | // expect(integerData).to.equal(false);
144 | // });
145 |
146 | // it("Validates an array with a single GUID object as a GUID", () => {
147 | // // Give the validator multiple GUIDs to validate in a single sitting
148 | // const arrayGUID = validateEmail(["123e4567-e89b-12d3-a456-426652340000", "00000000-0000-0000-0000-000000000000"]);
149 |
150 | // // This should return false as the GUID validator was designed to only work with one GUID at a time
151 | // expect(arrayGUID).to.equal(false);
152 | // });
153 | // });
154 | // });
155 |
156 |
157 | // Will not accept - *, / , {}, \, ", [], `, +, =, comma, <>, (), emoji
158 | // Will Accept - #, !, ^, ', ., ~, -, _, A-Z, a-z, 0-9,
159 |
160 |
161 | // Email address examples to be tested in the unit test
162 |
163 | // debug("Valid single addresses when 'multiple' attribute is not set.");
164 | // emailCheck("a@b.b", "a@b.b", expectValid); Single letter TLD
165 | // emailCheck("a/b@domain.com", "a/b@domain.com", expectValid); Forward-slash in Username field
166 | // emailCheck("{}@domain.com", "{}@domain.com", expectValid); Brackets as valid user name
167 | // emailCheck("m*'!%@something.sa", "m*'!%@something.sa", expectValid);
168 | // emailCheck("tu!!7n7.ad##0!!!@company.ca", "tu!!7n7.ad##0!!!@company.ca", expectValid);
169 | // emailCheck("%@com.com", "%@com.com", expectValid);
170 | // emailCheck("!#$%&'*+/=?^_`{|}~.-@com.com", "!#$%&'*+/=?^_`{|}~.-@com.com", expectValid);
171 | // emailCheck(".wooly@example.com", ".wooly@example.com", expectValid);
172 | // emailCheck("wo..oly@example.com", "wo..oly@example.com", expectValid);
173 | // emailCheck("someone@do-ma-in.com", "someone@do-ma-in.com", expectValid);
174 | // emailCheck("somebody@example", "somebody@example", expectValid);
175 | // emailCheck("\u000Aa@p.com\u000A", "a@p.com", expectValid); - emoji
176 | // emailCheck("\u000Da@p.com\u000D", "a@p.com", expectValid);
177 | // emailCheck("a\u000A@p.com", "a@p.com", expectValid);
178 | // emailCheck("a\u000D@p.com", "a@p.com", expectValid);
179 | // emailCheck("", "", expectValid);
180 | // emailCheck(" ", "", expectValid);
181 | // emailCheck(" a@p.com", "a@p.com", expectValid);
182 | // emailCheck("a@p.com ", "a@p.com", expectValid);
183 | // emailCheck(" a@p.com ", "a@p.com", expectValid);
184 | // emailCheck("\u0020a@p.com\u0020", "a@p.com", expectValid);
185 | // emailCheck("\u0009a@p.com\u0009", "a@p.com", expectValid);
186 | // emailCheck("\u000Ca@p.com\u000C", "a@p.com", expectValid);
187 |
188 | // debug("Invalid single addresses when 'multiple' attribute is not set.");
189 | // emailCheck("invalid:email@example.com", "invalid:email@example.com", expectInvalid);
190 | // emailCheck("@somewhere.com", "@somewhere.com", expectInvalid);
191 | // emailCheck("example.com", "example.com", expectInvalid);
192 | // emailCheck("@@example.com", "@@example.com", expectInvalid);
193 | // emailCheck("a space@example.com", "a space@example.com", expectInvalid);
194 | // emailCheck("something@ex..ample.com", "something@ex..ample.com", expectInvalid);
195 | // emailCheck("a\b@c", "a\b@c", expectInvalid);
196 | // emailCheck("someone@somewhere.com.", "someone@somewhere.com.", expectInvalid);
197 | // emailCheck("\"\"test\blah\"\"@example.com", "\"\"test\blah\"\"@example.com", expectInvalid);
198 | // emailCheck("\"testblah\"@example.com", "\"testblah\"@example.com", expectInvalid);
199 | // emailCheck("someone@somewhere.com@", "someone@somewhere.com@", expectInvalid);
200 | // emailCheck("someone@somewhere_com", "someone@somewhere_com", expectInvalid);
201 | // emailCheck("someone@some:where.com", "someone@some:where.com", expectInvalid);
202 | // emailCheck(".", ".", expectInvalid);
203 | // emailCheck("F/s/f/a@feo+re.com", "F/s/f/a@feo+re.com", expectInvalid);
204 | // emailCheck("some+long+email+address@some+host-weird-/looking.com", "some+long+email+address@some+host-weird-/looking.com", expectInvalid);
205 | // emailCheck("a @p.com", "a @p.com", expectInvalid);
206 | // emailCheck("a\u0020@p.com", "a\u0020@p.com", expectInvalid);
207 | // emailCheck("a\u0009@p.com", "a\u0009@p.com", expectInvalid);
208 | // emailCheck("a\u000B@p.com", "a\u000B@p.com", expectInvalid);
209 | // emailCheck("a\u000C@p.com", "a\u000C@p.com", expectInvalid);
210 | // emailCheck("a\u2003@p.com", "a\u2003@p.com", expectInvalid);
211 | // emailCheck("a\u3000@p.com", "a\u3000@p.com", expectInvalid);
212 | // emailCheck("ddjk-s-jk@asl-.com", "ddjk-s-jk@asl-.com", expectInvalid);
213 | // emailCheck("someone@do-.com", "someone@do-.com", expectInvalid);
214 | // emailCheck("somebody@-p.com", "somebody@-p.com", expectInvalid);
215 | // emailCheck("somebody@-.com", "somebody@-.com", expectInvalid);
--------------------------------------------------------------------------------
/User-Interface/src/models/mocks/autopilotDeviceMock.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | import type { IPsmAutopilotDevice } from ".."
5 |
6 | export const autopilotDeviceList: IPsmAutopilotDevice[] = [
7 | {
8 | "serialNumber": "dumvxj",
9 | "azureActiveDirectoryDeviceId": "0776442e-6846-4ea1-9c20-089244ea43de",
10 | "azureAdDeviceId": "0776442e-6846-4ea1-9c20-089244ea43de"
11 | },
12 | {
13 | "serialNumber": "skbtngeu",
14 | "azureActiveDirectoryDeviceId": "ea6f7e9b-91ee-4910-9e5f-217430d9b759",
15 | "displayName": "Desktop-cmlroks",
16 | "azureAdDeviceId": "ea6f7e9b-91ee-4910-9e5f-217430d9b759"
17 | },
18 | {
19 | "serialNumber": "ajeyikdn",
20 | "azureActiveDirectoryDeviceId": "ed8d9ea1-d517-457b-9587-d2ab3262ad36",
21 | "azureAdDeviceId": "ed8d9ea1-d517-457b-9587-d2ab3262ad36"
22 | },
23 | {
24 | "serialNumber": "vwpzjhy",
25 | "azureActiveDirectoryDeviceId": "914c5734-ab85-4352-a874-b29f03bab63d",
26 | "displayName": "Desktop-vpsldib",
27 | "azureAdDeviceId": "914c5734-ab85-4352-a874-b29f03bab63d"
28 | },
29 | {
30 | "serialNumber": "yhvwux",
31 | "azureActiveDirectoryDeviceId": "150caf82-9db0-41ce-bfbf-3b2c443123fd",
32 | "azureAdDeviceId": "150caf82-9db0-41ce-bfbf-3b2c443123fd"
33 | },
34 | {
35 | "serialNumber": "indry",
36 | "azureActiveDirectoryDeviceId": "ac1e099a-50fb-4529-a904-837feba837ba",
37 | "displayName": "Desktop-ynifogu",
38 | "azureAdDeviceId": "ac1e099a-50fb-4529-a904-837feba837ba"
39 | },
40 | {
41 | "serialNumber": "mklpsbtd",
42 | "azureActiveDirectoryDeviceId": "31a05039-51ec-48c8-90b6-2e73c5151dce",
43 | "displayName": "Desktop-bghdrup",
44 | "azureAdDeviceId": "31a05039-51ec-48c8-90b6-2e73c5151dce"
45 | },
46 | {
47 | "serialNumber": "rlyuzfkd",
48 | "azureActiveDirectoryDeviceId": "142ef523-b6ba-4a23-9642-e233afe137e4",
49 | "displayName": "Desktop-zosqluf",
50 | "azureAdDeviceId": "142ef523-b6ba-4a23-9642-e233afe137e4"
51 | },
52 | {
53 | "serialNumber": "iksqunwtcr",
54 | "azureActiveDirectoryDeviceId": "4c4232da-7758-4061-846d-2b793f4e4a6f",
55 | "azureAdDeviceId": "4c4232da-7758-4061-846d-2b793f4e4a6f"
56 | },
57 | {
58 | "serialNumber": "baxqen",
59 | "azureActiveDirectoryDeviceId": "6b839edc-43c6-46c6-9d5d-d418a9a40023",
60 | "azureAdDeviceId": "6b839edc-43c6-46c6-9d5d-d418a9a40023"
61 | },
62 | {
63 | "serialNumber": "naumpdw",
64 | "azureActiveDirectoryDeviceId": "e71e4dbd-915c-4e7d-a871-1ce819ba90c8",
65 | "displayName": "Desktop-xvohuji",
66 | "azureAdDeviceId": "e71e4dbd-915c-4e7d-a871-1ce819ba90c8"
67 | },
68 | {
69 | "serialNumber": "jkcmf",
70 | "azureActiveDirectoryDeviceId": "c236af0d-e95c-4d17-a443-9b951233c058",
71 | "displayName": "Desktop-muqvsgk",
72 | "azureAdDeviceId": "c236af0d-e95c-4d17-a443-9b951233c058"
73 | },
74 | {
75 | "serialNumber": "xgjwov",
76 | "azureActiveDirectoryDeviceId": "960259c5-7fc7-43ec-85cf-155c1fe131c3",
77 | "displayName": "Desktop-tqkfyve",
78 | "azureAdDeviceId": "960259c5-7fc7-43ec-85cf-155c1fe131c3"
79 | },
80 | {
81 | "serialNumber": "ujqlygdw",
82 | "azureActiveDirectoryDeviceId": "f0c6db2c-65fa-49f4-b7c6-5be32a1c238c",
83 | "displayName": "Desktop-cblwspo",
84 | "azureAdDeviceId": "f0c6db2c-65fa-49f4-b7c6-5be32a1c238c"
85 | },
86 | {
87 | "serialNumber": "kigudyt",
88 | "azureActiveDirectoryDeviceId": "3ae30854-be27-40ff-acf7-8e04672a198c",
89 | "azureAdDeviceId": "3ae30854-be27-40ff-acf7-8e04672a198c"
90 | },
91 | {
92 | "serialNumber": "pfdklnm",
93 | "azureActiveDirectoryDeviceId": "87ef2f84-cd6b-4ad2-b57f-dc67cbcfb3c8",
94 | "displayName": "Desktop-nerdvwf",
95 | "azureAdDeviceId": "87ef2f84-cd6b-4ad2-b57f-dc67cbcfb3c8"
96 | },
97 | {
98 | "serialNumber": "kgbxtlsnqj",
99 | "azureActiveDirectoryDeviceId": "03f52ecc-6ee5-4a08-a1ab-f4dcb0e323db",
100 | "displayName": "Desktop-rkyjpot",
101 | "azureAdDeviceId": "03f52ecc-6ee5-4a08-a1ab-f4dcb0e323db"
102 | },
103 | {
104 | "serialNumber": "qckpbdx",
105 | "azureActiveDirectoryDeviceId": "424f5c0d-8afd-4da0-9b8b-bc30c3332dbb",
106 | "displayName": "Desktop-zdpexsa",
107 | "azureAdDeviceId": "424f5c0d-8afd-4da0-9b8b-bc30c3332dbb"
108 | },
109 | {
110 | "serialNumber": "qyujkpem",
111 | "azureActiveDirectoryDeviceId": "a8118077-dcdc-4975-a694-82e10c4213f8",
112 | "displayName": "Desktop-cmkhyzn",
113 | "azureAdDeviceId": "a8118077-dcdc-4975-a694-82e10c4213f8"
114 | },
115 | {
116 | "serialNumber": "tiwmjn",
117 | "azureActiveDirectoryDeviceId": "d34bf26f-c905-4755-9a27-242155993857",
118 | "azureAdDeviceId": "d34bf26f-c905-4755-9a27-242155993857"
119 | },
120 | {
121 | "serialNumber": "ojwecqgy",
122 | "azureActiveDirectoryDeviceId": "5ad2a73f-f9c5-444d-9191-a4be28040383",
123 | "displayName": "Desktop-zofnhgw",
124 | "azureAdDeviceId": "5ad2a73f-f9c5-444d-9191-a4be28040383"
125 | },
126 | {
127 | "serialNumber": "tepqhjafv",
128 | "azureActiveDirectoryDeviceId": "8b49acdf-2ff4-4f52-a885-fefadac308c1",
129 | "azureAdDeviceId": "8b49acdf-2ff4-4f52-a885-fefadac308c1"
130 | },
131 | {
132 | "serialNumber": "vikho",
133 | "azureActiveDirectoryDeviceId": "a567c4fb-0fdb-4e64-bd94-2c84971512a9",
134 | "displayName": "Desktop-dbgipmh",
135 | "azureAdDeviceId": "a567c4fb-0fdb-4e64-bd94-2c84971512a9"
136 | },
137 | {
138 | "serialNumber": "yzcrsx",
139 | "azureActiveDirectoryDeviceId": "d6159b50-f3ed-4def-a4c2-1dba07e3930e",
140 | "azureAdDeviceId": "d6159b50-f3ed-4def-a4c2-1dba07e3930e"
141 | },
142 | {
143 | "serialNumber": "dnfkzo",
144 | "azureActiveDirectoryDeviceId": "d0891700-9197-401f-a333-5378436c6b99",
145 | "azureAdDeviceId": "d0891700-9197-401f-a333-5378436c6b99"
146 | },
147 | {
148 | "serialNumber": "pemwfhu",
149 | "azureActiveDirectoryDeviceId": "09aa5e98-e859-467d-8601-dd8465828553",
150 | "azureAdDeviceId": "09aa5e98-e859-467d-8601-dd8465828553"
151 | },
152 | {
153 | "serialNumber": "dazcisuyo",
154 | "azureActiveDirectoryDeviceId": "85da8e18-9ed9-4715-9252-3af036184ac0",
155 | "displayName": "Desktop-awpueqm",
156 | "azureAdDeviceId": "85da8e18-9ed9-4715-9252-3af036184ac0"
157 | },
158 | {
159 | "serialNumber": "fnimbol",
160 | "azureActiveDirectoryDeviceId": "68c972e1-a33e-43e4-9dda-9ea2e8f8715d",
161 | "displayName": "Desktop-iwutaph",
162 | "azureAdDeviceId": "68c972e1-a33e-43e4-9dda-9ea2e8f8715d"
163 | },
164 | {
165 | "serialNumber": "fibgmu",
166 | "azureActiveDirectoryDeviceId": "b5a9c121-7e19-4dbd-a447-d24d5462f644",
167 | "azureAdDeviceId": "b5a9c121-7e19-4dbd-a447-d24d5462f644"
168 | },
169 | {
170 | "serialNumber": "ukxcdzmahn",
171 | "azureActiveDirectoryDeviceId": "316af25c-e72e-47f4-9a40-40cb249de5b2",
172 | "azureAdDeviceId": "316af25c-e72e-47f4-9a40-40cb249de5b2"
173 | },
174 | {
175 | "serialNumber": "oupmaxlrtj",
176 | "azureActiveDirectoryDeviceId": "6fddb047-7be8-4470-9700-9e04d986ec77",
177 | "azureAdDeviceId": "6fddb047-7be8-4470-9700-9e04d986ec77"
178 | },
179 | {
180 | "serialNumber": "okpiyl",
181 | "azureActiveDirectoryDeviceId": "e13f73cd-588a-47a9-bf0a-de6c50c20032",
182 | "azureAdDeviceId": "e13f73cd-588a-47a9-bf0a-de6c50c20032"
183 | },
184 | {
185 | "serialNumber": "wkgjinb",
186 | "azureActiveDirectoryDeviceId": "17f94781-6585-4fa2-88b6-f309f34e8190",
187 | "azureAdDeviceId": "17f94781-6585-4fa2-88b6-f309f34e8190"
188 | },
189 | {
190 | "serialNumber": "grjfvoxmas",
191 | "azureActiveDirectoryDeviceId": "d36bc77b-830c-4591-a5ab-0e2409dbcb5a",
192 | "azureAdDeviceId": "d36bc77b-830c-4591-a5ab-0e2409dbcb5a"
193 | },
194 | {
195 | "serialNumber": "yrden",
196 | "azureActiveDirectoryDeviceId": "d8d50bd8-d458-4b69-af51-eb3309a40785",
197 | "azureAdDeviceId": "d8d50bd8-d458-4b69-af51-eb3309a40785"
198 | },
199 | {
200 | "serialNumber": "jumiz",
201 | "azureActiveDirectoryDeviceId": "19613951-fab8-413d-a3d6-6d74eb3e38b0",
202 | "displayName": "Desktop-ltfzxpd",
203 | "azureAdDeviceId": "19613951-fab8-413d-a3d6-6d74eb3e38b0"
204 | },
205 | {
206 | "serialNumber": "xbmkwt",
207 | "azureActiveDirectoryDeviceId": "480e5028-02b2-40fa-b5dd-d7b62505d633",
208 | "displayName": "Desktop-npxbtuo",
209 | "azureAdDeviceId": "480e5028-02b2-40fa-b5dd-d7b62505d633"
210 | },
211 | {
212 | "serialNumber": "ldncsz",
213 | "azureActiveDirectoryDeviceId": "38e12681-25e7-49eb-a549-86a5a5651671",
214 | "displayName": "Desktop-gcufnjd",
215 | "azureAdDeviceId": "38e12681-25e7-49eb-a549-86a5a5651671"
216 | },
217 | {
218 | "serialNumber": "qgmdk",
219 | "azureActiveDirectoryDeviceId": "85baf69b-0faf-47b5-a8c3-2ab9ad2daeb8",
220 | "azureAdDeviceId": "85baf69b-0faf-47b5-a8c3-2ab9ad2daeb8"
221 | },
222 | {
223 | "serialNumber": "ywebl",
224 | "azureActiveDirectoryDeviceId": "d24c1675-aa36-4444-8287-97c3fddc1841",
225 | "azureAdDeviceId": "d24c1675-aa36-4444-8287-97c3fddc1841"
226 | },
227 | {
228 | "serialNumber": "gwobh",
229 | "azureActiveDirectoryDeviceId": "42c2934c-d7bb-43b4-8ff4-e8faaaf847ff",
230 | "azureAdDeviceId": "42c2934c-d7bb-43b4-8ff4-e8faaaf847ff"
231 | },
232 | {
233 | "serialNumber": "rehzaug",
234 | "azureActiveDirectoryDeviceId": "c9a2b364-0efc-4b75-ab61-671c6f2801e4",
235 | "azureAdDeviceId": "c9a2b364-0efc-4b75-ab61-671c6f2801e4"
236 | },
237 | {
238 | "serialNumber": "hzbnp",
239 | "azureActiveDirectoryDeviceId": "411d7369-7fd1-43cb-a08b-c18e50c0de29",
240 | "displayName": "Desktop-fjtvohi",
241 | "azureAdDeviceId": "411d7369-7fd1-43cb-a08b-c18e50c0de29"
242 | },
243 | {
244 | "serialNumber": "mibvkxe",
245 | "azureActiveDirectoryDeviceId": "e2b386ff-b44a-421d-abd0-5bd4f8300936",
246 | "displayName": "Desktop-koacfyd",
247 | "azureAdDeviceId": "e2b386ff-b44a-421d-abd0-5bd4f8300936"
248 | },
249 | {
250 | "serialNumber": "lcutbonyaf",
251 | "azureActiveDirectoryDeviceId": "fea8b717-2d9d-4e4d-9b14-5d43473fa2a5",
252 | "displayName": "Desktop-lwbyjgp",
253 | "azureAdDeviceId": "fea8b717-2d9d-4e4d-9b14-5d43473fa2a5"
254 | },
255 | {
256 | "serialNumber": "rjpxqnuf",
257 | "azureActiveDirectoryDeviceId": "feae6b79-843b-4d59-9d4b-a1eb00938b8b",
258 | "azureAdDeviceId": "feae6b79-843b-4d59-9d4b-a1eb00938b8b"
259 | },
260 | {
261 | "serialNumber": "qsfpev",
262 | "azureActiveDirectoryDeviceId": "c8bdde3b-6996-47f2-a8b4-e3e6132cb4f1",
263 | "azureAdDeviceId": "c8bdde3b-6996-47f2-a8b4-e3e6132cb4f1"
264 | },
265 | {
266 | "serialNumber": "izepnflvm",
267 | "azureActiveDirectoryDeviceId": "eebd70f6-25e8-4aec-a1be-4116e02044f4",
268 | "azureAdDeviceId": "eebd70f6-25e8-4aec-a1be-4116e02044f4"
269 | },
270 | {
271 | "serialNumber": "fbwsox",
272 | "azureActiveDirectoryDeviceId": "6e7f17b0-dc88-4b4c-9b70-cb17d34460b6",
273 | "displayName": "Desktop-ipndkyq",
274 | "azureAdDeviceId": "6e7f17b0-dc88-4b4c-9b70-cb17d34460b6"
275 | },
276 | {
277 | "serialNumber": "vzuhymqf",
278 | "azureActiveDirectoryDeviceId": "ca088fb9-4ca6-4908-a288-0112b342e446",
279 | "displayName": "Desktop-aynzkbd",
280 | "azureAdDeviceId": "ca088fb9-4ca6-4908-a288-0112b342e446"
281 | }
282 | ]
--------------------------------------------------------------------------------