├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── BUG_BASH.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── controls └── recommended.json ├── secmgmt-insights-connector.sln ├── src └── SecMgmtInsights │ ├── Diagnostics.pqm │ ├── Schema.pqm │ ├── SecMgmtInsights.mproj │ ├── SecMgmtInsights.pq │ ├── SecMgmtInsights.query.pq │ ├── SecMgmtInsights16.png │ ├── SecMgmtInsights20.png │ ├── SecMgmtInsights24.png │ ├── SecMgmtInsights32.png │ ├── SecMgmtInsights40.png │ ├── SecMgmtInsights48.png │ ├── SecMgmtInsights64.png │ ├── SecMgmtInsights80.png │ ├── Table.ChangeType.pqm │ ├── Table.GenerateByPage.pqm │ ├── Table.ToNavigationTable.pqm │ ├── client_id │ └── resources.resx └── templates ├── secmgmt-insights-customer.pbit └── secmgmt-insights-partner.pbit /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Instructions for CODEOWNERS file format and automatic build failure notifications: 2 | # https://github.com/Azure/azure-sdk/blob/master/docs/policies/opensource.md#codeowners 3 | 4 | * @isaiahwilliams -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ## Steps to reproduce 8 | 9 | > What steps can reproduce the defect? 10 | > Please share the setup, sample project, target, etc. 11 | 12 | ## Expected behavior 13 | 14 | > Share the expected output 15 | 16 | ## Actual behavior 17 | 18 | > What is the behavior observed? 19 | 20 | ## Diagnostic logs 21 | 22 | > Please share test platform diagnostics logs. 23 | > The logs may contain test assembly paths, kindly review and mask those before sharing. 24 | 25 | ## Environment 26 | 27 | > Please share additional details about your environment. 28 | > Version -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | ## Feature Request 8 | 9 | **Is your feature request related to a problem?** 10 | A clear and concise description of what the problem is. Ex. I am always frustrated when [...] 11 | 12 | **Describe the solution you would like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you have considered** 16 | A clear and concise description of any alternative solutions or features you have considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please add a meaningful description for this change. Ensure the PR has required unit tests. 4 | 5 | ## Related issue 6 | 7 | Kindly link any related issues (e.g. Fixes #xyz). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # VS Code 353 | .vscode 354 | 355 | # Azure Functions 356 | local.settings.json -------------------------------------------------------------------------------- /BUG_BASH.md: -------------------------------------------------------------------------------- 1 | # Security and Management Insights Connector 2 | 3 | As enhancements are made to the connector and new features added it can be difficult to test for every scenario. While we strive to ensure the solution is free of any complications and issues, there is a chance something might make it through testing. This means there is a possbility that the connector might not behave as expected. With this in mind from time to time we will have a virtual bug bash where everyone is encouraged to create bugs for any unexpected behavior. Often these events will happen when significant changes are made to the connector. 4 | 5 | ## Bug Bash August 2020 6 | 7 | Starting on August 1, 2020 will be starting our first bug bash for the connector. During this time we will be hunting for bugs, issues, and unexpected behavior with the recent change to dynamically detect the table schema. It is important to note that these changes have not been officially released, so anyone who wants to test this new feature will need to compile the module. You can compile the module by following the process below 8 | 9 | 1. Install the [Power Query SDK](https://marketplace.visualstudio.com/items?itemName=Dakahn.PowerQuerySDK) extension for Visual Studio 10 | 2. Clone this repository and then open the *SecMgmt-Insights-Connector.sln* solution 11 | 3. Replace the GUID in the *client_id* with the application identifier 12 | 4. Build the solution and then copy *src\secmgmt-insights-connector\artifacts\SecMgmtInsights.mez* to the *[Documents]\Microsoft Power BI Desktop\Custom Connectors* directory 13 | 14 | After performing the above process it is recommend that you restart Power BI Desktop if you had it open. Now you will be able to test all the latest features that have been recently added to the module. 15 | 16 | During this time there were a number of general updates, fixes, and new features added to the connector. The following sections provide a snapshot of what was accomplished during the August 2020 event 17 | 18 | ### Breaking Changes 19 | 20 | - Query folding is one of the new features, and when it was implemented the way functions are called needed to be changed. 21 | 22 | ```powerquery-m 23 | // Old way to invoke 24 | SecMgmtInsights.ManagedDevices() 25 | 26 | // New way to invoke 27 | source = SecMgmtInsights.Contents("Partner"), 28 | managedDevices = source{[Name="ManagedDevices"]}[Data] 29 | ``` 30 | 31 | - You will need to add the `IdentityRiskEvent.Read.All` permission to the Azure AD application registration to utilize the `RiskDetections` function 32 | 33 | ### General Updates 34 | 35 | - Performance has been improved by only selecting the fields, from the API, that are needed ([#76](https://github.com/microsoft/secmgmt-insights-connector/issues/76)) 36 | - The device actions function has been refactored to optimize the request 37 | 38 | ### Fixes 39 | 40 | - Access tokens should no longer expire ([#92](https://github.com/microsoft/secmgmt-insights-connector/issues/92)) 41 | - Column types are no longer lost by the connector when it expands tables ([#70](https://github.com/microsoft/secmgmt-insights-connector/issues/70)) 42 | - Device actions will now show configuration actions as expected ([#97](https://github.com/microsoft/secmgmt-insights-connector/issues/97])) 43 | - Device compliance policies functions have been updated to account for the new dynamic schema feature ([#79](https://github.com/microsoft/secmgmt-insights-connector/issues/79)) 44 | - Device configuration profiles functions have been updated and renamed to account for the new dynamic schema feature ([#82](https://github.com/microsoft/secmgmt-insights-connector/issues/82)) 45 | - Functions that require a dependency (e.g. Microsoft Intune) will now return null if the customer has not fulfilled the dependency. Also, additional diagnostics have been added to provide insight to when this is happening ([#88](https://github.com/microsoft/secmgmt-insights-connector/issues/88) [#90](https://github.com/microsoft/secmgmt-insights-connector/issues/90) [#91](https://github.com/microsoft/secmgmt-insights-connector/issues/91)) 46 | - Get data feature now loads each functions without error, if at least once tenant in the request has data ([#89](https://github.com/microsoft/secmgmt-insights-connector/issues/89)) 47 | - Errors are no longer replaced by null ([#67](https://github.com/microsoft/secmgmt-insights-connector/issues/67)) 48 | - Network requests are no longer being duplicated ([#96](https://github.com/microsoft/secmgmt-insights-connector/issues/96)) 49 | - Mobile applications function now load data as expected ([#77](https://github.com/microsoft/secmgmt-insights-connector/issues/77)) 50 | - Office 365 usage reports now loads with a dynamic schema ([#94](https://github.com/microsoft/secmgmt-insights-connector/issues/94)) 51 | - Risk detection information for users is now available ([#72](https://github.com/microsoft/secmgmt-insights-connector/issues/72)) 52 | - Security baseline setting states function no longer encounters an error if no customers are utilizing security baselines ([#106](https://github.com/microsoft/secmgmt-insights-connector/issues/106)) 53 | - Service status information now loads with a dynamic schema ([#84](https://github.com/microsoft/secmgmt-insights-connector/issues/84)) 54 | - Software update summary information now loads as expected ([#104](https://github.com/microsoft/secmgmt-insights-connector/issues/104)) 55 | - Subscribed SKUs function now loads correctly with a dynamic schema ([#85](https://github.com/microsoft/secmgmt-insights-connector/issues/85)) 56 | - When the API only returns an enumeration, the response will now load correctly ([#103](https://github.com/microsoft/secmgmt-insights-connector/issues/103)) 57 | - Windows Autopilot settings now load as expected ([#101](https://github.com/microsoft/secmgmt-insights-connector/issues/101)) 58 | 59 | ### New Features 60 | 61 | - License detail for each user is now available through the `LicenseDetails` function 62 | - Query folding support has been enabled ([#65](https://github.com/microsoft/secmgmt-insights-connector/issues/65)) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Security and Management Insights Connector 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to and actually do, grant us 4 | the rights to use your contribution. View the [Contributor License Agreement](https://cla.microsoft.com) for more details. 5 | 6 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 7 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 8 | provided by the bot. You will only need to do this once across all repos using our CLA. 9 | 10 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 11 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 12 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 13 | 14 | - [Code of Conduct](#code-of-conduct) 15 | - [Issues and Bugs](#finding-issues) 16 | - [Feature Requests](#requesting-features) 17 | - [Submission Guidelines](#submission-guidelines) 18 | 19 | ## Code of Conduct 20 | 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Finding Issues 24 | 25 | If you find a bug in the source code or a mistake in the documentation, you can help us by 26 | [submitting an issue](#submitting-an-issue) to the GitHub Repository. Even better, you can 27 | [submit a Pull Request](#submitting-a-pull-request) with a fix. 28 | 29 | ## Requesting Features 30 | 31 | You can *request* a new feature by [submitting an issue](#submitting-an-issue) to the GitHub 32 | Repository. If you would like to *implement* a new feature, please submit an issue with 33 | a proposal for your work first, to be sure that we can use it. 34 | 35 | **Small Features** can be crafted and directly [submitted as a Pull Request](#submitting-a-pull-request). 36 | 37 | ## Submission Guidelines 38 | 39 | ### Submitting an Issue 40 | 41 | Before you submit an issue, search the archive, maybe your question was already answered. 42 | 43 | If your issue appears to be a bug and hasn't been reported, open a new issue. 44 | Help us to maximize the effort we can spend fixing issues and add new 45 | features, by not reporting duplicate issues. Providing the following information will increase the 46 | chances of your issue being dealt with quickly: 47 | 48 | - **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 49 | - **Version** - what version is affected (e.g. 0.1.2) 50 | - **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 51 | - **Browsers and Operating System** - is this a problem with all browsers? 52 | - **Reproduce the Error** - provide a live example or an unambiguous set of steps 53 | - **Related Issues** - has a similar issue been reported before? 54 | - **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 55 | causing the problem (line of code or commit) 56 | 57 | You can file new issues by providing the above information at the [corresponding repository's issues link](https://github.com/microsoft/secmgmt-insights-connector/issues/new). 58 | 59 | ### Submitting a Pull Request 60 | 61 | Before you submit your Pull Request (PR) consider the following guidelines: 62 | 63 | - [Search the repository](https://github.com/microsoft/secmgmt-insights-connector/pulls) for an open or closed PR 64 | that relates to your submission. You don't want to duplicate effort. 65 | 66 | - Make your changes in a new git fork: 67 | 68 | - Commit your changes using a descriptive commit message 69 | - Push your fork to GitHub: 70 | - In GitHub, create a pull request 71 | - If we suggest changes then: 72 | - Make the required updates. 73 | - Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 74 | 75 | ```shell 76 | git rebase master -i 77 | git push -f 78 | ``` 79 | 80 | That is it! Thank you for your contribution! 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Security and Management Insights Connector 2 | 3 | Microsoft 365 provides several advanced security and management features that empower you to improve your, or your customers, security posture. Knowing what features are configured and whether they adhere to the recommended configurations is challenging. Using this connector, you will be able gain insights what components have been adopted and how they are configured. 4 | 5 | ## Bug Bash 6 | 7 | As enhancements are made to the connector and new features added it can be difficult to test for every scenario. While we strive to ensure the solution is free of any complications and issues, there is a chance something might make it through testing. This means there is a possbility that the connector might not behave as expected. With this in mind from time to time we will have a virtual bug bash where everyone is encouraged to create bugs for any unexpected behavior. Often these events will happen when significant changes are made to the connector. See our [bug bash](BUG_BASH.md) guide for more details. 8 | 9 | ## Getting Started 10 | 11 | Prior to utilizing this connector to gain insights from your, or your customers, environment there are some required actions that need to be taken. The remaining sections of this article will guide you through fulfilling the prerequisites, installing the connector, and how to leverage on the templates. If you have any questions about this process please open an [issue](https://github.com/microsoft/secmgmt-insights-connector/issues/new/choose) for help. 12 | 13 | ### Prerequisites 14 | 15 | If you do not already have Power BI Desktop installed, then [download](https://powerbi.microsoft.com/downloads/) and install it. Once installed you will want to perform the perform the following 16 | 17 | 1. Start Power BI Desktop on the device where you plan to install the connector 18 | 2. Click file -> options and settings -> options 19 | 3. Click security under the global section 20 | 4. Click (Not Recommended) Allow any extension to load without validation or warning 21 | 22 | > This step is required because by default the connector that will be installed is not digitally signed. It is not signed because during the installation process an application identifier value will be injected that is unique for your environment. See [handling Power Query Connector signing](https://docs.microsoft.com/power-query/HandlingConnectorSigning) for details on the signing the connector if you are interested. 23 | 24 | ### Installation 25 | 26 | To simplify the process of creating and configuring the dependent resources for the connect, the [Install-SecMgmtInsightsConnector](https://github.com/microsoft/secmgmt-open-powershell/blob/master/docs/help/Install-SecMgmtInsightsConnector.md) cmdlet has been added to the [Security and Management Open PowerShell module](https://www.powershellgallery.com/packages/SecMgmt). You can leverage the following PowerShell to install the connector on the device invoking the command 27 | 28 | ```powershell 29 | Install-Module SecMgmt 30 | 31 | # When prompt for credentials you will need to specify an account that has the ability to create an Azure Active Directory application. 32 | Connect-SecMgmtAccount 33 | 34 | # Use the following if you are planning to gain insights for a single tenant. 35 | Install-SecMgmtInsightsConnector -ApplicationDisplayName 'Security and Management Insights' 36 | 37 | # Use the following line if you plan to use the connector to gain insights for customers you have through the Cloud Solution Provider program. 38 | Install-SecMgmtInsightsConnector -ApplicationDisplayName 'Security and Management Insights' -ConfigurePreconsent:$true 39 | ``` 40 | 41 | > When you invoke the above a new Azure Active Directory application will be created, for use with the connector. Then the latest version of the connector is downloaded, configured, and installed on the local device. 42 | 43 | ### Template 44 | 45 | Once the prerequisites have been fulfilled and the connector has be installed, you can start building reports that incorporate functionality from the connector. You can start from scratch or leverage one of the following templates 46 | 47 | | Name | Description | 48 | |------|-------------| 49 | | [Customer template](https://github.com/microsoft/secmgmt-insights-connector/raw/master/templates/secmgmt-insights-customer.pbit) |Template that is intended to be used if you are looking to get insights for your own tenant | 50 | | [Partner template](https://github.com/microsoft/secmgmt-insights-connector/raw/master/templates/secmgmt-insights-partner.pbit) | Template that is intended to be used by partners to gain insights for their customers | 51 | 52 | ## Contributing 53 | 54 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . 55 | 56 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 57 | 58 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /controls/recommended.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "recommended", 3 | "description": "Recommended controls for SMB customers", 4 | "version": "1.3", 5 | "controls": [ 6 | { 7 | "expectedValue": "true", 8 | "id": "EnableAntispoofEnforcement", 9 | "info": "https://docs.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365-atp", 10 | "resource": "antiPhishPolicy", 11 | "tenantFilter": [], 12 | "type": "data" 13 | }, 14 | { 15 | "expectedValue": "false", 16 | "id": "EnableInternalSenderNotifications", 17 | "info": "https://docs.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365-atp", 18 | "resource": "malwareFilterPolicy", 19 | "tenantFilter": [], 20 | "type": "data" 21 | }, 22 | { 23 | "expectedValue": "true", 24 | "id": "ZapEnabled", 25 | "info": "https://docs.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365-atp", 26 | "resource": "malwareFilterPolicy", 27 | "tenantFilter": [], 28 | "type": "data" 29 | }, 30 | { 31 | "expectedValue": "true", 32 | "id": "EnableTargetedUserProtection", 33 | "info": "https://docs.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365-atp", 34 | "resource": "antiPhishPolicy", 35 | "tenantFilter": [], 36 | "type": "data" 37 | }, 38 | { 39 | "expectedValue": "true", 40 | "id": "UnifiedAuditLogIngestionEnabled", 41 | "info": "https://docs.microsoft.com/microsoft-365/compliance/turn-audit-log-search-on-or-off", 42 | "resource": "adminAuditLogConfig", 43 | "tenantFilter": [], 44 | "type": "data" 45 | }, 46 | { 47 | "expectedValue": "true", 48 | "id": "Windows10CompliancePolicy.SecureBootEnabled", 49 | "info": "https://docs.microsoft.com/mem/intune/protect/device-compliance-get-started", 50 | "resource": "deviceCompliancePolicy", 51 | "tenantFilter": [], 52 | "type": "deviceManagement" 53 | }, 54 | { 55 | "expectedValue": "true", 56 | "id": "Windows10CompliancePolicy.StorageRequireEncryption", 57 | "info": "https://docs.microsoft.com/mem/intune/protect/device-compliance-get-started", 58 | "resource": "deviceCompliancePolicy", 59 | "tenantFilter": [], 60 | "type": "deviceManagement" 61 | }, 62 | { 63 | "expectedValue": "true", 64 | "id": "Windows10EndpointProtectionConfiguration.BitLockerEncryptDevice", 65 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 66 | "resource": "deviceConfigurationPolicy", 67 | "tenantFilter": [], 68 | "type": "deviceManagement" 69 | }, 70 | { 71 | "expectedValue": "2", 72 | "id": "Windows10EndpointProtectionConfiguration.DefenderGuardMyFoldersType", 73 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 74 | "resource": "deviceConfigurationPolicy", 75 | "tenantFilter": [], 76 | "type": "deviceManagement" 77 | }, 78 | { 79 | "expectedValue": "2", 80 | "id": "Windows10EndpointProtectionConfiguration.DefenderNetworkProtectionType", 81 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 82 | "resource": "deviceConfigurationPolicy", 83 | "tenantFilter": [], 84 | "type": "deviceManagement" 85 | }, 86 | { 87 | "expectedValue": "2", 88 | "id": "Windows10GeneralConfiguration.DefenderPromptForSampleSubmission", 89 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 90 | "resource": "deviceConfigurationPolicy", 91 | "tenantFilter": [], 92 | "type": "deviceManagement" 93 | }, 94 | { 95 | "expectedValue": "true", 96 | "id": "Windows10GeneralConfiguration.DefenderRequireCloudProtection", 97 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 98 | "resource": "deviceConfigurationPolicy", 99 | "tenantFilter": [], 100 | "type": "deviceManagement" 101 | }, 102 | { 103 | "expectedValue": "true", 104 | "id": "Windows10GeneralConfiguration.DefenderRequireRealTimeMonitoring", 105 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 106 | "resource": "deviceConfigurationPolicy", 107 | "tenantFilter": [], 108 | "type": "deviceManagement" 109 | }, 110 | { 111 | "expectedValue": "true", 112 | "id": "Windows10GeneralConfiguration.EdgeRequireSmartScreen", 113 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 114 | "resource": "deviceConfigurationPolicy", 115 | "tenantFilter": [], 116 | "type": "deviceManagement" 117 | }, 118 | { 119 | "expectedValue": "5", 120 | "id": "Windows10GeneralConfiguration.PasswordMinutesOfInactivityBeforeScreenTimeout", 121 | "info": "https://docs.microsoft.com/mem/intune/configuration/device-profile-create", 122 | "resource": "deviceConfigurationPolicy", 123 | "tenantFilter": [], 124 | "type": "deviceManagement" 125 | } 126 | ] 127 | } -------------------------------------------------------------------------------- /secmgmt-insights-connector.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30413.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8AE2B348-0F2E-46F6-A290-4A4695F10205}" 7 | ProjectSection(SolutionItems) = preProject 8 | BUG_BASH.md = BUG_BASH.md 9 | CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md 10 | CONTRIBUTING.md = CONTRIBUTING.md 11 | LICENSE = LICENSE 12 | README.md = README.md 13 | SECURITY.md = SECURITY.md 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1F19011A-E9AC-4B7F-B9FD-D10AFE584D7E}" 17 | EndProject 18 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "SecMgmtInsights", "src\SecMgmtInsights\SecMgmtInsights.mproj", "{2ED357A0-2201-43D0-926D-35C9A89EEF75}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|x86 = Debug|x86 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {2ED357A0-2201-43D0-926D-35C9A89EEF75}.Debug|x86.ActiveCfg = Debug|x86 27 | {2ED357A0-2201-43D0-926D-35C9A89EEF75}.Debug|x86.Build.0 = Debug|x86 28 | {2ED357A0-2201-43D0-926D-35C9A89EEF75}.Release|x86.ActiveCfg = Release|x86 29 | {2ED357A0-2201-43D0-926D-35C9A89EEF75}.Release|x86.Build.0 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(NestedProjects) = preSolution 35 | {2ED357A0-2201-43D0-926D-35C9A89EEF75} = {1F19011A-E9AC-4B7F-B9FD-D10AFE584D7E} 36 | EndGlobalSection 37 | GlobalSection(ExtensibilityGlobals) = postSolution 38 | SolutionGuid = {C4F7A29E-B0C7-422E-B4BB-0C0A438F9DF4} 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /src/SecMgmtInsights/Diagnostics.pqm: -------------------------------------------------------------------------------- 1 | let 2 | Diagnostics.LogValue = (prefix, value, optional delayed) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & (try Diagnostics.ValueToText(value) otherwise ""), value, delayed), 3 | Diagnostics.LogValue2 = (prefix, value, result, optional delayed) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Diagnostics.ValueToText(value), result, delayed), 4 | Diagnostics.LogFailure = (text, function) => 5 | let 6 | result = try function() 7 | in 8 | if result[HasError] then Diagnostics.LogValue2(text, result[Error], () => error result[Error], true) else result[Value], 9 | 10 | Diagnostics.WrapFunctionResult = (innerFunction as function, outerFunction as function) as function => 11 | Function.From(Value.Type(innerFunction), (list) => outerFunction(() => Function.Invoke(innerFunction, list))), 12 | 13 | Diagnostics.WrapHandlers = (handlers as record) as record => 14 | Record.FromList( 15 | List.Transform( 16 | Record.FieldNames(handlers), 17 | (h) => Diagnostics.WrapFunctionResult(Record.Field(handlers, h), (fn) => Diagnostics.LogFailure(h, fn))), 18 | Record.FieldNames(handlers)), 19 | 20 | Diagnostics.ValueToText = (value) => 21 | let 22 | List.TransformAndCombine = (list, transform, separator) => Text.Combine(List.Transform(list, transform), separator), 23 | 24 | Serialize.Binary = (x) => "#binary(" & Serialize(Binary.ToList(x)) & ") ", 25 | 26 | Serialize.Function = (x) => _serialize_function_param_type( 27 | Type.FunctionParameters(Value.Type(x)), 28 | Type.FunctionRequiredParameters(Value.Type(x)) ) & 29 | " as " & 30 | _serialize_function_return_type(Value.Type(x)) & 31 | " => (...) ", 32 | 33 | Serialize.List = (x) => "{" & List.TransformAndCombine(x, Serialize, ", ") & "} ", 34 | 35 | Serialize.Record = (x) => "[ " & 36 | List.TransformAndCombine( 37 | Record.FieldNames(x), 38 | (item) => Serialize.Identifier(item) & " = " & Serialize(Record.Field(x, item)), 39 | ", ") & 40 | " ] ", 41 | 42 | Serialize.Table = (x) => "#table( type " & 43 | _serialize_table_type(Value.Type(x)) & 44 | ", " & 45 | Serialize(Table.ToRows(x)) & 46 | ") ", 47 | 48 | Serialize.Identifier = Expression.Identifier, 49 | 50 | Serialize.Type = (x) => "type " & _serialize_typename(x), 51 | 52 | 53 | _serialize_typename = (x, optional funtype as logical) => /* Optional parameter: Is this being used as part of a function signature? */ 54 | let 55 | isFunctionType = (x as type) => try if Type.FunctionReturn(x) is type then true else false otherwise false, 56 | isTableType = (x as type) => try if Type.TableSchema(x) is table then true else false otherwise false, 57 | isRecordType = (x as type) => try if Type.ClosedRecord(x) is type then true else false otherwise false, 58 | isListType = (x as type) => try if Type.ListItem(x) is type then true else false otherwise false 59 | in 60 | 61 | if funtype=null and isTableType(x) then _serialize_table_type(x) else 62 | if funtype=null and isListType(x) then "{ " & @_serialize_typename( Type.ListItem(x) ) & " }" else 63 | if funtype=null and isFunctionType(x) then "function " & _serialize_function_type(x) else 64 | if funtype=null and isRecordType(x) then _serialize_record_type(x) else 65 | 66 | if x = type any then "any" else 67 | let base = Type.NonNullable(x) in 68 | (if Type.IsNullable(x) then "nullable " else "") & 69 | (if base = type anynonnull then "anynonnull" else 70 | if base = type binary then "binary" else 71 | if base = type date then "date" else 72 | if base = type datetime then "datetime" else 73 | if base = type datetimezone then "datetimezone" else 74 | if base = type duration then "duration" else 75 | if base = type logical then "logical" else 76 | if base = type none then "none" else 77 | if base = type null then "null" else 78 | if base = type number then "number" else 79 | if base = type text then "text" else 80 | if base = type time then "time" else 81 | if base = type type then "type" else 82 | 83 | /* Abstract types: */ 84 | if base = type function then "function" else 85 | if base = type table then "table" else 86 | if base = type record then "record" else 87 | if base = type list then "list" else 88 | 89 | "any /*Actually unknown type*/"), 90 | 91 | _serialize_table_type = (x) => 92 | let 93 | schema = Type.TableSchema(x) 94 | in 95 | "table " & 96 | (if Table.IsEmpty(schema) then "" else 97 | "[" & List.TransformAndCombine( 98 | Table.ToRecords(Table.Sort(schema,"Position")), 99 | each Serialize.Identifier(_[Name]) & " = " & _[Kind], 100 | ", ") & 101 | "] "), 102 | 103 | _serialize_record_type = (x) => 104 | let flds = Type.RecordFields(x) 105 | in 106 | if Record.FieldCount(flds)=0 then "record" else 107 | "[" & List.TransformAndCombine( 108 | Record.FieldNames(flds), 109 | (item) => Serialize.Identifier(item) & "=" & _serialize_typename(Record.Field(flds,item)[Type]), 110 | ", ") & 111 | (if Type.IsOpenRecord(x) then ", ..." else "") & 112 | "]", 113 | 114 | _serialize_function_type = (x) => _serialize_function_param_type( 115 | Type.FunctionParameters(x), 116 | Type.FunctionRequiredParameters(x) ) & 117 | " as " & 118 | _serialize_function_return_type(x), 119 | 120 | _serialize_function_param_type = (t,n) => 121 | let 122 | funsig = Table.ToRecords( 123 | Table.TransformColumns( 124 | Table.AddIndexColumn( Record.ToTable( t ), "isOptional", 1 ), 125 | { "isOptional", (x)=> x>n } ) ) 126 | in 127 | "(" & 128 | List.TransformAndCombine( 129 | funsig, 130 | (item)=> 131 | (if item[isOptional] then "optional " else "") & 132 | Serialize.Identifier(item[Name]) & " as " & _serialize_typename(item[Value], true), 133 | ", ") & 134 | ")", 135 | 136 | _serialize_function_return_type = (x) => _serialize_typename(Type.FunctionReturn(x), true), 137 | 138 | Serialize = (x) as text => 139 | if x is binary then try Serialize.Binary(x) otherwise "null /*serialize failed*/" else 140 | if x is date then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 141 | if x is datetime then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 142 | if x is datetimezone then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 143 | if x is duration then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 144 | if x is function then try Serialize.Function(x) otherwise "null /*serialize failed*/" else 145 | if x is list then try Serialize.List(x) otherwise "null /*serialize failed*/" else 146 | if x is logical then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 147 | if x is null then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 148 | if x is number then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 149 | if x is record then try Serialize.Record(x) otherwise "null /*serialize failed*/" else 150 | if x is table then try Serialize.Table(x) otherwise "null /*serialize failed*/" else 151 | if x is text then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 152 | if x is time then try Expression.Constant(x) otherwise "null /*serialize failed*/" else 153 | if x is type then try Serialize.Type(x) otherwise "null /*serialize failed*/" else 154 | "[#_unable_to_serialize_#]" 155 | in 156 | try Serialize(value) otherwise "" 157 | in 158 | [ 159 | LogValue = Diagnostics.LogValue, 160 | LogValue2 = Diagnostics.LogValue2, 161 | LogFailure = Diagnostics.LogFailure, 162 | WrapFunctionResult = Diagnostics.WrapFunctionResult, 163 | WrapHandlers = Diagnostics.WrapHandlers, 164 | ValueToText = Diagnostics.ValueToText 165 | ] -------------------------------------------------------------------------------- /src/SecMgmtInsights/Schema.pqm: -------------------------------------------------------------------------------- 1 | let 2 | Schema.GetEntityFromEntitySet = (entityContainer as table, value as text) => 3 | let 4 | entity = if(Text.Contains(value, "microsoft.graph.")) then List.Last(Text.Split(value, ".")) else if(Text.Contains(value, "Collection(")) then 5 | null 6 | else 7 | let 8 | entitySetValue = Table.SelectRows(entityContainer{0}[EntitySet], each [#"Attribute:Name"] = Text.BeforeDelimiter(value, "(")), 9 | entityValue = if(Table.IsEmpty(entitySetValue)) then 10 | Table.SelectRows(entityContainer{0}[Singleton], each [#"Attribute:Name"] = Text.BeforeDelimiter(value, "(")) 11 | else 12 | entitySetValue, 13 | entity = if(Table.IsEmpty(entitySetValue)) then 14 | Text.Split(entityValue{0}[#"Attribute:Type"], "."){2} 15 | else 16 | Text.Split(entityValue{0}[#"Attribute:EntityType"], "."){2} 17 | in 18 | entity 19 | in 20 | entity, 21 | 22 | Schema.GetEntityFromSingleton = (entityContainer as table, entityType as table, values as list) => 23 | let 24 | singletonTable = entityContainer{0}[Singleton], 25 | singletonValue = Table.SelectRows(singletonTable, each [#"Attribute:Name"] = values{0}), 26 | singleton = Text.Split(singletonValue{0}[#"Attribute:Type"], "."){2}, 27 | 28 | filtered = Table.SelectRows(entityType, each [#"Attribute:Name"] = singleton), 29 | property = if(filtered{0}[Property] <> null) then Table.SelectRows(filtered{0}[Property], each [#"Attribute:Name"] = values{1}) else null, 30 | 31 | entity = if(property <> null and not Table.IsEmpty(property)) then 32 | let 33 | value = Text.Split(property{0}[#"Attribute:Type"], "."){1} 34 | in 35 | value 36 | else 37 | Schema.GetTypeFromNavigationProperty(entityType, singleton, values) 38 | in 39 | entity, 40 | 41 | Schema.GetEntityProperties = (entityType as table, entity as text, tables as list) => 42 | let 43 | entry = Table.SelectRows(entityType, each [#"Attribute:Name"] = entity), 44 | properties = List.InsertRange(tables, 0, entry[Property]), 45 | value = if(entry{0}[#"Attribute:BaseType"] = null) then properties else @Schema.GetEntityProperties(entityType, Text.AfterDelimiter(entry{0}[#"Attribute:BaseType"], "."), properties) 46 | in 47 | value, 48 | 49 | Schema.GetEntityType = (entityContainer as table, entityType as table, value as text) => 50 | let 51 | response = if(Text.Contains(value, "Collection(")) then 52 | let 53 | source = Text.Split(Text.BetweenDelimiters(value, "(", ")"), "."), 54 | output = List.Last(source) 55 | in 56 | output 57 | else if(Text.Contains(Text.Split(value, "/"){0}, "('")) then 58 | let 59 | parts = Text.Split(value, "/"), 60 | removedEntity = if(List.Last(parts) = "$entity") then List.RemoveLastN(parts, 1) else parts, 61 | resourceEntity = Schema.GetEntityFromEntitySet(entityContainer, Text.BeforeDelimiter(removedEntity{0}, "(")), 62 | entity = Schema.GetTypeFromNavigationProperty(entityType, resourceEntity, removedEntity) 63 | in 64 | entity 65 | else 66 | let 67 | parts = Text.Split(value, "/"), 68 | removedEntity = if(List.Last(parts) = "$entity") then List.RemoveLastN(parts, 1) else parts, 69 | entity = if(List.Count(removedEntity) = 1) then Schema.GetEntityFromEntitySet(entityContainer, value) else Schema.GetEntityFromSingleton(entityContainer, entityType, removedEntity) 70 | in 71 | entity 72 | in 73 | response, 74 | 75 | Schema.GetKnownType = (metadata as table, resource as text, optional additionalTypes as text) => 76 | let 77 | entityType = Table.SelectRows(metadata{0}[EntityType], each [#"Attribute:Name"] = resource), 78 | value = if(Table.IsEmpty(entityType)) then Table.SelectRows(metadata{0}[ComplexType], each [#"Attribute:Name"] = resource) else entityType, 79 | 80 | isEnumType = if(Table.IsEmpty(value)) then 81 | let 82 | data = Table.SelectRows(metadata{0}[EnumType], each [#"Attribute:Name"] = resource), 83 | output = if(Table.IsEmpty(data)) then false else true 84 | in 85 | output 86 | else 87 | false, 88 | 89 | GetData = (row as any) => 90 | let 91 | data = if(Record.HasFields(row, "Attribute:Nullable") and row[#"Attribute:Nullable"] <> null and row[#"Attribute:Nullable"] = true) then 92 | [Entity = row[#"Attribute:Name"], Type = " nullable " & GetDataType(row[#"Attribute:Type"])] 93 | else 94 | [Entity = row[#"Attribute:Name"], Type = GetDataType(row[#"Attribute:Type"])] 95 | in 96 | data, 97 | 98 | GetDataType = (value) as text => 99 | if (value = "Edm.Boolean") then "logical" else if (value = "Edm.DateTimeOffset") then "datetimezone" 100 | else if (value = "Edm.Int32") then "number" else if (value = "Edm.Int64") then "number" else if(value = "Edm.String") then "text" else "any", 101 | 102 | output = if (isEnumType) then 103 | if(additionalTypes = null or additionalTypes = "") then Expression.Evaluate("type table [value = text]") else Expression.Evaluate("type table [" & additionalTypes & ", value = text]") 104 | else if(Table.IsEmpty(value)) then 105 | null 106 | else 107 | let 108 | // Build the schema table based on data from the property table 109 | propertyTableList = Schema.GetEntityProperties(if(Table.IsEmpty(entityType)) then metadata{0}[ComplexType] else metadata{0}[EntityType], resource, {}), 110 | convertedTable = Table.FromList(propertyTableList, Splitter.SplitByNothing(), null, null, ExtraValues.Error), 111 | propertyTable = Table.ExpandTableColumn(convertedTable, "Column1", {"Attribute:Name", "Attribute:Type", "Attribute:Nullable"}, {"Attribute:Name", "Attribute:Type", "Attribute:Nullable"}), 112 | addedSchema = Table.AddColumn(propertyTable, "schema", each GetData(_)), 113 | schemaTable = Table.FromList(addedSchema[schema], Splitter.SplitByNothing(), null, null, ExtraValues.Error), 114 | expandedColumn = Table.ExpandRecordColumn(schemaTable, "Column1", {"Entity", "Type"}, {"Entity", "Type"}), 115 | 116 | custom = Table.AddColumn(expandedColumn, "Expression", each [Entity] & " = " & [Type]), 117 | output = if(additionalTypes = null or additionalTypes = "") then Expression.Evaluate("type table [" & Text.Combine(custom[Expression], ", ") & "]") else Expression.Evaluate("type table [" & additionalTypes & "," & Text.Combine(custom[Expression], ", ") & "]") 118 | in 119 | output 120 | in 121 | output, 122 | 123 | Schema.GetTypeFromNavigationProperty = (entityType as table, entityName as text, values as list) => 124 | let 125 | filtered = Table.SelectRows(entityType, each [#"Attribute:Name"] = entityName), 126 | 127 | dataTable = if(filtered{0}[NavigationProperty] = null) then 128 | Table.SelectRows(filtered{0}[Property], each [#"Attribute:Name"] = Text.BeforeDelimiter(values{1}, "(")) 129 | else 130 | Table.SelectRows(filtered{0}[NavigationProperty], each [#"Attribute:Name"] = Text.BeforeDelimiter(values{1}, "(")), 131 | 132 | resource = dataTable{0}[#"Attribute:Type"], 133 | entity = if(Text.Contains(resource, "(")) then Text.AfterDelimiter(Text.BetweenDelimiters(resource, "(", ")"), ".") else Text.AfterDelimiter(resource, "."), 134 | 135 | value = if(List.Count(values) <= 2) then entity else @Schema.GetTypeFromNavigationProperty(entityType, entity, List.RemoveFirstN(values, 1)) 136 | in 137 | value 138 | in 139 | [ 140 | GetKnownType = Schema.GetKnownType 141 | ] -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | SecMgmtInsights 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Content 72 | 73 | 74 | Content 75 | 76 | 77 | Content 78 | 79 | 80 | Code 81 | 82 | 83 | Content 84 | 85 | 86 | Content 87 | 88 | 89 | Content 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights.pq: -------------------------------------------------------------------------------- 1 | section SecMgmtInsights; 2 | 3 | // Global variables 4 | 5 | authorize_uri = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize"; 6 | client_id = Text.FromBinary(Extension.Contents("client_id")); 7 | graph_endpoint = "https://graph.microsoft.com"; 8 | logout_uri = "https://login.microsoftonline.com/logout.srf"; 9 | redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html"; 10 | token_uri = "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"; 11 | version = "2.2-preview-1"; 12 | 13 | // Data Source Kind description 14 | 15 | SecMgmtInsights = [ 16 | Authentication = [ 17 | OAuth = [ 18 | FinishLogin = FinishLogin, 19 | Logout = Logout, 20 | Refresh = Refresh, 21 | StartLogin = StartLogin 22 | ] 23 | ], 24 | Label = Extension.LoadString("DataSourceLabel"), 25 | TestConnection = (dataSourcePath) => {"SecMgmtInsights.Contents"} 26 | ]; 27 | 28 | // Data Source UI publishing description 29 | 30 | SecMgmtInsights.Publish = [ 31 | Beta = true, 32 | Category = "Other", 33 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 34 | LearnMoreUrl = "https://github.com/microsoft/secmgmt-insights-connector", 35 | SourceImage = SecMgmtInsights.Icons, 36 | SourceTypeImage = SecMgmtInsights.Icons 37 | ]; 38 | 39 | SecMgmtInsights.Icons = [ 40 | Icon16 = { Extension.Contents("SecMgmtInsights16.png"), Extension.Contents("SecMgmtInsights20.png"), Extension.Contents("SecMgmtInsights24.png"), Extension.Contents("SecMgmtInsights32.png") }, 41 | Icon32 = { Extension.Contents("SecMgmtInsights32.png"), Extension.Contents("SecMgmtInsights40.png"), Extension.Contents("SecMgmtInsights48.png"), Extension.Contents("SecMgmtInsights64.png") } 42 | ]; 43 | 44 | // Data Source type 45 | 46 | SecMgmtInsightsType = type function ( 47 | optional tenants as (type list meta [ 48 | Documentation.FieldCaption = "List of tenants", 49 | Documentation.FieldDescription = "List of tenant identifiers if left blank the list of tenants will be obtained using contracts feature of Microsoft Graph" 50 | ])) 51 | as table meta [ 52 | Documentation.Name = "Security and Management Insights", 53 | Documentation.LongDescription = "Security and Management Insights", 54 | Documentation.Icon = Extension.Contents("SecMgmtInsights32.png") 55 | ]; 56 | 57 | // Authentication 58 | 59 | FinishLogin = (context, callbackUri, state) => 60 | let 61 | parts = Uri.Parts(callbackUri)[Query], 62 | result = if (Record.HasFields(parts, {"error", "error_description"})) then 63 | error Error.Record(parts[error], parts[error_description], parts) 64 | else 65 | TokenMethod(token_uri, "authorization_code", "code", parts[code]) 66 | in 67 | result; 68 | 69 | Logout = (token) => logout_uri; 70 | 71 | Refresh = (resourceUrl, refresh_token) => TokenMethod(token_uri, "refresh_token", "refresh_token", refresh_token); 72 | 73 | StartLogin = (resourceUrl, state, display) => 74 | let 75 | authorizeUrl = authorize_uri & "?" & Uri.BuildQueryString([ 76 | client_id = client_id, 77 | redirect_uri = redirect_uri, 78 | state = state, 79 | scope = "offline_access https://graph.microsoft.com/.default", 80 | response_type = "code", 81 | response_mode = "query", 82 | login = "login", 83 | acr_values = "urn:microsoft:policies:mfa" 84 | ]) 85 | in 86 | [ 87 | LoginUri = authorizeUrl, 88 | CallbackUri = redirect_uri, 89 | WindowHeight = 860, 90 | WindowWidth = 1024, 91 | Context = null 92 | ]; 93 | 94 | TokenMethod = (tokenUri, grantType, tokenField, parameter, optional scope as text) => 95 | let 96 | queryString = [ 97 | client_id = client_id, 98 | scope = if (scope <> null) then scope else "offline_access https://graph.microsoft.com/.default", 99 | grant_type = grantType, 100 | redirect_uri = redirect_uri 101 | ], 102 | queryWithCode = Record.AddField(queryString, tokenField, parameter), 103 | 104 | tokenResponse = Web.Contents(tokenUri, [ 105 | Content = Text.ToBinary(Uri.BuildQueryString(queryWithCode)), 106 | Headers = [ 107 | #"Content-type" = "application/x-www-form-urlencoded", 108 | #"Accept" = "application/json" 109 | ], 110 | ManualStatusHandling = {400, 401, 403} 111 | ]), 112 | body = Json.Document(tokenResponse), 113 | result = if (Record.HasFields(body, {"error", "error_description"})) then 114 | error Error.Record(body[error], body[error_description], body) 115 | else 116 | body 117 | in 118 | result; 119 | 120 | Token.GetAccessToken = (optional tenantId as text, optional scope as text) => 121 | let 122 | authResult = if (tenantId <> null) then 123 | TokenMethod("https://login.microsoftonline.com/" & tenantId & "/oauth2/v2.0/token", "refresh_token", "refresh_token", Extension.CurrentCredential()[refresh_token], scope) 124 | else 125 | TokenMethod(token_uri, "refresh_token", "refresh_token", Extension.CurrentCredential()[refresh_token], scope) 126 | in 127 | authResult[access_token]; 128 | 129 | // Azure Active Directory 130 | 131 | SecMgmtInsights.ConditionalAccessPolicies = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 132 | let 133 | result = Request.GetData("conditionalAccessPolicy", schemaOnly, tenants, "beta", true, "/beta/identity/conditionalAccess/policies", query, schema, metadata) 134 | in 135 | result; 136 | 137 | SecMgmtInsights.Contracts = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 138 | let 139 | result = Request.GetData("contract", schemaOnly, {}, "v1.0", true, "/v1.0/contracts", query, schema, metadata) 140 | in 141 | result; 142 | 143 | SecMgmtInsights.CredentialUserRegistrationDetails = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 144 | let 145 | result = Request.GetData("credentialUserRegistrationDetails", schemaOnly, tenants, "beta", true, "/beta/reports/credentialUserRegistrationDetails", query, schema, metadata) 146 | in 147 | result; 148 | 149 | SecMgmtInsights.Devices = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 150 | let 151 | result = Request.GetData("device", schemaOnly, tenants, "v1.0", true, "/v1.0/devices", query, schema, metadata) 152 | in 153 | result; 154 | 155 | SecMgmtInsights.LicenseDetails = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 156 | let 157 | result = Request.GetData("licenseDetails", schemaOnly, tenants, "v1.0", true, "/v1.0/users?$select=id", query, null, metadata, "tenantId = text, userId = text", (input as table) => 158 | let 159 | renamedColumns = Table.RenameColumns(input, {"id", "userId"}, MissingField.Ignore), 160 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/users/" & [userId] & "/licenseDetails"), 161 | output = Rest.Feed(requests, true) 162 | in 163 | output) 164 | in 165 | result; 166 | 167 | SecMgmtInsights.IdentitySecurityDefaultsEnforcementPolicy = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 168 | let 169 | removedTopParameter = if (query <> null and Text.Contains(query, "top")) then null else query, 170 | result = Request.GetData("identitySecurityDefaultsEnforcementPolicy", schemaOnly, tenants, "v1.0", false, "/v1.0/policies/identitySecurityDefaultsEnforcementPolicy", removedTopParameter, schema, metadata) 171 | in 172 | result; 173 | 174 | SecMgmtInsights.SignIns = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 175 | let 176 | result = Request.GetData("signIn", schemaOnly, tenants, "beta", true, "/beta/auditLogs/signIns", query, schema, metadata) 177 | in 178 | result; 179 | 180 | SecMgmtInsights.SubscribedSkus = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 181 | let 182 | removedTopParameter = if (query <> null and Text.Contains(query, "top")) then null else query, 183 | result = Request.GetData("subscribedSku", schemaOnly, tenants, "v1.0", true, "/v1.0/subscribedSkus", removedTopParameter, schema, metadata) 184 | in 185 | result; 186 | 187 | SecMgmtInsights.Users = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 188 | let 189 | result = Request.GetData("user", schemaOnly, tenants, "beta", true, "/beta/users", query, schema, metadata) 190 | in 191 | result; 192 | 193 | // Device Management 194 | 195 | SecMgmtInsights.AndroidManagedStoreAccountEnterpriseSettings = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 196 | let 197 | result = Request.GetData("androidManagedStoreAccountEnterpriseSettings", schemaOnly, tenants, "beta", false, "/beta/deviceManagement/androidManagedStoreAccountEnterpriseSettings", query, schema, metadata) 198 | in 199 | result; 200 | 201 | SecMgmtInsights.AppleMDMPushCertificate = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 202 | let 203 | result = Request.GetData("dataSharingConsent", schemaOnly, tenants, "beta", false, "/beta/deviceManagement/appleMDMPushCertificate", query, schema, metadata) 204 | in 205 | result; 206 | 207 | SecMgmtInsights.DetectedApps = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 208 | let 209 | result = Request.GetData("detectedApp", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text", (input as table) => 210 | let 211 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 212 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/detectedApps"), 213 | output = Rest.Feed(requests, true) 214 | in 215 | output) 216 | in 217 | result; 218 | 219 | SecMgmtInsights.DetectedMalwareState = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 220 | let 221 | result = Request.GetData("windowsDeviceMalwareState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id&$filter=(((deviceType eq 'desktop') or (deviceType eq 'windowsRT') or (deviceType eq 'winEmbedded') or (deviceType eq 'surfaceHub') or (deviceType eq 'desktop') or (deviceType eq 'windowsRT') or (deviceType eq 'winEmbedded') or (deviceType eq 'surfaceHub') or (deviceType eq 'windowsPhone') or (deviceType eq 'holoLens')))", query, null, metadata, "tenantId = text, deviceId = text", (input as table) => 222 | let 223 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 224 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/windowsProtectionState/detectedMalwareState"), 225 | output = Rest.Feed(requests, true) 226 | in 227 | output) 228 | in 229 | result; 230 | 231 | SecMgmtInsights.DeviceCompliancePolicies = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 232 | let 233 | result = Request.GetData("deviceCompliancePolicy", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/deviceCompliancePolicies?$select=id,displayName,createdDateTime,description,lastModifiedDateTime,roleScopeTagIds,version", query, schema, metadata) 234 | in 235 | result; 236 | 237 | DeviceComplianceSettingStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 238 | let 239 | result = Request.GetData("deviceCompliancePolicySettingState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text, policyId = text", (input as table) => 240 | let 241 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 242 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/deviceManagement/managedDevices/" & [deviceId] & "/deviceCompliancePolicyStates?$select=id"), 243 | policies = Rest.Feed(requests, true), 244 | renamePolicyIdColumn = Table.RenameColumns(policies, {"id", "policyId"}, MissingField.Ignore), 245 | stateRequests = Table.AddColumn(renamePolicyIdColumn, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/deviceCompliancePolicyStates/" & [policyId] & "/settingStates"), 246 | output = Rest.Feed(stateRequests, true) 247 | in 248 | output) 249 | in 250 | result; 251 | 252 | SecMgmtInsights.DeviceCompliancePolicySettingStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 253 | let 254 | result = Request.GetData("deviceCompliancePolicySettingState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text, policyId = text", (input as table) => 255 | let 256 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 257 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/deviceManagement/managedDevices/" & [deviceId] & "/deviceCompliancePolicyStates?$select=id"), 258 | policies = Rest.Feed(requests, true), 259 | renamePolicyIdColumn = Table.RenameColumns(policies, {"id", "policyId"}, MissingField.Ignore), 260 | stateRequests = Table.AddColumn(renamePolicyIdColumn, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/deviceCompliancePolicyStates/" & [policyId] & "/settingStates"), 261 | output = Rest.Feed(stateRequests, true) 262 | in 263 | output) 264 | in 265 | result; 266 | 267 | SecMgmtInsights.DeviceCompliancePolicyStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 268 | let 269 | result = Request.GetData("deviceCompliancePolicyState", schemaOnly, tenants, "v1.0", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text", (input as table) => 270 | let 271 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 272 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/deviceManagement/managedDevices/" & [deviceId] & "/deviceCompliancePolicyStates"), 273 | output = Rest.Feed(requests, true) 274 | in 275 | output) 276 | in 277 | result; 278 | 279 | SecMgmtInsights.DeviceConfigurationProfiles = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 280 | let 281 | result = Request.GetData("deviceConfiguration", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/deviceConfigurations?$select=id,displayName,createdDateTime,description,lastModifiedDateTime,roleScopeTagIds,supportsScopeTags,deviceManagementApplicabilityRuleOsEdition,deviceManagementApplicabilityRuleOsVersion,deviceManagementApplicabilityRuleDeviceMode,version", query, schema, metadata) 282 | in 283 | result; 284 | 285 | DeviceConfigurationDeviceSettingStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 286 | let 287 | result = Request.GetData("deviceConfigurationSettingState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text, policyId = text", (input as table) => 288 | let 289 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 290 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/deviceManagement/managedDevices/" & [deviceId] & "/deviceConfigurationStates?$select=id"), 291 | policies = Rest.Feed(requests, true), 292 | renamePolicyIdColumn = Table.RenameColumns(policies, {"id", "policyId"}, MissingField.Ignore), 293 | stateRequests = Table.AddColumn(renamePolicyIdColumn, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/deviceConfigurationStates/" & [policyId] & "/settingStates"), 294 | output = Rest.Feed(stateRequests, true) 295 | in 296 | output) 297 | in 298 | result; 299 | 300 | SecMgmtInsights.DeviceConfigurationProfileSettingStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 301 | let 302 | result = Request.GetData("deviceConfigurationSettingState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text, policyId = text", (input as table) => 303 | let 304 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 305 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/deviceManagement/managedDevices/" & [deviceId] & "/deviceConfigurationStates?$select=id"), 306 | policies = Rest.Feed(requests, true), 307 | renamePolicyIdColumn = Table.RenameColumns(policies, {"id", "policyId"}, MissingField.Ignore), 308 | stateRequests = Table.AddColumn(renamePolicyIdColumn, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/deviceConfigurationStates/" & [policyId] & "/settingStates"), 309 | output = Rest.Feed(stateRequests, true) 310 | in 311 | output) 312 | in 313 | result; 314 | 315 | SecMgmtInsights.DeviceConfigurationProfileStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 316 | let 317 | result = Request.GetData("deviceConfigurationState", schemaOnly, tenants, "v1.0", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text", (input as table) => 318 | let 319 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 320 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/v1.0/deviceManagement/managedDevices/" & [deviceId] & "/deviceConfigurationStates"), 321 | output = Rest.Feed(requests, true) 322 | in 323 | output) 324 | in 325 | result; 326 | 327 | SecMgmtInsights.DeviceManagement = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 328 | let 329 | removedTopParameter = if (query <> null and Text.Contains(query, "top")) then null else query, 330 | result = Request.GetData("deviceManagement", schemaOnly, tenants, "beta", false, "/beta/deviceManagement", removedTopParameter, schema, metadata) 331 | in 332 | result; 333 | 334 | SecMgmtInsights.Intents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 335 | let 336 | result = Request.GetData("deviceManagementIntent", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/intents", query, schema, metadata) 337 | in 338 | result; 339 | 340 | SecMgmtInsights.ManagedDeviceOverview = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 341 | let 342 | removedTopParameter = if (query <> null and Text.Contains(query, "top")) then null else query, 343 | result = Request.GetData("managedDeviceOverview", schemaOnly, tenants, "beta", false, "/beta/deviceManagement/managedDeviceOverview", removedTopParameter, schema, metadata) 344 | in 345 | result; 346 | 347 | SecMgmtInsights.ManagedDevices = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 348 | let 349 | result = Request.GetData("managedDevice", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/managedDevices", query, schema, metadata) 350 | in 351 | result; 352 | 353 | SecMgmtInsights.MobileAppDeviceStatuses = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 354 | let 355 | result = Request.GetData("mobileAppInstallStatus", schemaOnly, tenants, "beta", true, "/beta/deviceAppManagement/mobileApps?$select=id&filter=isAssigned+eq+true", query, null, metadata, "tenantId = text, mobileAppId = text", (input as table) => 356 | let 357 | renamedColumns = Table.RenameColumns(input, {"id", "mobileAppId"}, MissingField.Ignore), 358 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceAppManagement/mobileApps/" & [mobileAppId] & "/deviceStatuses"), 359 | output = Rest.Feed(requests, true) 360 | in 361 | output) 362 | in 363 | result; 364 | 365 | SecMgmtInsights.MobileAppUserStatuses = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 366 | let 367 | result = Request.GetData("userAppInstallStatus", schemaOnly, tenants, "beta", true, "/beta/deviceAppManagement/mobileApps?$select=id&filter=isAssigned+eq+true", query, null, metadata, "tenantId = text, mobileAppId = text", (input as table) => 368 | let 369 | renamedColumns = Table.RenameColumns(input, {"id", "mobileAppId"}, MissingField.Ignore), 370 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceAppManagement/mobileApps/" & [mobileAppId] & "/userStatuses"), 371 | output = Rest.Feed(requests, true) 372 | in 373 | output) 374 | in 375 | result; 376 | 377 | SecMgmtInsights.MobileApps = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 378 | let 379 | result = Request.GetData("mobileApp", schemaOnly, tenants, "beta", true, "/beta/deviceAppManagement/mobileApps?$select=id,displayName,lastModifiedDateTime,roleScopeTagIds", query, schema, metadata) 380 | in 381 | result; 382 | 383 | SecMgmtInsights.RemoteActionAudits = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 384 | let 385 | result = Request.GetData("remoteActionAudit", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/remoteActionAudits", query, schema, metadata) 386 | in 387 | result; 388 | 389 | SecMgmtInsights.SecurityBaselineSettingStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 390 | let 391 | result = Request.GetData("securityBaselineSettingState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text, templateId = text", (input as table) => 392 | let 393 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 394 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/securityBaselineStates?$select=id"), 395 | policies = Rest.Feed(requests, true), 396 | renamePolicyIdColumn = Table.RenameColumns(policies, {"id", "templateId"}, MissingField.Ignore), 397 | stateRequests = Table.AddColumn(renamePolicyIdColumn, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/securityBaselineStates/" & [templateId] & "/settingStates"), 398 | output = Rest.Feed(stateRequests, true) 399 | in 400 | output) 401 | in 402 | result; 403 | 404 | SecMgmtInsights.SecurityBaselineStates = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 405 | let 406 | result = Request.GetData("securityBaselineState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id", query, null, metadata, "tenantId = text, deviceId = text", (input as table) => 407 | let 408 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 409 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/securityBaselineStates"), 410 | output = Rest.Feed(requests, true) 411 | in 412 | output) 413 | in 414 | result; 415 | 416 | SecMgmtInsights.SoftwareUpdateStatusSummary = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 417 | let 418 | removedTopParameter = if (query <> null and Text.Contains(query, "top")) then null else query, 419 | result = Request.GetData("softwareUpdateStatusSummary", schemaOnly, tenants, "beta", false, "/beta/deviceManagement/softwareUpdateStatusSummary", removedTopParameter, schema, metadata) 420 | in 421 | result; 422 | 423 | SecMgmtInsights.SubscriptionState = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 424 | let 425 | result = Request.GetData("deviceManagementSubscriptionState", schemaOnly, tenants, "beta", false, "/beta/deviceManagement/subscriptionState", query, schema, metadata) 426 | in 427 | result; 428 | 429 | SecMgmtInsights.UserExperienceAnalyticsDevicePerformance = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 430 | let 431 | result = Request.GetData("userExperienceAnalyticsDevicePerformance", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/userExperienceAnalyticsDevicePerformance", query, schema, metadata) 432 | in 433 | result; 434 | 435 | SecMgmtInsights.UserExperienceAnalyticsDeviceStartupProcessPerformance = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 436 | let 437 | result = Request.GetData("userExperienceAnalyticsDeviceStartupProcessPerformance", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/userExperienceAnalyticsDeviceStartupProcessPerformance", query, schema, metadata) 438 | in 439 | result; 440 | 441 | SecMgmtInsights.WindowsAutopilotDeviceIdentities = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 442 | let 443 | result = Request.GetData("windowsAutopilotDeviceIdentity", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/windowsAutopilotDeviceIdentities", query, schema, metadata) 444 | in 445 | result; 446 | 447 | SecMgmtInsights.WindowsAutopilotProfiles = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 448 | let 449 | result = Request.GetData("windowsAutopilotDeploymentProfile", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/windowsAutopilotDeploymentProfiles", query, schema, metadata) 450 | in 451 | result; 452 | 453 | SecMgmtInsights.WindowsAutopilotSettings = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 454 | let 455 | result = Request.GetData("windowsAutopilotSettings", schemaOnly, tenants, "beta", false, "/beta/deviceManagement/windowsAutopilotSettings", query, schema, metadata) 456 | in 457 | result; 458 | 459 | SecMgmtInsights.WindowsMalwareInformation = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 460 | let 461 | result = Request.GetData("windowsMalwareInformation", schemaOnly, tenants, "beta", true, "/beta/deviceManagement/windowsMalwareInformation", query, schema, metadata) 462 | in 463 | result; 464 | 465 | SecMgmtInsights.WindowsProtectionState = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 466 | let 467 | result = Request.GetData("windowsProtectionState", schemaOnly, tenants, "beta", true, "/v1.0/deviceManagement/managedDevices?$select=id&$filter=(((deviceType eq 'desktop') or (deviceType eq 'windowsRT') or (deviceType eq 'winEmbedded') or (deviceType eq 'surfaceHub') or (deviceType eq 'desktop') or (deviceType eq 'windowsRT') or (deviceType eq 'winEmbedded') or (deviceType eq 'surfaceHub') or (deviceType eq 'windowsPhone') or (deviceType eq 'holoLens')))", query, null, metadata, "tenantId = text, deviceId = text", (input as table) => 468 | let 469 | renamedColumns = Table.RenameColumns(input, {"id", "deviceId"}, MissingField.Ignore), 470 | requests = Table.AddColumn(renamedColumns, "secMgmtInsightsRequest", each graph_endpoint & "/beta/deviceManagement/managedDevices/" & [deviceId] & "/windowsProtectionState"), 471 | output = Rest.Feed(requests, true, schema) 472 | in 473 | output) 474 | in 475 | result; 476 | 477 | // Entities 478 | 479 | EntityTable = #table({"Entity", "Action"}, { 480 | { "ActivitySubscriptions", SecMgmtInsights.ActivitySubscriptions }, 481 | { "Alerts", SecMgmtInsights.Alerts }, 482 | { "AndroidManagedStoreAccountEnterpriseSettings", SecMgmtInsights.AndroidManagedStoreAccountEnterpriseSettings }, 483 | { "AppleMDMPushCertificate", SecMgmtInsights.AppleMDMPushCertificate }, 484 | { "AzureActiveDirectoryEvents", SecMgmtInsights.AzureActiveDirectoryEvents }, 485 | { "ComplianceCaseEvents", SecMgmtInsights.ComplianceCaseEvents }, 486 | { "ConditionalAccessPolicies", SecMgmtInsights.ConditionalAccessPolicies }, 487 | { "Contracts", SecMgmtInsights.Contracts }, 488 | { "Controls", SecMgmtInsights.Controls }, 489 | { "CredentialUserRegistrationDetails", SecMgmtInsights.CredentialUserRegistrationDetails }, 490 | { "DetectedApps", SecMgmtInsights.DetectedApps }, 491 | { "DetectedMalwareState", SecMgmtInsights.DetectedMalwareState }, 492 | { "DeviceActions", DeviceActions }, 493 | { "DeviceCompliancePolicies", SecMgmtInsights.DeviceCompliancePolicies }, 494 | { "DeviceCompliancePolicySettingStates", SecMgmtInsights.DeviceCompliancePolicySettingStates }, 495 | { "DeviceCompliancePolicyStates", SecMgmtInsights.DeviceCompliancePolicyStates }, 496 | { "DeviceConfigurationProfiles", SecMgmtInsights.DeviceConfigurationProfiles }, 497 | { "DeviceConfigurationProfileSettingStates", SecMgmtInsights.DeviceConfigurationProfileSettingStates }, 498 | { "DeviceConfigurationProfileStates", SecMgmtInsights.DeviceConfigurationProfileStates }, 499 | { "DeviceManagement", SecMgmtInsights.DeviceManagement }, 500 | { "Devices", SecMgmtInsights.Devices }, 501 | { "DlpEvents", SecMgmtInsights.DlpEvents }, 502 | { "ExchangeEvents", SecMgmtInsights.ExchangeEvents}, 503 | { "GeneralEvents", SecMgmtInsights.GeneralEvents }, 504 | { "HygieneTenantEvents", SecMgmtInsights.HygieneTenantEvents }, 505 | { "IdentitySecurityDefaultsEnforcementPolicy", SecMgmtInsights.IdentitySecurityDefaultsEnforcementPolicy }, 506 | { "Intents", SecMgmtInsights.Intents }, 507 | { "LicenseDetails", SecMgmtInsights.LicenseDetails }, 508 | { "MailboxUsageDetail", SecMgmtInsights.MailboxUsageDetail }, 509 | { "ManagedDeviceOverview", SecMgmtInsights.ManagedDeviceOverview }, 510 | { "ManagedDevices", SecMgmtInsights.ManagedDevices }, 511 | { "MobileAppDeviceStatuses", SecMgmtInsights.MobileAppDeviceStatuses }, 512 | { "MobileAppUserStatuses", SecMgmtInsights.MobileAppUserStatuses }, 513 | { "MobileApps", SecMgmtInsights.MobileApps }, 514 | { "Office365ActivationsUserDetail", SecMgmtInsights.Office365ActivationsUserDetail }, 515 | { "Office365ActiveUserDetails", SecMgmtInsights.Office365ActiveUserDetails }, 516 | { "Office365ServicesUserCounts", SecMgmtInsights.Office365ServicesUserCounts }, 517 | { "OneDriveUsageAccountDetail", SecMgmtInsights.OneDriveUsageAccountDetail }, 518 | { "QuarantineReleaseMessage", SecMgmtInsights.QuarantineReleaseMessage }, 519 | { "RemoteActionAudits", SecMgmtInsights.RemoteActionAudits }, 520 | { "RiskDetections", SecMgmtInsights.RiskDetections }, 521 | { "SafeAttachmentEvents", SecMgmtInsights.SafeAttachmentEvents }, 522 | { "SafeLinksEvents", SecMgmtInsights.SafeLinksEvents }, 523 | { "SecureScore", SecMgmtInsights.SecureScore }, 524 | { "SecureScoreControlProfiles", SecMgmtInsights.SecureScoreControlProfiles }, 525 | { "SecurityBaselineSettingStates", SecMgmtInsights.SecurityBaselineSettingStates }, 526 | { "SecurityBaselineStates", SecMgmtInsights.SecurityBaselineStates }, 527 | { "SecurityComplianceAlerts", SecMgmtInsights.SecurityComplianceAlerts }, 528 | { "ServiceCurrentStatus", SecMgmtInsights.ServiceCurrentStatus }, 529 | { "ServiceHistoricalStatus", SecMgmtInsights.ServiceHistoricalStatus }, 530 | { "ServiceMessages", SecMgmtInsights.ServiceMessages }, 531 | { "SharePointEvents", SecMgmtInsights.SharePointEvents }, 532 | { "SharePointSiteUsageDetail", SecMgmtInsights.SharePointSiteUsageDetail }, 533 | { "SignIns", SecMgmtInsights.SignIns }, 534 | { "SoftwareUpdateStatusSummary", SecMgmtInsights.SoftwareUpdateStatusSummary }, 535 | { "SubmissionEvents", SecMgmtInsights.SubmissionEvents }, 536 | { "SubscribedSkus", SecMgmtInsights.SubscribedSkus }, 537 | { "SubscriptionState", SecMgmtInsights.SubscriptionState }, 538 | { "TeamsUserActivityUserDetail", SecMgmtInsights.TeamsUserActivityUserDetail }, 539 | { "UserExperienceAnalyticsDevicePerformance", SecMgmtInsights.UserExperienceAnalyticsDevicePerformance }, 540 | { "UserExperienceAnalyticsDeviceStartupProcessPerformance", SecMgmtInsights.UserExperienceAnalyticsDeviceStartupProcessPerformance }, 541 | { "Users", SecMgmtInsights.Users }, 542 | { "WindowsAutopilotDeviceIdentities", SecMgmtInsights.WindowsAutopilotDeviceIdentities }, 543 | { "WindowsAutopilotProfiles", SecMgmtInsights.WindowsAutopilotProfiles }, 544 | { "WindowsAutopilotSettings", SecMgmtInsights.WindowsAutopilotSettings }, 545 | { "WindowsMalwareInformation", SecMgmtInsights.WindowsMalwareInformation }, 546 | { "WindowsProtectionState", SecMgmtInsights.WindowsProtectionState }, 547 | { "YammerActivityUserDetail", SecMgmtInsights.YammerActivityUserDetail } 548 | }); 549 | 550 | GetActionForEntity = (entity as text) => 551 | try 552 | EntityTable{[Entity = entity]}[Action] 553 | otherwise 554 | let 555 | message = Text.Format("Could not find entity: '#{0}'", {entity}) 556 | in 557 | Diagnostics.Trace(TraceLevel.Error, message, () => error message, true); 558 | 559 | // Extensions 560 | 561 | Diagnostics = Extension.LoadFunction("Diagnostics.pqm"); 562 | 563 | Diagnostics.LogValue = Diagnostics[LogValue]; 564 | Diagnostics.LogFailure = Diagnostics[LogFailure]; 565 | Diagnostics.WrapHandlers = Diagnostics[WrapHandlers]; 566 | 567 | Extension.LoadFunction = (name as text) => 568 | let 569 | binary = Extension.Contents(name), 570 | asText = Text.FromBinary(binary) 571 | in 572 | Expression.Evaluate(asText, #shared); 573 | 574 | Schema = Extension.LoadFunction("Schema.pqm"); 575 | 576 | Schema.GetKnownType = Schema[GetKnownType]; 577 | 578 | Table.ChangeType = Extension.LoadFunction("Table.ChangeType.pqm"); 579 | Table.GenerateByPage = Extension.LoadFunction("Table.GenerateByPage.pqm"); 580 | Table.ToNavigationTable = Extension.LoadFunction("Table.ToNavigationTable.pqm"); 581 | 582 | // GitHub 583 | 584 | SecMgmtInsights.Controls = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 585 | let 586 | output = if(schemaOnly) then type table[tenantId = text, expectedValue = text, id = text, info = text, resource = text, #"type" = text] else 587 | let 588 | data = GitHub.GetContent("https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/master/controls/recommended.json"), 589 | controls = Table.FromList(data[controls], Splitter.SplitByNothing(), null, null, ExtraValues.Error), 590 | expandRecord = Table.ExpandRecordColumn(controls, "Column1", {"expectedValue", "id", "info", "resource", "tenantFilter", "type"}, {"expectedValue", "id", "info", "resource", "tenantFilter", "type"}), 591 | 592 | GetControls = (tenantId as text) => 593 | let 594 | source = Table.SelectRows(expandRecord, each (List.Contains([tenantFilter], tenantId) = false)) 595 | in 596 | source, 597 | 598 | source = Table.FromList(tenants, Splitter.SplitByNothing(), {"tenantId"}), 599 | appliedControls = Table.AddColumn(source, "Custom", each GetControls([tenantId])), 600 | expandcustom = Table.ExpandTableColumn(appliedControls, "Custom", {"expectedValue", "id", "info", "resource", "type"}, {"expectedValue", "id", "info", "resource", "type"}) 601 | in 602 | expandcustom 603 | in 604 | output; 605 | 606 | DeviceActions = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 607 | let 608 | GetActions = (input as table, expectedValue as text, id as text, resource as text, tenantId as text) => 609 | let 610 | value = if(Table.HasColumns(input, "policyId")) then 611 | Table.SelectColumns(input, {"tenantId", "deviceId", "policyId", "setting", "state", "currentValue", "errorCode", "errorDescription" }) 612 | else 613 | let 614 | appended = Table.AddColumn(input, "Column1", each [policyId = null, setting = null, state = null, currentValue = null, errorCode = null, errorDescription = null]), 615 | expanded = Table.ExpandRecordColumn(appended, "Column1", {"policyId", "setting", "state", "currentValue", "errorCode", "errorDescription"}) 616 | in 617 | expanded, 618 | 619 | filterRows = Table.SelectRows(value, each ([setting] = id and [state] <> "compliant" and [tenantId] = tenantId)) 620 | in 621 | filterRows, 622 | 623 | GetPortalLink = (resource as text, tenantId as text) => 624 | let 625 | source = if resource = "deviceCompliancePolicy" then "https://endpoint.microsoft.com/" & tenantId & "/#blade/Microsoft_Intune_DeviceSettings/DevicesComplianceMenu/policies" else "https://endpoint.microsoft.com/" & tenantId & "/#blade/Microsoft_Intune_DeviceSettings/DevicesMenu/configurationProfiles" 626 | in 627 | source, 628 | 629 | output = if(schemaOnly) then 630 | type table[tenantId = text, deviceId = text, #"type" = text, policyId = text, id = text, resource = text, expectedValue = text, currentValue = text, errorCode = number, errorDescription = text, state = text, info = text, portal = text] 631 | else 632 | let 633 | controls = SecMgmtInsights.Controls(tenants, false), 634 | deviceComplianceControls = Table.SelectRows(controls, each [resource] = "deviceCompliancePolicy"), 635 | deviceConfigurationControls = Table.SelectRows(controls, each [resource] = "deviceConfigurationPolicy"), 636 | 637 | deviceCompliancePolicySettingStates = DeviceComplianceSettingStates(tenants, false), 638 | deviceConfigurationProfileSettingStates = DeviceConfigurationDeviceSettingStates(tenants, false), 639 | 640 | deviceComplianceActions = Table.AddColumn(deviceComplianceControls, "action", each GetActions(deviceCompliancePolicySettingStates, [expectedValue], [id], [resource], [tenantId])), 641 | deviceConfigurationActions = Table.AddColumn(deviceConfigurationControls, "action", each GetActions(deviceConfigurationProfileSettingStates, [expectedValue], [id], [resource], [tenantId])), 642 | 643 | combined = Table.Combine({deviceComplianceActions, deviceConfigurationActions}), 644 | expanded = Table.ExpandTableColumn(combined, "action", {"deviceId", "policyId", "state", "currentValue", "errorCode", "errorDescription"}), 645 | filtered = Table.SelectRows(expanded, each [deviceId] <> null and [policyId] <> null), 646 | 647 | linkAdded = Table.AddColumn(filtered, "portal", each GetPortalLink([resource], [tenantId])) 648 | in 649 | linkAdded 650 | in 651 | output; 652 | 653 | // Helper 654 | 655 | Request.GetData = (resource as text, schemaOnly as logical, tenants as list, version as text, isPaged as logical, relativeUrl as text, optional query, optional schema as type, optional metadata, optional additionalSchema as text, optional func as function) => 656 | let 657 | result = if(schemaOnly) then 658 | Schema.GetKnownType(metadata{[Version = version]}[Value], resource, if(additionalSchema <> null) then additionalSchema else "tenantId = text") 659 | else 660 | let 661 | requests = Graph.BuildRequests(tenants, relativeUrl, query), 662 | data = Rest.Feed(requests, isPaged, schema), 663 | output = if(func <> null) then func(data) else data 664 | in 665 | output 666 | in 667 | result; 668 | 669 | // Identity Protection 670 | 671 | SecMgmtInsights.RiskDetections = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 672 | let 673 | result = Request.GetData("riskDetection", schemaOnly, tenants, "beta", true, "/beta/identityProtection/riskDetections", query, schema, metadata) 674 | in 675 | result; 676 | 677 | // Intelligent Security Graph 678 | 679 | SecMgmtInsights.Alerts = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 680 | let 681 | result = Request.GetData("alert", schemaOnly, tenants, "v1.0", true, "/v1.0/security/alerts", query, schema, metadata) 682 | in 683 | result; 684 | 685 | SecMgmtInsights.SecureScore = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 686 | let 687 | result = Request.GetData("secureScore", schemaOnly, tenants, "v1.0", true, "/v1.0/security/secureScores", query, schema, metadata) 688 | in 689 | result; 690 | 691 | SecMgmtInsights.SecureScoreControlProfiles = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 692 | let 693 | result = Request.GetData("secureScoreControlProfile", schemaOnly, tenants, "v1.0", true, "/v1.0/security/secureScoreControlProfiles", query, schema, metadata) 694 | in 695 | result; 696 | 697 | // Navigation 698 | 699 | [DataSource.Kind="SecMgmtInsights", Publish="SecMgmtInsights.Publish"] 700 | shared SecMgmtInsights.Contents = Value.ReplaceType(SecMgmtInsights.NavigationTable, SecMgmtInsightsType); 701 | 702 | SecMgmtInsights.NavigationTable = (optional tenants as list) as table => 703 | let 704 | GetMetadata = (version as text) => 705 | let 706 | data = Web.Contents(Uri.Combine(graph_endpoint, version & "/$metadata")), 707 | buffered = Binary.Buffer(data), 708 | metadata = Xml.Tables(buffered){0}[DataServices]{0}[#"http://docs.oasis-open.org/odata/ns/edm"]{0}[Schema] 709 | in 710 | metadata, 711 | 712 | listOfTenants = if(tenants <> null and not List.IsEmpty(tenants)) then 713 | tenants 714 | else if(tenants = null or List.IsEmpty(tenants)) then 715 | Graph.GetTenant()[tenantId] 716 | else 717 | Rest.GetPages(graph_endpoint & "/v1.0/contracts?$select=customerId", Token.GetAccessToken())[customerId], 718 | 719 | metadata = #table({"Version", "Value"}, { 720 | { "beta", GetMetadata("beta") }, 721 | { "v1.0", GetMetadata("v1.0") } 722 | }), 723 | 724 | // Use our schema table as the source of top level items in the navigation tree 725 | entities = Table.SelectColumns(EntityTable, {"Entity"}), 726 | rename = Table.RenameColumns(entities, {{"Entity", "Name"}}), 727 | // Add Data as a calculated column 728 | withData = Table.AddColumn(rename, "Data", each SecMgmtInsights.View([Name], metadata, listOfTenants), type table), 729 | // Add ItemKind and ItemName as fixed text values 730 | withItemKind = Table.AddColumn(withData, "ItemKind", each "Table", type text), 731 | withItemName = Table.AddColumn(withItemKind, "ItemName", each "Table", type text), 732 | // Indicate that the node should not be expandable 733 | withIsLeaf = Table.AddColumn(withItemName, "IsLeaf", each true, type logical), 734 | // Generate the nav table 735 | navTable = Table.ToNavigationTable(withIsLeaf, {"Name"}, "Name", "Data", "ItemKind", "ItemName", "IsLeaf") 736 | in 737 | navTable; 738 | 739 | SecMgmtInsights.View = (entity as text, metadata as table, tenants as list) as table => 740 | let 741 | // Implementation of Table.View handlers. 742 | // 743 | // We wrap the record with Diagnostics.WrapHandlers() to get some automatic 744 | // tracing if a handler returns an error. 745 | // 746 | View = (state as record) => Table.View(null, Diagnostics.WrapHandlers([ 747 | 748 | // Returns the table type returned by GetRows() 749 | GetType = () => CalculateSchema(state), 750 | 751 | // Called last - retrieves the data from the calculated URL 752 | GetRows = () => 753 | let 754 | finalSchema = CalculateSchema(state), 755 | finalUrl = CalculateUrl(state), 756 | // TODO - Need to change var finalUrl to query because the URL is calculated in another function downstream 757 | result = GetActionForEntity(entity)(tenants, false, finalUrl, finalSchema), 758 | appliedType = Table.ChangeType(result, finalSchema) 759 | in 760 | appliedType, 761 | 762 | // GetRowCount - called when all we want is the total row count. 763 | // Most OData services support $count, but it only works if 764 | // no other query parameters are sent (i.e. $top, or $filter). 765 | // Our implementation will first check for other query state - 766 | // if there are any state fields set by other handlers, we 767 | // return "..." unimplemented, because we won't be able to fold 768 | // the request to the server. 769 | GetRowCount = () as number => 770 | if (Record.FieldCount(Record.RemoveFields(state, {"Url", "Entity", "Schema"}, MissingField.Ignore)) > 0) then 771 | ... 772 | else 773 | let 774 | newState = state & [ RowCountOnly = true ], 775 | finalUrl = CalculateUrl(newState), 776 | value = Rest.Scalar(finalUrl), 777 | converted = Number.FromText(value) 778 | in 779 | converted, 780 | 781 | // OnTake - handles the Table.FirstN transform, limiting 782 | // the maximum number of rows returned in the result set. 783 | // The count value should be >= 0. 784 | OnTake = (count as number) => 785 | let 786 | value = if(count >= 1000) then 999 else count, 787 | newState = state & [ Top = value ] 788 | in 789 | @View(newState), 790 | 791 | // OnSkip - handles the Table.Skip transform. 792 | // The count value should be >= 0. 793 | OnSkip = (count as number) => 794 | let 795 | newState = state & [ Skip = count ] 796 | in 797 | @View(newState), 798 | 799 | // OnSelectColumns - handles column selection 800 | OnSelectColumns = (columns as list) => 801 | let 802 | // get the current schema 803 | currentSchema = CalculateSchema(state), 804 | // get the columns from the current schema (which is an M Type value) 805 | rowRecordType = Type.RecordFields(Type.TableRow(currentSchema)), 806 | existingColumns = Record.FieldNames(rowRecordType), 807 | // calculate the new schema 808 | columnsToRemove = List.Difference(existingColumns, columns), 809 | updatedColumns = Record.RemoveFields(rowRecordType, columnsToRemove), 810 | newSchema = type table (Type.ForRecord(updatedColumns, false)) 811 | in 812 | @View(state & 813 | [ 814 | SelectColumns = columns, 815 | Schema = newSchema 816 | ] 817 | ), 818 | 819 | // OnSort - receives a list of records containing two fields: 820 | // [Name] - the name of the column to sort on 821 | // [Order] - equal to Order.Ascending or Order.Descending 822 | // If there are multiple records, the sort order must be maintained. 823 | // 824 | // OData allows you to sort on columns that do not appear in the result 825 | // set, so we do not have to validate that the sorted columns are in our 826 | // existing schema. 827 | OnSort = (order as list) => 828 | let 829 | // This will convert the list of records to a list of text, 830 | // where each entry is " " 831 | sorting = List.Transform(order, (o) => 832 | let 833 | column = o[Name], 834 | order = o[Order], 835 | orderText = if (order = Order.Ascending) then "asc" else "desc" 836 | in 837 | column & " " & orderText 838 | ), 839 | orderBy = Text.Combine(sorting, ", ") 840 | in 841 | @View(state & [ OrderBy = orderBy ]), 842 | 843 | // 844 | // Helper functions 845 | // 846 | // Retrieves the cached schema. If this is the first call 847 | // to CalculateSchema, the table type is calculated based on 848 | // entity name that was passed into the function. 849 | CalculateSchema = (state) as type => 850 | if (state[Schema]? = null) then 851 | GetActionForEntity(entity)(tenants, true, null, null, metadata) 852 | else 853 | state[Schema], 854 | 855 | // Calculates the final URL based on the current state. 856 | CalculateUrl = (state) as text => 857 | let 858 | entity = "", 859 | 860 | // Check for $count. If all we want is a row count, 861 | // then we add /$count to the path value (following the entity name). 862 | urlWithRowCount = 863 | if (state[RowCountOnly]? = true) then 864 | entity & "/$count" 865 | else 866 | entity, 867 | 868 | // Uri.BuildQueryString requires that all field values 869 | // are text literals. 870 | defaultQueryString = [], 871 | 872 | // Check for Top defined in our state 873 | qsWithTop = 874 | if (state[Top]? <> null) then 875 | defaultQueryString & [ #"$top" = Number.ToText(state[Top]) ] 876 | else 877 | defaultQueryString, 878 | 879 | // Check for Skip defined in our state 880 | qsWithSkip = 881 | if (state[Skip]? <> null) then 882 | qsWithTop & [ #"$skip" = Number.ToText(state[Skip]) ] 883 | else 884 | qsWithTop, 885 | 886 | // Check for explicitly selected columns 887 | qsWithSelect = 888 | if (state[SelectColumns]? <> null) then 889 | qsWithSkip & [ #"$select" = Text.Combine(List.RemoveItems(state[SelectColumns], {"tenantId"}), ",") ] 890 | else 891 | qsWithSkip, 892 | 893 | qsWithOrderBy = 894 | if (state[OrderBy]? <> null) then 895 | qsWithSelect & [ #"$orderby" = state[OrderBy] ] 896 | else 897 | qsWithSelect, 898 | 899 | encodedQueryString = Uri.BuildQueryString(qsWithOrderBy), 900 | finalUrl = urlWithRowCount & "?" & encodedQueryString 901 | in 902 | finalUrl 903 | ])) 904 | in 905 | View([Entity = entity]); 906 | 907 | // Network 908 | 909 | GitHub.GetContent = (url as text) => 910 | let 911 | response = Web.Contents(url, 912 | [ 913 | Headers = [ 914 | #"Accept" = "application/json", 915 | #"Accept-encoding" = "gzip" 916 | ], 917 | ManualCredentials = true, 918 | ManualStatusHandling = {401, 403} 919 | ]), 920 | buffered = Binary.Buffer(response), 921 | result = Json.Document(buffered) 922 | in 923 | result; 924 | 925 | Graph.BuildRequests = (tenants as list, relativeUrl as text, optional query as text) => 926 | let 927 | request = if(query = null or query = "") then 928 | Uri.Combine(graph_endpoint, relativeUrl) 929 | else if(Text.Contains(relativeUrl, "?")) then 930 | Uri.Combine(graph_endpoint, relativeUrl) & "&" & Text.AfterDelimiter(query, "?") 931 | else 932 | Uri.Combine(graph_endpoint, relativeUrl) & query, 933 | 934 | data = #table({"tenantId", "secMgmtInsightsScope", "secMgmtInsightsRequest"}, {{tenants, "https://graph.microsoft.com/.default", request}}), 935 | expandedList = Table.ExpandListColumn(data, "tenantId") 936 | in 937 | expandedList; 938 | 939 | Graph.GetTenant = () => 940 | let 941 | data = Rest.GetPages(graph_endpoint & "/v1.0/organization", Extension.CurrentCredential()[access_token]), 942 | renamedColumns = Table.RenameColumns(data, {"id", "tenantId"}, MissingField.Ignore) 943 | in 944 | renamedColumns; 945 | 946 | Rest.Feed = (requests as table, isPaged as logical, optional schema as type) => 947 | let 948 | GetData = (input as record) => 949 | let 950 | requestContainsTopQuery = Text.Contains(input[secMgmtInsightsRequest], "top="), 951 | 952 | data = try if(isPaged and not requestContainsTopQuery) then 953 | Rest.GetPages(input[secMgmtInsightsRequest], Token.GetAccessToken(input[tenantId], input[secMgmtInsightsScope]), schema) 954 | else 955 | Rest.GetContents(input[secMgmtInsightsRequest], Token.GetAccessToken(input[tenantId], input[secMgmtInsightsScope]), isPaged and requestContainsTopQuery, schema), 956 | 957 | response = if(data[HasError] or Table.IsEmpty(data[Value])) then 958 | let 959 | listOfFields = List.RemoveMatchingItems(Record.FieldNames(input), { "secMgmtInsightsRequest" }), 960 | tableFromRecord = Table.FromRecords({input}, listOfFields), 961 | value = Table.ToRecords(tableFromRecord) 962 | in 963 | value 964 | else 965 | let 966 | base = Table.AddColumn(data[Value], "secMgmtInsightsBase", each input), 967 | listOfFields = List.RemoveMatchingItems(Record.FieldNames(input), { "secMgmtInsightsRequest" }), 968 | expanded = Table.ExpandRecordColumn(base, "secMgmtInsightsBase", listOfFields), 969 | value = Table.ToRecords(expanded) 970 | in 971 | value 972 | in 973 | response, 974 | 975 | data = Table.AddColumn(requests, "Column1", each GetData(_)), 976 | buffered = Table.Buffer(data), 977 | 978 | mergedLists = List.Combine(buffered[Column1]), 979 | tableFromList = Table.FromList(mergedLists, Splitter.SplitByNothing(), {"Column1"}), 980 | 981 | listOfColumns = List.Union(List.Transform(mergedLists, each Record.FieldNames(_))), 982 | expandedRecord = Table.ExpandRecordColumn(tableFromList, "Column1", listOfColumns) 983 | in 984 | expandedRecord; 985 | 986 | Rest.GetContents = (url as text, token as text, isPaged as logical, optional schema as type) => 987 | let 988 | response = Web.Contents(url, 989 | [ 990 | Headers = [ 991 | #"Accept" = "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", 992 | #"Accept-encoding" = "gzip, deflate", 993 | #"Authorization" = "Bearer " & token, 994 | #"User-Agent" = "secmgmt-insights-connector" 995 | ], 996 | ManualCredentials = true, 997 | ManualStatusHandling = {400, 401, 403} 998 | ]), 999 | buffered = Binary.Buffer(response), 1000 | body = Json.Document(buffered), 1001 | nextLink = Rest.GetNextLink(body), 1002 | 1003 | data = if (isPaged and schema = null) then 1004 | Diagnostics.LogFailure( 1005 | "Error converting response body. Are the records uniform?", 1006 | () => Table.FromRecords(body[value]) 1007 | ) 1008 | else if(isPaged and schema <> null) then 1009 | let 1010 | asTable = Table.FromList(body[value], Splitter.SplitByNothing(), {"Column1"}), 1011 | fields = Record.FieldNames(Type.RecordFields(Type.TableRow(schema))), 1012 | expanded = Table.ExpandRecordColumn(asTable, "Column1", fields), 1013 | // TODO HACK 1014 | test = Table.RemoveColumns(expanded, {"tenantId"}, MissingField.Ignore) 1015 | in 1016 | test 1017 | else 1018 | let 1019 | abstract = if(Type.Is(Value.Type(body), List.Type)) then body else {body}, 1020 | contents = Table.FromList(abstract, Splitter.SplitByNothing(), null, null, ExtraValues.Error), 1021 | 1022 | firstRow = contents{0}?, 1023 | listOfColumns = List.Union(List.Transform(abstract, each Record.FieldNames(_))), 1024 | 1025 | value = if(firstRow = null) then Table.FromRows({}) else Table.ExpandRecordColumn(contents, "Column1", listOfColumns) 1026 | in 1027 | value 1028 | in 1029 | data meta [NextLink = nextLink]; 1030 | 1031 | Rest.GetNextLink = (response) as nullable text => Record.FieldOrDefault(response, "@odata.nextLink"); 1032 | 1033 | Rest.GetPages = (url as text, token as text, optional schema as type) => Table.GenerateByPage((previous) => 1034 | let 1035 | // if previous is null, then this is our first page of data 1036 | nextLink = if (previous = null) then url else Value.Metadata(previous)[NextLink]?, 1037 | // if NextLink was set to null by the previous call, we know we have no more data 1038 | page = if (nextLink <> null) then Rest.GetContents(nextLink, token, true, schema) else null 1039 | in 1040 | page); 1041 | 1042 | Rest.Scalar = (url as text) => 1043 | let 1044 | token = Token.GetAccessToken(null, "https://graph.microsoft.com/.default"), 1045 | 1046 | response = Web.Contents(url, 1047 | [ 1048 | Headers = [ 1049 | #"Accept" = "text/plain", 1050 | #"Authorization" = "Bearer " & token, 1051 | #"User-Agent" = "secmgmt-insights-connector" 1052 | ], 1053 | ManualCredentials = true, 1054 | ManualStatusHandling = {400, 401, 403} 1055 | ]), 1056 | buffered = Binary.Buffer(response), 1057 | value = Text.FromBinary(buffered) 1058 | in 1059 | value; 1060 | 1061 | ServiceCommunications.BuildRequests = (tenants as list, relativeUrl as text, optional queryString as text) => 1062 | let 1063 | source = #table({"tenantId", "secMgmtInsightsScope"}, {{tenants, "https://manage.office.com/.default"}}), 1064 | expandedList = Table.ExpandListColumn(source, "tenantId"), 1065 | data = Table.AddColumn(expandedList, "secMgmtInsightsRequest", each 1066 | if(queryString = null or queryString = "") then 1067 | "https://manage.office.com/api/v1.0/" & [tenantId] & relativeUrl 1068 | else if(Text.Contains(relativeUrl, "?")) then 1069 | "https://manage.office.com/api/v1.0/" & [tenantId] & relativeUrl & "&" & Text.AfterDelimiter(queryString, "?") 1070 | else 1071 | "https://manage.office.com/api/v1.0/" & [tenantId] & relativeUrl & queryString) 1072 | in 1073 | data; 1074 | 1075 | // Office 365 Management Activity 1076 | 1077 | ManagementActivity.GetEvents = (tenants as list, contentType as text) => 1078 | let 1079 | requests = ServiceCommunications.BuildRequests(tenants, "/activity/feed/subscriptions/content?contentType=" & contentType), 1080 | availableContent = Rest.Feed(requests, false), 1081 | renamedColumns = Table.RenameColumns(availableContent, {"contentUri", "secMgmtInsightsRequest"}, MissingField.Ignore), 1082 | data = Rest.Feed(renamedColumns, false) 1083 | in 1084 | data; 1085 | 1086 | SecMgmtInsights.ActivitySubscriptions = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1087 | let 1088 | output = if(schemaOnly) then 1089 | type table [tenantId = text, contentType = text, status = text, webhook = any] 1090 | else 1091 | let 1092 | requests = ServiceCommunications.BuildRequests(tenants, "/activity/feed/subscriptions/list"), 1093 | data = Rest.Feed(requests, false) 1094 | in 1095 | data 1096 | in 1097 | output; 1098 | 1099 | SecMgmtInsights.AzureActiveDirectoryEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1100 | let 1101 | output = if(schemaOnly) then 1102 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, ResultStatus = text, UserKey = text, Version = text, Workload = text, ClientIP = text, ObjectId = text, UserId = text, AzureActiveDirectoryEventType = text, ExtendedProperties = any, ModifiedProperties = any, Actor = any, ActorContextId = text, ActorIpAddress = text, InterSystemsId = text, IntraSystemId = text, SupportTicketId = text, Target = any, TargetContextId = text, ApplicationId = text] 1103 | else 1104 | ManagementActivity.GetEvents(tenants, "audit.azureactivedirectory") 1105 | in 1106 | output; 1107 | 1108 | SecMgmtInsights.ComplianceCaseEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1109 | let 1110 | output = if(schemaOnly) then 1111 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, UserKey = text, UserType = text, Version = number, Workload = text, ObjectId = text, UserId = text, Case = text, ExchangeLocations = any, ExtendedProperties = any, ObjectType = text, Parameters = any, PublicFolderLocations = any, Query = any, SharepointLocations = any] 1112 | else 1113 | let 1114 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1115 | filtered = Table.SelectRows(events, each try [Operation] = "CaseAdded" or [Operation] = "CaseViewed" otherwise null) 1116 | in 1117 | filtered 1118 | in 1119 | output; 1120 | 1121 | SecMgmtInsights.DlpEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1122 | let 1123 | output = if(schemaOnly) then 1124 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, UserKey = text, UserType = text, Version = text, Workload = text, ObjectId = text, UserId = text, IncidentId = text, PolicyDetails = any, SensitiveInfoDetectionIsIncluded = logical, ExchangeMetaData = any, SharePointMetaData = any, ExceptionInfo = text] 1125 | else 1126 | ManagementActivity.GetEvents(tenants, "dlp.all") 1127 | in 1128 | output; 1129 | 1130 | SecMgmtInsights.ExchangeEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1131 | let 1132 | output = if(schemaOnly) then 1133 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, ResultStatus = logical, UserKey = text, UserType = text, Version = text, Workload = text, ClientIP = any, ObjectId = text, UserId = text, AppId = text, ClientAppId = text, ExternalAccess = logical, OrganizationName = text, OriginatingServer = text, Parameters = any, SessionId = text] 1134 | else 1135 | let 1136 | events = ManagementActivity.GetEvents(tenants, "audit.exchange"), 1137 | filtered = Table.SelectRows(events, each try [Operation] <> "DlpRuleMatch" otherwise null) 1138 | in 1139 | filtered 1140 | in 1141 | output; 1142 | 1143 | SecMgmtInsights.HygieneTenantEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1144 | let 1145 | output = if(schemaOnly) then 1146 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, ResultStatus = text, UserKey = text, UserType = text, Version = text, Workload = text, UserId = text, Audit = text, Event = text, EventId = text, EventValue = text, Reason = text] 1147 | else 1148 | let 1149 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1150 | filtered = Table.SelectRows(events, each try [Operation] = "HygieneTenantEvents" otherwise null) 1151 | in 1152 | filtered 1153 | in 1154 | output; 1155 | 1156 | SecMgmtInsights.GeneralEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1157 | let 1158 | output = if(schemaOnly) then 1159 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, ResultStatus = text,Version = text, Workload = text, UserId = text] 1160 | else 1161 | ManagementActivity.GetEvents(tenants, "audit.general") 1162 | in 1163 | output; 1164 | 1165 | SecMgmtInsights.QuarantineReleaseMessage = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1166 | let 1167 | output = if(schemaOnly) then 1168 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, ResultStatus = text, UserKey = text, UserType = text, Version = number, Worload = text, UserId = text, NetworkMessageId = text, ReleaseTo = text, RequestSource = text, RequestType = text] 1169 | else 1170 | let 1171 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1172 | filtered = Table.SelectRows(events, each try [Operation] = "QuarantineReleaseMessage" otherwise null) 1173 | in 1174 | filtered 1175 | in 1176 | output; 1177 | 1178 | SecMgmtInsights.SafeAttachmentEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1179 | let 1180 | output = if(schemaOnly) then 1181 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, UserKey = text, UserType = text, Version = text, Workload = text, ObjectId = text, UserId = text, AttachmentData = any, DetectionMethod = text, DetectionType = text, EventDeepLink = text, InternetMessageId = text, MessageTime = datetime, NetworkMessageId = text, P1Sender = text, P2Sender = text, Policy = text, PolicyAction = text, Recipients = any, SenderIp = text, Subject = text, Verdict = text] 1182 | else 1183 | let 1184 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1185 | filtered = Table.SelectRows(events, each try [Operation] = "TIMailData" otherwise null) 1186 | in 1187 | filtered 1188 | in 1189 | output; 1190 | 1191 | SecMgmtInsights.SafeLinksEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1192 | let 1193 | output = if(schemaOnly) then 1194 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, UserKey = text, UserType = text, Version = text, Workload = text, AppName = text, AppVersion = text, EventDeepLink = text, OS = text, SourceId = text, TimeOfClick = datetime, Url = text, UrlClickAction = text] 1195 | else 1196 | let 1197 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1198 | filtered = Table.SelectRows(events, each try [Operation] = "TIUrlClickData" otherwise null) 1199 | in 1200 | filtered 1201 | in 1202 | output; 1203 | 1204 | SecMgmtInsights.SecurityComplianceAlerts = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1205 | let 1206 | output = if(schemaOnly) then 1207 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, ResultStatus = text, UserKey = text, UserType = text, Version = text, Workload = text, ObjectId = text, UserId = text, AlertId = text, AlertLinks = any, AlertType = text, Category = text, Comments = text, Data = any, Name = text, PolicyId = text, Severity = text, Source = text, Status = text] 1208 | else 1209 | let 1210 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1211 | filtered = Table.SelectRows(events, each try [UserKey] = "SecurityComplianceAlerts" otherwise null) 1212 | in 1213 | filtered 1214 | in 1215 | output; 1216 | 1217 | SecMgmtInsights.SharePointEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1218 | let 1219 | output = if(schemaOnly) then 1220 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, UserKey = text, UserType = text, Version = text, Workload = text, ClientIP = text, ObjectId = text, UserId = text, CorrelationId = text, EventSource = text, ItemType = text, ListId = text, ListItemUniqueId = text, Site = text, UserAgent = text, WebId = text, SourceFileExtension = text, SiteUrl = text, SourceFileName = text, SourceRelativeUrl = text] 1221 | else 1222 | ManagementActivity.GetEvents(tenants, "audit.sharepoint") 1223 | in 1224 | output; 1225 | 1226 | SecMgmtInsights.SubmissionEvents = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1227 | let 1228 | output = if(schemaOnly) then 1229 | type table [tenantId = text, CreationTime = datetimezone, Id = text, Operation = text, OrganizationId = text, RecordType = text, UserKey = text, UserType = text, Version = text, Workload = text, ObjectId = text, UserId = text, BCLValue = text, ExtendedProperties = any, FilteringDate = datetime, KesMailId = text, Language = text, MessageDate = datetime, P1Sender = text, P1SenderDomain = text, P2Sender = text, P2SenderDomain = text, Recipients = any, RescanResult = any, SenderIP = text, Subject = text, SubmissionId = text, SubmissionState = text, SubmissionType = text] 1230 | else 1231 | let 1232 | events = ManagementActivity.GetEvents(tenants, "audit.general"), 1233 | filtered = Table.SelectRows(events, each try [Operation] = "AdminSubmission" or [Operation] = "UserSubmission" otherwise null) 1234 | in 1235 | filtered 1236 | in 1237 | output; 1238 | 1239 | // Office 365 Service Communication 1240 | 1241 | SecMgmtInsights.ServiceCurrentStatus = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1242 | let 1243 | output = if(schemaOnly) then 1244 | type table [tenantId = text, Id = text, Workload = text, StatusDate = datetime, WorkloadDisplayName = text, Status = text, IncidentIds = any, FeatureGroupStatusCollection = any] 1245 | else 1246 | let 1247 | requests = ServiceCommunications.BuildRequests(tenants, "/ServiceComms/CurrentStatus"), 1248 | data = Rest.Feed(requests, true) 1249 | in 1250 | data 1251 | in 1252 | output; 1253 | 1254 | SecMgmtInsights.ServiceHistoricalStatus = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1255 | let 1256 | output = if(schemaOnly) then 1257 | type table [tenantId = text, Id = text, Workload = text, StatusDate = datetime, WorkloadDisplayName = text, Status = text, IncidentIds = any, FeatureGroupStatusCollection = any] 1258 | else 1259 | let 1260 | requests = ServiceCommunications.BuildRequests(tenants, "/ServiceComms/HistoricalStatus"), 1261 | data = Rest.Feed(requests, true) 1262 | in 1263 | data 1264 | in 1265 | output; 1266 | 1267 | SecMgmtInsights.ServiceMessages = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1268 | let 1269 | output = if(schemaOnly) then 1270 | type table [tenantId = text, Id = text, Name = text, Title = text, StartTime = datetimezone, EndTime = datetimezone, Status = text, Messages = any, LastUpdatedTime = datetimezone, Workload = text, WorkloadDisplayName = text, Feature = text, FeatureDisplayName = text] 1271 | else 1272 | let 1273 | requests = ServiceCommunications.BuildRequests(tenants, "/ServiceComms/Messages"), 1274 | data = Rest.Feed(requests, true) 1275 | in 1276 | data 1277 | in 1278 | output; 1279 | 1280 | // Reporting 1281 | 1282 | SecMgmtInsights.MailboxUsageDetail = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1283 | let 1284 | result = Request.GetData("mailboxUsageDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getMailboxUsageDetail(period='D30')?$format=application/json", query, schema, metadata) 1285 | in 1286 | result; 1287 | 1288 | SecMgmtInsights.Office365ActivationsUserDetail = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1289 | let 1290 | result = Request.GetData("office365ActivationsUserDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getOffice365ActivationsUserDetail?$format=application/json", query, schema, metadata) 1291 | in 1292 | result; 1293 | 1294 | SecMgmtInsights.Office365ActiveUserDetails = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1295 | let 1296 | result = Request.GetData("office365ActiveUserDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getOffice365ActiveUserDetail(period='D30')?$format=application/json", query, schema, metadata) 1297 | in 1298 | result; 1299 | 1300 | SecMgmtInsights.Office365ServicesUserCounts = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1301 | let 1302 | result = Request.GetData("office365ServicesUserCounts", schemaOnly, tenants, "beta", true, "/beta/reports/getOffice365ServicesUserCounts(period='D30')?$format=application/json", query, schema, metadata) 1303 | in 1304 | result; 1305 | 1306 | SecMgmtInsights.OneDriveUsageAccountDetail = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1307 | let 1308 | result = Request.GetData("oneDriveUsageAccountDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getOneDriveUsageAccountDetail(period='D30')?$format=application/json", query, schema, metadata) 1309 | in 1310 | result; 1311 | 1312 | SecMgmtInsights.SharePointSiteUsageDetail = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1313 | let 1314 | result = Request.GetData("sharePointSiteUsageDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getSharePointSiteUsageDetail(period='D30')?$format=application/json", query, schema, metadata) 1315 | in 1316 | result; 1317 | 1318 | SecMgmtInsights.TeamsUserActivityUserDetail = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1319 | let 1320 | result = Request.GetData("teamsUserActivityUserDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getTeamsUserActivityUserDetail(period='D30')?$format=application/json", query, schema, metadata) 1321 | in 1322 | result; 1323 | 1324 | SecMgmtInsights.YammerActivityUserDetail = (tenants as list, schemaOnly as logical, optional query as text, optional schema as type, optional metadata as table) => 1325 | let 1326 | result = Request.GetData("yammerActivityUserDetail", schemaOnly, tenants, "beta", true, "/beta/reports/getYammerActivityUserDetail(period='D30')?$format=application/json", query, schema, metadata) 1327 | in 1328 | result; -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = SecMgmtInsights.Contents() 4 | in 5 | result -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights16.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights20.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights24.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights32.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights40.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights48.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights64.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/SecMgmtInsights80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/src/SecMgmtInsights/SecMgmtInsights80.png -------------------------------------------------------------------------------- /src/SecMgmtInsights/Table.ChangeType.pqm: -------------------------------------------------------------------------------- 1 | let 2 | // table should be an actual Table.Type, or a List.Type of Records 3 | Table.ChangeType = (table, tableType as type) as nullable table => 4 | // we only operate on table types 5 | if (not Type.Is(tableType, type table)) then error "type argument should be a table type" else 6 | // if we have a null value, just return it 7 | if (table = null) then table else 8 | let 9 | columnsForType = Type.RecordFields(Type.TableRow(tableType)), 10 | columnsAsTable = Record.ToTable(columnsForType), 11 | schema = Table.ExpandRecordColumn(columnsAsTable, "Value", {"Type"}, {"Type"}), 12 | previousMeta = Value.Metadata(tableType), 13 | 14 | // make sure we have a table 15 | parameterType = Value.Type(table), 16 | _table = 17 | if (Type.Is(parameterType, type table)) then table 18 | else if (Type.Is(parameterType, type list)) then 19 | let 20 | asTable = Table.FromList(table, Splitter.SplitByNothing(), {"Column1"}), 21 | firstValueType = Value.Type(Table.FirstValue(asTable, null)), 22 | result = 23 | // if the member is a record (as expected), then expand it. 24 | if (Type.Is(firstValueType, type record)) then 25 | Table.ExpandRecordColumn(asTable, "Column1", schema[Name]) 26 | else 27 | error Error.Record("Error.Parameter", "table argument is a list, but not a list of records", [ ValueType = firstValueType ]) 28 | in 29 | if (List.IsEmpty(table)) then 30 | #table({"a"}, {}) 31 | else result 32 | else 33 | error Error.Record("Error.Parameter", "table argument should be a table or list of records", [ValueType = parameterType]), 34 | 35 | reordered = Table.SelectColumns(_table, schema[Name], MissingField.UseNull), 36 | 37 | // process primitive values - this will call Table.TransformColumnTypes 38 | map = (t) => if Type.Is(t, type table) or Type.Is(t, type list) or Type.Is(t, type record) or t = type any then null else t, 39 | mapped = Table.TransformColumns(schema, {"Type", map}), 40 | omitted = Table.SelectRows(mapped, each [Type] <> null), 41 | existingColumns = Table.ColumnNames(reordered), 42 | removeMissing = Table.SelectRows(omitted, each List.Contains(existingColumns, [Name])), 43 | primativeTransforms = Table.ToRows(removeMissing), 44 | changedPrimatives = Table.TransformColumnTypes(reordered, primativeTransforms), 45 | 46 | // Get the list of transforms we'll use for Record types 47 | recordColumns = Table.SelectRows(schema, each Type.Is([Type], type record)), 48 | recordTypeTransformations = Table.AddColumn(recordColumns, "RecordTransformations", each (r) => Record.ChangeType(r, [Type]), type function), 49 | recordChanges = Table.ToRows(Table.SelectColumns(recordTypeTransformations, {"Name", "RecordTransformations"})), 50 | 51 | // Get the list of transforms we'll use for List types 52 | listColumns = Table.SelectRows(schema, each Type.Is([Type], type list)), 53 | listTransforms = Table.AddColumn(listColumns, "ListTransformations", each (t) => List.ChangeType(t, [Type]), Function.Type), 54 | listChanges = Table.ToRows(Table.SelectColumns(listTransforms, {"Name", "ListTransformations"})), 55 | 56 | // Get the list of transforms we'll use for Table types 57 | tableColumns = Table.SelectRows(schema, each Type.Is([Type], type table)), 58 | tableTransforms = Table.AddColumn(tableColumns, "TableTransformations", each (t) => @Table.ChangeType(t, [Type]), Function.Type), 59 | tableChanges = Table.ToRows(Table.SelectColumns(tableTransforms, {"Name", "TableTransformations"})), 60 | 61 | // Perform all of our transformations 62 | allColumnTransforms = recordChanges & listChanges & tableChanges, 63 | changedRecordTypes = if (List.IsEmpty(allColumnTransforms)) then changedPrimatives else Table.TransformColumns(changedPrimatives, allColumnTransforms, null, MissingField.Ignore), 64 | 65 | // set final type 66 | withType = Value.ReplaceType(changedRecordTypes, tableType) 67 | in 68 | if (List.IsEmpty(Record.FieldNames(columnsForType))) then table else withType meta previousMeta, 69 | 70 | // If given a generic record type (no predefined fields), the original record is returned 71 | Record.ChangeType = (record as record, recordType as type) => 72 | let 73 | // record field format is [ fieldName = [ Type = type, Optional = logical], ... ] 74 | fields = try Type.RecordFields(recordType) otherwise error "Record.ChangeType: failed to get record fields. Is this a record type?", 75 | fieldNames = Record.FieldNames(fields), 76 | fieldTable = Record.ToTable(fields), 77 | optionalFields = Table.SelectRows(fieldTable, each [Value][Optional])[Name], 78 | requiredFields = List.Difference(fieldNames, optionalFields), 79 | // make sure all required fields exist 80 | withRequired = Record.SelectFields(record, requiredFields, MissingField.UseNull), 81 | // append optional fields 82 | withOptional = withRequired & Record.SelectFields(record, optionalFields, MissingField.Ignore), 83 | // set types 84 | transforms = GetTransformsForType(recordType), 85 | withTypes = Record.TransformFields(withOptional, transforms, MissingField.Ignore), 86 | // order the same as the record type 87 | reorder = Record.ReorderFields(withTypes, fieldNames, MissingField.Ignore) 88 | in 89 | if (List.IsEmpty(fieldNames)) then record else reorder, 90 | 91 | List.ChangeType = (list as list, listType as type) => 92 | if (not Type.Is(listType, type list)) then error "type argument should be a list type" else 93 | let 94 | listItemType = Type.ListItem(listType), 95 | transform = GetTransformByType(listItemType), 96 | modifiedValues = List.Transform(list, transform), 97 | typed = Value.ReplaceType(modifiedValues, listType) 98 | in 99 | typed, 100 | 101 | // Returns a table type for the provided schema table 102 | Schema.ToTableType = (schema as table) as type => 103 | let 104 | toList = List.Transform(schema[Type], (t) => [Type=t, Optional=false]), 105 | toRecord = Record.FromList(toList, schema[Name]), 106 | toType = Type.ForRecord(toRecord, false), 107 | previousMeta = Value.Metadata(schema) 108 | in 109 | type table (toType) meta previousMeta, 110 | 111 | // Returns a list of transformations that can be passed to Table.TransformColumns, or Record.TransformFields 112 | // Format: {"Column", (f) => ...) .... ex: {"A", Number.From} 113 | GetTransformsForType = (_type as type) as list => 114 | let 115 | fieldsOrColumns = if (Type.Is(_type, type record)) then Type.RecordFields(_type) 116 | else if (Type.Is(_type, type table)) then Type.RecordFields(Type.TableRow(_type)) 117 | else error "GetTransformsForType: record or table type expected", 118 | toTable = Record.ToTable(fieldsOrColumns), 119 | transformColumn = Table.AddColumn(toTable, "Transform", each GetTransformByType([Value][Type]), Function.Type), 120 | transformMap = Table.ToRows(Table.SelectColumns(transformColumn, {"Name", "Transform"})) 121 | in 122 | transformMap, 123 | 124 | GetTransformByType = (_type as type) as function => 125 | if (Type.Is(_type, type number)) then Number.From 126 | else if (Type.Is(_type, type text)) then Text.From 127 | else if (Type.Is(_type, type date)) then Date.From 128 | else if (Type.Is(_type, type datetime)) then DateTime.From 129 | else if (Type.Is(_type, type duration)) then Duration.From 130 | else if (Type.Is(_type, type datetimezone)) then DateTimeZone.From 131 | else if (Type.Is(_type, type logical)) then Logical.From 132 | else if (Type.Is(_type, type time)) then Time.From 133 | else if (Type.Is(_type, type record)) then (t) => if (t <> null) then @Record.ChangeType(t, _type) else t 134 | else if (Type.Is(_type, type table)) then (t) => if (t <> null) then @Table.ChangeType(t, _type) else t 135 | else if (Type.Is(_type, type list)) then (t) => if (t <> null) then @List.ChangeType(t, _type) else t 136 | else (t) => t 137 | in 138 | Table.ChangeType -------------------------------------------------------------------------------- /src/SecMgmtInsights/Table.GenerateByPage.pqm: -------------------------------------------------------------------------------- 1 | (getNextPage as function) as table => 2 | let 3 | listOfPages = List.Generate( 4 | () => getNextPage(null), // get the first page of data 5 | (lastPage) => lastPage <> null, // stop when the function returns null 6 | (lastPage) => getNextPage(lastPage) // pass the previous page to the next function call 7 | ), 8 | // concatenate the pages together 9 | tableOfPages = Table.FromList(listOfPages, Splitter.SplitByNothing(), {"Column1"}), 10 | buffered = Table.Buffer(tableOfPages), 11 | firstRow = buffered{0}? 12 | in 13 | // if we didn't get back any pages of data, return an empty table 14 | // otherwise set the table type based on the columns of the first page 15 | if (firstRow = null) then 16 | Table.FromRows({}) 17 | // check for empty first table 18 | else if (Table.IsEmpty(firstRow[Column1])) then 19 | firstRow[Column1] 20 | else 21 | Value.ReplaceType( 22 | Table.ExpandTableColumn(buffered, "Column1", Table.ColumnNames(firstRow[Column1])), 23 | Value.Type(firstRow[Column1]) 24 | ) -------------------------------------------------------------------------------- /src/SecMgmtInsights/Table.ToNavigationTable.pqm: -------------------------------------------------------------------------------- 1 | ( 2 | table as table, 3 | keyColumns as list, 4 | nameColumn as text, 5 | dataColumn as text, 6 | itemKindColumn as text, 7 | itemNameColumn as text, 8 | isLeafColumn as text 9 | ) as table => 10 | let 11 | tableType = Value.Type(table), 12 | newTableType = Type.AddTableKey(tableType, keyColumns, true) meta 13 | [ 14 | NavigationTable.NameColumn = nameColumn, 15 | NavigationTable.DataColumn = dataColumn, 16 | NavigationTable.ItemKindColumn = itemKindColumn, 17 | Preview.DelayColumn = itemNameColumn, 18 | NavigationTable.IsLeafColumn = isLeafColumn 19 | ], 20 | navigationTable = Value.ReplaceType(table, newTableType) 21 | in 22 | navigationTable -------------------------------------------------------------------------------- /src/SecMgmtInsights/client_id: -------------------------------------------------------------------------------- 1 | 723f43ad-733a-4d75-9fbc-817e5a0a066e -------------------------------------------------------------------------------- /src/SecMgmtInsights/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Security and Management Insights 122 | 123 | 124 | Security and Management Insights 125 | 126 | 127 | Security and Management Insights 128 | 129 | -------------------------------------------------------------------------------- /templates/secmgmt-insights-customer.pbit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/templates/secmgmt-insights-customer.pbit -------------------------------------------------------------------------------- /templates/secmgmt-insights-partner.pbit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/secmgmt-insights-connector/3b4d44d0af9dd6fe12b6bd4182b4b7e3e6a07d4b/templates/secmgmt-insights-partner.pbit --------------------------------------------------------------------------------