diff --git a/.gitignore b/.gitignore
index e8cef5c36c55..97735ad0415c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,8 @@ yarn-error.log*
# vscode debug logs
debug.log
-app.log
\ No newline at end of file
+app.log
+
+# AI rules
+.*/rules
+AGENTS.md
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 1bcdb9744954..91a6a5c671f2 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -10,6 +10,12 @@
"type": "shell",
"command": "azurite --location ../",
"isBackground": true,
+ "options": {
+ "env": {
+ "LC_ALL": "en-US.UTF-8",
+ "LANG": "en-US"
+ }
+ },
"problemMatcher": {
"pattern": [
{
diff --git a/CLA.md b/CLA.md
new file mode 100644
index 000000000000..1f94b337f7bf
--- /dev/null
+++ b/CLA.md
@@ -0,0 +1,83 @@
+# Contributor License Agreement (CLA)
+
+This Contributor License Agreement ("Agreement") is entered into by the individual or entity ("You") submitting a Contribution to this project. By submitting a Contribution, You agree to the following terms and conditions:
+
+---
+
+## 1. Definitions
+
+1. **"Contribution"** means any original work of authorship, including modifications or additions to existing works, submitted in any form (including source code, object code, documentation, or other materials) to this repository.
+2. **"CyberDrain"** means the maintainers, owners, or legal rights holders of this repository, including successors and assigns.
+3. **"Project License"** refers to the **GNU Affero General Public License, version 3 (AGPL-3.0)** under which this project is distributed, unless CyberDrain elects to relicense under a custom license.
+
+---
+
+## 2. Copyright Assignment
+
+You hereby assign to CyberDrain, effective on submission of any Contribution, **all right, title, and interest worldwide in and to the copyright** of Your Contributions.
+
+This assignment includes, without limitation, the exclusive rights to:
+
+* Reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the Contributions in any medium, and
+* Relicense the Contributions under the AGPL-3.0 license, any future versions of that license, or under custom/commercial licenses as CyberDrain deems appropriate.
+
+To the extent that applicable law prohibits the assignment of certain moral rights or similar rights, You hereby irrevocably waive those rights to the maximum extent permitted by law.
+
+---
+
+## 3. Patent Grant
+
+You hereby grant to CyberDrain, its successors, assigns, and licensees a **perpetual, worldwide, non-exclusive, transferable, irrevocable, royalty-free, fully paid-up license** under any patents that You own or control, to make, have made, use, offer to sell, sell, import, and otherwise transfer Your Contributions.
+
+This patent license extends only to the combination of Your Contributions with the Project to which they were submitted.
+
+---
+
+## 4. License Grant Back to You
+
+CyberDrain hereby grants You a non-exclusive, worldwide, royalty-free, irrevocable license to use, reproduce, and prepare derivative works of Your Contributions for any purpose, **provided such use does not conflict with the licensing terms applied by CyberDrain** (including AGPL-3.0 or custom licenses).
+
+---
+
+## 5. Representations and Warranties
+
+By submitting a Contribution, You represent and warrant that:
+
+1. The Contribution is Your original creation, or You have sufficient rights to submit it.
+2. The Contribution does not knowingly violate or infringe any third-party intellectual property rights.
+3. You are legally entitled to assign copyright and grant the licenses described herein.
+4. The Contribution is submitted free of any encumbrances, liens, or claims by any third party.
+
+---
+
+## 6. Custom Licensing
+
+CyberDrain reserves the right to distribute the Project, including Your Contributions, under:
+
+* The **AGPL-3.0 license**, and/or
+* **Custom or commercial licenses**, including licenses granted to sponsors via GitHub Sponsorships.
+
+Contributors acknowledge and agree that:
+
+* Their Contributions may be included under such custom licenses.
+* No royalties, fees, or other compensation shall be due to Contributors in connection with such relicensing.
+
+---
+
+## 7. Disclaimer of Warranty
+
+Except as expressly stated in this Agreement, You provide Contributions **βAS ISβ**, without warranties or conditions of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement.
+
+---
+
+## 8. Limitation of Liability
+
+In no event shall You be liable for any direct, indirect, incidental, special, exemplary, or consequential damages arising out of or in connection with Your Contributions, even if advised of the possibility of such damages.
+
+---
+
+## 9. Acceptance
+
+By submitting a Contribution to this repository, You acknowledge that You have read and understood this Agreement, and that You agree to be legally bound by its terms.
+
+No signature is required β **submission of a Contribution constitutes acceptance**.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000000..219f861658e2
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,47 @@
+# Contributing to This Project
+
+First of all β thank you for considering contributing! π Contributions help improve this project for everyone, and we welcome issues, discussions, and pull requests.
+
+Please read through this document before contributing.
+
+---
+
+## Contributor License Agreement (CLA)
+
+By contributing to this repository, you agree to the terms of our **Contributor License Agreement (CLA):**
+
+* **Copyright Transfer**: All contributions (commits, pull requests, issues, or code reviews) are automatically assigned to **CyberDrain**.
+* Contributors give up ownership rights of their contributions and transfer them fully to CyberDrain.
+* CyberDrain may use, modify, distribute, sublicense, or relicense the contributions under any terms it deems fit, including custom or commercial licenses.
+* **You do not need to sign anything** β the act of contributing implies agreement with this CLA.
+
+---
+
+## Custom Licenses
+
+This project is generally open source, but we also provide **custom licensing options**:
+
+* Custom licenses are available **upon agreement**.
+* Sponsors who arrange a custom license are **not required** to publish their license terms in this repository.
+* Since copyright of contributions is transferred to CyberDrain, CyberDrain has full authority to include contributions under such custom licensing terms.
+
+
+---
+
+## How to Contribute
+
+As this project is ever evolving, we recommend checking out the contributions docs on our doc page here:
+
+- https://docs.cipp.app/dev-documentation/contributing-to-the-code
+- https://docs.cipp.app/dev-documentation/cipp-dev-guide
+- https://docs.cipp.app/dev-documentation/contributing-to-the-documentation
+
+---
+
+## Code of Conduct
+
+We expect all contributors to follow respectful, inclusive, and collaborative practices.
+Please help keep this project a safe and welcoming place for everyone.
+
+π By contributing to this repository, you acknowledge that your contributions are automatically and irrevocably transferred in copyright to **CyberDrain**, and that they are covered by the CLA described above.
+
diff --git a/LICENSE.CustomLicenses.md b/LICENSE.CustomLicenses.md
new file mode 100644
index 000000000000..2c8ebfbe3dea
--- /dev/null
+++ b/LICENSE.CustomLicenses.md
@@ -0,0 +1,14 @@
+1. Availability of Custom Licenses
+Custom licenses are available to sponsors via GitHub Sponsorships. Upon mutual agreement between the project maintainers and the sponsor, such licenses shall apply to the sponsored party.
+
+2. Publication Exemption
+Custom licenses granted through GitHub Sponsorships are exempt from publication in this repository. Sponsors and maintainers may keep such agreements private.
+
+3. Contributor License Agreement (CLA)
+By contributing to this repository in any form (including but not limited to commits, pull requests, and code reviews), contributors explicitly agree to the terms of this Contributor License Agreement.
+
+4. Coverage of Contributions
+Any and all commits made to this repository are automatically considered covered under this CLA. Contributors retain copyright to their individual contributions, while granting the maintainers the necessary rights to use, modify, distribute, and sublicense such contributions in accordance with the terms of the project.
+
+5. Automatic Acceptance
+All contributors to this repository, by the act of contribution, automatically and irrevocably agree to the provisions of this CLA and the terms herein.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000000..29ebfa545f55
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/context7.json b/context7.json
new file mode 100644
index 000000000000..3a0b7c321b31
--- /dev/null
+++ b/context7.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://context7.com/schema/context7.json",
+ "projectTitle": "CIPP - Cyberdrain Improved Partner Portal",
+ "description": "The CyberDrain Improved Partner Portal is a portal to help manage administration for Microsoft Partners.",
+ "folders": [],
+ "excludeFolders": [],
+ "excludeFiles": [],
+ "rules": [],
+ "previousVersions": [],
+ "branch": "docs"
+}
diff --git a/cspell.json b/cspell.json
index 1ff07bf80063..9ffb9e0d6e27 100644
--- a/cspell.json
+++ b/cspell.json
@@ -1,64 +1,81 @@
{
- "version": "0.2",
- "ignorePaths": [],
- "dictionaryDefinitions": [],
- "dictionaries": [],
- "words": [
- "ADMS",
- "AITM",
- "Augmentt",
- "Autotask",
- "Choco",
- "CIPP",
- "CIPP-API",
- "Datto",
- "Entra",
- "ESET",
- "GDAP",
- "HIBP",
- "Hudu",
- "ImmyBot",
- "Intune",
- "LCID",
- "OBEE",
- "Passwordless",
- "pwpush",
- "Rewst",
- "Sherweb",
- "Syncro",
- "Yubikey"
- ],
- "ignoreWords": [
- "Addins",
- "CIPPAPI",
- "PSTN",
- "TNEF",
- "exo_individualsharing",
- "exo_mailboxaudit",
- "exo_mailtipsenabled",
- "exo_outlookaddins",
- "exo_storageproviderrestricted",
- "locationcipp",
- "mdo_antiphishingpolicies",
- "mdo_autoforwardingmode",
- "mdo_blockmailforward",
- "mdo_commonattachmentsfilter",
- "mdo_highconfidencephishaction",
- "mdo_highconfidencespamaction",
- "mdo_phishthresholdlevel",
- "mdo_phisspamacation",
- "mdo_safeattachmentpolicy",
- "mdo_safeattachments",
- "mdo_safedocuments",
- "mdo_safelinksforOfficeApps",
- "mdo_safelinksforemail",
- "mdo_spam_notifications_only_for_admins",
- "mdo_zapmalware",
- "mdo_zapphish",
- "mdo_zapspam",
- "microsoftonline",
- "mip_search_auditlog",
- "winmail"
- ],
- "import": []
+ "version": "0.2",
+ "ignorePaths": [],
+ "dictionaryDefinitions": [],
+ "dictionaries": [],
+ "words": [
+ "ADMS",
+ "AITM",
+ "AOSP",
+ "Augmentt",
+ "Automapping",
+ "Autotask",
+ "Choco",
+ "cipp",
+ "CIPP",
+ "CIPP-API",
+ "Datto",
+ "DMARC",
+ "Entra",
+ "ESET",
+ "GDAP",
+ "HIBP",
+ "Hudu",
+ "ImmyBot",
+ "Intune",
+ "LCID",
+ "OBEE",
+ "passwordless",
+ "Passwordless",
+ "pwpush",
+ "Reshare",
+ "Rewst",
+ "Sherweb",
+ "superadmin",
+ "Syncro",
+ "TERRL",
+ "unconfigured",
+ "Yubikey"
+ ],
+ "ignoreWords": [
+ "Addins",
+ "Disablex",
+ "Displayname",
+ "CIPPAPI",
+ "PSTN",
+ "TNEF",
+ "Equivio",
+ "defaultvalues",
+ "Excludedfile",
+ "exo_individualsharing",
+ "exo_mailboxaudit",
+ "exo_mailtipsenabled",
+ "exo_outlookaddins",
+ "exo_storageproviderrestricted",
+ "donotchange",
+ "locationcipp",
+ "mdo_antiphishingpolicies",
+ "mdo_autoforwardingmode",
+ "mdo_blockmailforward",
+ "mdo_commonattachmentsfilter",
+ "mdo_highconfidencephishaction",
+ "mdo_highconfidencespamaction",
+ "mdo_phishthresholdlevel",
+ "mdo_phisspamacation",
+ "mdo_safeattachmentpolicy",
+ "mdo_safeattachments",
+ "mdo_safedocuments",
+ "mdo_safelinksforOfficeApps",
+ "mdo_safelinksforemail",
+ "mdo_spam_notifications_only_for_admins",
+ "mdo_zapmalware",
+ "mdo_zapphish",
+ "mdo_zapspam",
+ "microsoftonline",
+ "mip_search_auditlog",
+ "winmail",
+ "onmicrosoft.com",
+ "MOERA"
+ ],
+ "import": []
}
diff --git a/generate-placeholders.js b/generate-placeholders.js
index 2f6b614fe9a8..2b34888fca7a 100644
--- a/generate-placeholders.js
+++ b/generate-placeholders.js
@@ -70,6 +70,8 @@ const pages = [
{ title: "Defender Deployment", path: "/security/defender/deployment" },
{ title: "Vulnerabilities", path: "/security/defender/list-defender-tvm" },
{ title: "Device Compliance", path: "/security/reports/list-device-compliance" },
+ { title: "Safe Links", path: "/security/safelinks/safelinks" },
+ { title: "Safe Links Templates", path: "/security/safelinks/safelinks-template" },
{ title: "Applications", path: "/endpoint/applications/list" },
{ title: "Application Queue", path: "/endpoint/applications/queue" },
{ title: "Add Choco App", path: "/endpoint/applications/add-choco-app" },
@@ -81,7 +83,6 @@ const pages = [
{ title: "Profiles", path: "/endpoint/autopilot/list-profiles" },
{ title: "Add Profile", path: "/endpoint/autopilot/add-profile" },
{ title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" },
- { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" },
{ title: "Devices", path: "/endpoint/MEM/devices" },
{ title: "Configuration Policies", path: "/endpoint/MEM/list-policies" },
{ title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" },
@@ -99,6 +100,7 @@ const pages = [
{ title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" },
{ title: "Mailbox Rules", path: "/email/administration/mailbox-rules" },
{ title: "Contacts", path: "/email/administration/contacts" },
+ { title: "Contact Templates", path: "/email/administration/contacts-template" },
{ title: "Quarantine", path: "/email/administration/quarantine" },
{ title: "Tenant Allow/Block Lists", path: "/email/administration/tenant-allow-block-lists" },
{ title: "Mailbox Restore Wizard", path: "/email/tools/mailbox-restore-wizard" },
@@ -121,7 +123,6 @@ const pages = [
{ title: "Message Trace", path: "/email/reports/message-trace" },
{ title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters" },
{ title: "Malware Filters", path: "/email/reports/malware-filters" },
- { title: "Safe Links Filters", path: "/email/reports/safelinks-filters" },
{ title: "Safe Attachments Filters", path: "/email/reports/safeattachments-filters" },
{
title: "Shared Mailbox with Enabled Account",
diff --git a/package.json b/package.json
index 934681339eba..1444ad6102ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "7.1.3",
+ "version": "8.5.2",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
@@ -27,38 +27,38 @@
"@emotion/cache": "11.14.0",
"@emotion/react": "11.14.0",
"@emotion/server": "11.11.0",
- "@emotion/styled": "11.14.0",
+ "@emotion/styled": "11.14.1",
"@heroicons/react": "2.2.0",
"@monaco-editor/react": "^4.6.0",
- "@mui/icons-material": "6.4.7",
- "@mui/lab": "6.0.0-beta.30",
- "@mui/material": "6.4.7",
- "@mui/system": "6.4.7",
- "@mui/x-date-pickers": "7.27.3",
+ "@mui/icons-material": "7.3.2",
+ "@mui/lab": "7.0.0-beta.17",
+ "@mui/material": "7.3.2",
+ "@mui/system": "7.3.2",
+ "@mui/x-date-pickers": "^8.11.1",
"@musement/iso-duration": "^1.0.0",
- "@react-pdf/renderer": "4.3.0",
- "@reduxjs/toolkit": "2.6.1",
+ "@react-pdf/renderer": "^4.3.0",
+ "@reduxjs/toolkit": "2.9.0",
"@tanstack/query-sync-storage-persister": "^5.76.0",
"@tanstack/react-query": "^5.51.11",
"@tanstack/react-query-devtools": "^5.51.11",
"@tanstack/react-query-persist-client": "^5.76.0",
"@tanstack/react-table": "^8.19.2",
- "@tiptap/core": "^2.9.1",
- "@tiptap/extension-heading": "^2.9.1",
- "@tiptap/extension-image": "^2.9.1",
- "@tiptap/extension-table": "^2.9.1",
- "@tiptap/pm": "^2.9.1",
- "@tiptap/react": "^2.9.1",
- "@tiptap/starter-kit": "^2.9.1",
+ "@tiptap/core": "^3.4.1",
+ "@tiptap/extension-heading": "^3.4.1",
+ "@tiptap/extension-image": "^3.4.1",
+ "@tiptap/extension-table": "^3.4.1",
+ "@tiptap/pm": "^3.4.1",
+ "@tiptap/react": "^3.4.1",
+ "@tiptap/starter-kit": "^3.4.1",
"@uiw/react-json-view": "^2.0.0-alpha.30",
- "apexcharts": "4.5.0",
+ "apexcharts": "5.3.5",
"axios": "^1.7.2",
"date-fns": "4.1.0",
"eml-parse-js": "^1.2.0-beta.0",
"export-to-csv": "^1.3.0",
"formik": "2.4.6",
"gray-matter": "4.0.3",
- "i18next": "24.2.3",
+ "i18next": "25.5.2",
"javascript-time-ago": "^2.5.11",
"jspdf": "^3.0.0",
"jspdf-autotable": "^5.0.2",
@@ -67,48 +67,51 @@
"leaflet.markercluster": "^1.5.3",
"lodash.isequal": "4.5.0",
"material-react-table": "^3.0.1",
- "monaco-editor": "^0.52.0",
+ "monaco-editor": "^0.53.0",
"mui-tiptap": "^1.14.0",
"next": "^15.2.2",
"nprogress": "0.2.0",
"numeral": "2.0.6",
"prop-types": "15.8.1",
"punycode": "^2.3.1",
- "react": "19.0.0",
+ "react": "19.1.1",
"react-apexcharts": "1.7.0",
"react-beautiful-dnd": "13.1.1",
"react-copy-to-clipboard": "^5.1.0",
- "react-dom": "19.0.0",
+ "react-dom": "19.1.1",
"react-dropzone": "14.3.8",
- "react-error-boundary": "^5.0.0",
+ "react-error-boundary": "^6.0.0",
"react-grid-layout": "^1.5.0",
"react-hook-form": "^7.53.0",
- "react-hot-toast": "2.5.2",
+ "react-hot-toast": "2.6.0",
"react-html-parser": "^2.0.2",
- "react-i18next": "15.4.1",
+ "react-i18next": "15.7.3",
"react-leaflet": "5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0",
"react-markdown": "10.1.0",
+ "rehype-raw": "^7.0.0",
+ "remark-gfm": "^3.0.1",
"react-media-hook": "^0.5.0",
"react-papaparse": "^4.4.0",
"react-quill": "^2.0.0",
"react-redux": "9.2.0",
"react-syntax-highlighter": "^15.6.1",
"react-time-ago": "^7.3.3",
- "react-window": "^1.8.10",
+ "react-virtuoso": "^4.12.8",
+ "react-window": "^2.1.0",
"redux": "5.0.1",
"redux-devtools-extension": "2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "3.1.0",
- "simplebar": "6.3.0",
- "simplebar-react": "3.3.0",
+ "simplebar": "6.3.2",
+ "simplebar-react": "3.3.2",
"stylis-plugin-rtl": "2.1.1",
- "typescript": "5.8.2",
- "yup": "1.6.1"
+ "typescript": "5.9.2",
+ "yup": "1.7.0"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
- "eslint": "9.22.0",
- "eslint-config-next": "15.2.2"
+ "eslint": "9.35.0",
+ "eslint-config-next": "15.5.2"
}
}
diff --git a/public/assets/illustrations/undraw-into-the-night-nd84.svg b/public/assets/illustrations/undraw-into-the-night-nd84.svg
new file mode 100644
index 000000000000..a7d05162299b
--- /dev/null
+++ b/public/assets/illustrations/undraw-into-the-night-nd84.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/languageList.json b/public/languageList.json
index 3bd0ec3d8e82..769bf0b60f6b 100644
--- a/public/languageList.json
+++ b/public/languageList.json
@@ -3,240 +3,595 @@
"language": "Arabic",
"Geographic area": "Saudi Arabia",
"tag": "ar-SA",
+ "languageTag": "Arabic (ar-SA)",
"LCID": "1025"
},
+ {
+ "language": "Arabic",
+ "Geographic area": "Algeria",
+ "tag": "ar-DZ",
+ "languageTag": "Arabic (ar-DZ)",
+ "LCID": "5121"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Egypt",
+ "tag": "ar-EG",
+ "languageTag": "Arabic (ar-EG)",
+ "LCID": "3073"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Bahrain",
+ "tag": "ar-BH",
+ "languageTag": "Arabic (ar-BH)",
+ "LCID": "15361"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Iraq",
+ "tag": "ar-IQ",
+ "languageTag": "Arabic (ar-IQ)",
+ "LCID": "2049"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Jordan",
+ "tag": "ar-JO",
+ "languageTag": "Arabic (ar-JO)",
+ "LCID": "11265"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Kuwait",
+ "tag": "ar-KW",
+ "languageTag": "Arabic (ar-KW)",
+ "LCID": "13313"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Lebanon",
+ "tag": "ar-LB",
+ "languageTag": "Arabic (ar-LB)",
+ "LCID": "12289"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Libya",
+ "tag": "ar-LY",
+ "languageTag": "Arabic (ar-LY)",
+ "LCID": "4097"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Morocco",
+ "tag": "ar-MA",
+ "languageTag": "Arabic (ar-MA)",
+ "LCID": "6145"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Oman",
+ "tag": "ar-OM",
+ "languageTag": "Arabic (ar-OM)",
+ "LCID": "8193"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Qatar",
+ "tag": "ar-QA",
+ "languageTag": "Arabic (ar-QA)",
+ "LCID": "16385"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Syria",
+ "tag": "ar-SY",
+ "languageTag": "Arabic (ar-SY)",
+ "LCID": "10241"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Tunisia",
+ "tag": "ar-TN",
+ "languageTag": "Arabic (ar-TN)",
+ "LCID": "7169"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "UAE",
+ "tag": "ar-AE",
+ "languageTag": "Arabic (ar-AE)",
+ "LCID": "14337"
+ },
+ {
+ "language": "Arabic",
+ "Geographic area": "Yemen",
+ "tag": "ar-YE",
+ "languageTag": "Arabic (ar-YE)",
+ "LCID": "9217"
+ },
{
"language": "Bulgarian",
"Geographic area": "Bulgaria",
"tag": "bg-BG",
+ "languageTag": "Bulgarian (bg-BG)",
"LCID": "1026"
},
{
"language": "Chinese (Simplified)",
"Geographic area": "People's Republic of China",
"tag": "zh-CN",
+ "languageTag": "Chinese (Simplified) (zh-CN)",
"LCID": "2052"
},
{
"language": "Chinese",
"Geographic area": "Taiwan",
"tag": "zh-TW",
+ "languageTag": "Chinese (zh-TW)",
"LCID": "1028"
},
+ {
+ "language": "Chinese",
+ "Geographic area": "Hong Kong SAR",
+ "tag": "zh-HK",
+ "languageTag": "Chinese (zh-HK)",
+ "LCID": "3076"
+ },
{
"language": "Croatian",
"Geographic area": "Croatia",
"tag": "hr-HR",
+ "languageTag": "Croatian (hr-HR)",
"LCID": "1050"
},
{
"language": "Czech",
"Geographic area": "Czech Republic",
"tag": "cs-CZ",
+ "languageTag": "Czech (cs-CZ)",
"LCID": "1029"
},
{
"language": "Danish",
"Geographic area": "Denmark",
"tag": "da-DK",
+ "languageTag": "Danish (da-DK)",
"LCID": "1030"
},
{
"language": "Dutch",
"Geographic area": "Netherlands",
"tag": "nl-NL",
+ "languageTag": "Dutch (nl-NL)",
"LCID": "1043"
},
{
"language": "English",
"Geographic area": "United States",
"tag": "en-US",
+ "languageTag": "English (en-US)",
"LCID": "1033"
},
+ {
+ "language": "English",
+ "Geographic area": "Australia",
+ "tag": "en-AU",
+ "languageTag": "English (en-AU)",
+ "LCID": "3081"
+ },
+ {
+ "language": "English",
+ "Geographic area": "United Kingdom",
+ "tag": "en-GB",
+ "languageTag": "English (en-GB)",
+ "LCID": "2057"
+ },
+ {
+ "language": "English",
+ "Geographic area": "New Zealand",
+ "tag": "en-NZ",
+ "languageTag": "English (en-NZ)",
+ "LCID": "5129"
+ },
+ {
+ "language": "English",
+ "Geographic area": "Canada",
+ "tag": "en-CA",
+ "languageTag": "English (en-CA)",
+ "LCID": "4105"
+ },
+ {
+ "language": "English",
+ "Geographic area": "South Africa",
+ "tag": "en-ZA",
+ "languageTag": "English (en-ZA)",
+ "LCID": "7177"
+ },
+ {
+ "language": "English",
+ "Geographic area": "Singapore",
+ "tag": "en-SG",
+ "languageTag": "English (en-SG)",
+ "LCID": "4100"
+ },
{
"language": "Estonian",
"Geographic area": "Estonia",
"tag": "et-EE",
+ "languageTag": "Estonian (et-EE)",
"LCID": "1061"
},
{
"language": "Finnish",
"Geographic area": "Finland",
"tag": "fi-FI",
+ "languageTag": "Finnish (fi-FI)",
"LCID": "1035"
},
{
"language": "French",
"Geographic area": "France",
"tag": "fr-FR",
+ "languageTag": "French (fr-FR)",
"LCID": "1036"
},
+ {
+ "language": "French",
+ "Geographic area": "Canada",
+ "tag": "fr-CA",
+ "languageTag": "French (fr-CA)",
+ "LCID": "3084"
+ },
+ {
+ "language": "French",
+ "Geographic area": "Switzerland",
+ "tag": "fr-CH",
+ "languageTag": "French (fr-CH)",
+ "LCID": "4108"
+ },
{
"language": "German",
"Geographic area": "Germany",
"tag": "de-DE",
+ "languageTag": "German (de-DE)",
"LCID": "1031"
},
+ {
+ "language": "German",
+ "Geographic area": "Switzerland",
+ "tag": "de-CH",
+ "languageTag": "German (de-CH)",
+ "LCID": "2055"
+ },
{
"language": "Greek",
"Geographic area": "Greece",
"tag": "el-GR",
+ "languageTag": "Greek (el-GR)",
"LCID": "1032"
},
{
"language": "Hebrew",
"Geographic area": "Israel",
"tag": "he-IL",
+ "languageTag": "Hebrew (he-IL)",
"LCID": "1037"
},
{
"language": "Hindi",
"Geographic area": "India",
"tag": "hi-IN",
+ "languageTag": "Hindi (hi-IN)",
"LCID": "1081"
},
{
"language": "Hungarian",
"Geographic area": "Hungary",
"tag": "hu-HU",
+ "languageTag": "Hungarian (hu-HU)",
"LCID": "1038"
},
{
"language": "Indonesian",
"Geographic area": "Indonesia",
"tag": "id-ID",
+ "languageTag": "Indonesian (id-ID)",
"LCID": "1057"
},
{
"language": "Italian",
"Geographic area": "Italy",
"tag": "it-IT",
+ "languageTag": "Italian (it-IT)",
"LCID": "1040"
},
{
"language": "Japanese",
"Geographic area": "Japan",
"tag": "ja-JP",
+ "languageTag": "Japanese (ja-JP)",
"LCID": "1041"
},
{
"language": "Kazakh",
"Geographic area": "Kazakhstan",
"tag": "kk-KZ",
+ "languageTag": "Kazakh (kk-KZ)",
"LCID": "1087"
},
{
"language": "Korean",
"Geographic area": "Korea",
"tag": "ko-KR",
+ "languageTag": "Korean (ko-KR)",
"LCID": "1042"
},
{
"language": "Latvian",
"Geographic area": "Latvia",
"tag": "lv-LV",
+ "languageTag": "Latvian (lv-LV)",
"LCID": "1062"
},
{
"language": "Lithuanian",
"Geographic area": "Lithuania",
"tag": "lt-LT",
+ "languageTag": "Lithuanian (lt-LT)",
"LCID": "1063"
},
{
"language": "Malay",
"Geographic area": "Malaysia",
"tag": "ms-MY",
+ "languageTag": "Malay (ms-MY)",
"LCID": "1086"
},
{
"language": "Norwegian (BokmΓ₯l)",
"Geographic area": "Norway",
"tag": "nb-NO",
+ "languageTag": "Norwegian (BokmΓ₯l) (nb-NO)",
"LCID": "1044"
},
+ {
+ "language": "Persian",
+ "Geographic area": "Iran",
+ "tag": "fa-IR",
+ "languageTag": "Persian (fa-IR)",
+ "LCID": "1065"
+ },
{
"language": "Polish",
"Geographic area": "Poland",
"tag": "pl-PL",
+ "languageTag": "Polish (pl-PL)",
"LCID": "1045"
},
{
"language": "Portuguese",
"Geographic area": "Brazil",
"tag": "pt-BR",
+ "languageTag": "Portuguese (pt-BR)",
"LCID": "1046"
},
{
"language": "Portuguese",
"Geographic area": "Portugal",
"tag": "pt-PT",
+ "languageTag": "Portuguese (pt-PT)",
"LCID": "2070"
},
{
"language": "Romanian",
"Geographic area": "Romania",
"tag": "ro-RO",
+ "languageTag": "Romanian (ro-RO)",
"LCID": "1048"
},
{
"language": "Russian",
"Geographic area": "Russia",
"tag": "ru-RU",
+ "languageTag": "Russian (ru-RU)",
"LCID": "1049"
},
{
"language": "Serbian (Latin)",
"Geographic area": "Serbia",
"tag": "sr-latn-RS",
+ "languageTag": "Serbian (Latin) (sr-latn-RS)",
"LCID": "2074"
},
{
"language": "Slovak",
"Geographic area": "Slovakia",
"tag": "sk-SK",
+ "languageTag": "Slovak (sk-SK)",
"LCID": "1051"
},
{
"language": "Slovenian",
"Geographic area": "Slovenia",
"tag": "sl-SI",
+ "languageTag": "Slovenian (sl-SI)",
"LCID": "1060"
},
{
"language": "Spanish",
"Geographic area": "Spain",
"tag": "es-ES",
+ "languageTag": "Spanish (es-ES)",
"LCID": "3082"
},
+ {
+ "language": "Spanish",
+ "Geographic area": "Argentina",
+ "tag": "es-AR",
+ "languageTag": "Spanish (es-AR)",
+ "LCID": "11274"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Bolivia",
+ "tag": "es-BO",
+ "languageTag": "Spanish (es-BO)",
+ "LCID": "16394"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Chile",
+ "tag": "es-CL",
+ "languageTag": "Spanish (es-CL)",
+ "LCID": "13322"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Colombia",
+ "tag": "es-CO",
+ "languageTag": "Spanish (es-CO)",
+ "LCID": "9226"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Costa Rica",
+ "tag": "es-CR",
+ "languageTag": "Spanish (es-CR)",
+ "LCID": "5130"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Dominican Republic",
+ "tag": "es-DO",
+ "languageTag": "Spanish (es-DO)",
+ "LCID": "7178"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Ecuador",
+ "tag": "es-EC",
+ "languageTag": "Spanish (es-EC)",
+ "LCID": "12298"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "El Salvador",
+ "tag": "es-SV",
+ "languageTag": "Spanish (es-SV)",
+ "LCID": "17418"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Guatemala",
+ "tag": "es-GT",
+ "languageTag": "Spanish (es-GT)",
+ "LCID": "4106"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Honduras",
+ "tag": "es-HN",
+ "languageTag": "Spanish (es-HN)",
+ "LCID": "18442"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Mexico",
+ "tag": "es-MX",
+ "languageTag": "Spanish (es-MX)",
+ "LCID": "2058"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Nicaragua",
+ "tag": "es-NI",
+ "languageTag": "Spanish (es-NI)",
+ "LCID": "19466"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Panama",
+ "tag": "es-PA",
+ "languageTag": "Spanish (es-PA)",
+ "LCID": "6154"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Paraguay",
+ "tag": "es-PY",
+ "languageTag": "Spanish (es-PY)",
+ "LCID": "15370"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Peru",
+ "tag": "es-PE",
+ "languageTag": "Spanish (es-PE)",
+ "LCID": "10250"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Uruguay",
+ "tag": "es-UY",
+ "languageTag": "Spanish (es-UY)",
+ "LCID": "14346"
+ },
+ {
+ "language": "Spanish",
+ "Geographic area": "Venezuela",
+ "tag": "es-VE",
+ "languageTag": "Spanish (es-VE)",
+ "LCID": "8202"
+ },
{
"language": "Swedish",
"Geographic area": "Sweden",
"tag": "sv-SE",
+ "languageTag": "Swedish (sv-SE)",
"LCID": "1053"
},
{
"language": "Thai",
"Geographic area": "Thailand",
"tag": "th-TH",
+ "languageTag": "Thai (th-TH)",
"LCID": "1054"
},
{
"language": "Turkish",
"Geographic area": "Turkey",
"tag": "tr-TR",
+ "languageTag": "Turkish (tr-TR)",
"LCID": "1055"
},
{
"language": "Ukrainian",
- "Geographic area": "Ukrainian",
+ "Geographic area": "Ukraine",
"tag": "uk-UA",
+ "languageTag": "Ukrainian (uk-UA)",
"LCID": "1058"
},
+ {
+ "language": "Urdu",
+ "Geographic area": "Pakistan",
+ "tag": "ur-PK",
+ "languageTag": "Urdu (ur-PK)",
+ "LCID": "1056"
+ },
{
"language": "Vietnamese",
"Geographic area": "Vietnam",
"tag": "vi-VN",
+ "languageTag": "Vietnamese (vi-VN)",
"LCID": "1066"
+ },
+ {
+ "language": "Welsh",
+ "Geographic area": "Wales",
+ "tag": "cy-GB",
+ "languageTag": "Welsh (cy-GB)",
+ "LCID": "1106"
}
-]
+]
\ No newline at end of file
diff --git a/public/permissionsList.json b/public/permissionsList.json
index c91c29126036..8ec55a707b7e 100644
--- a/public/permissionsList.json
+++ b/public/permissionsList.json
@@ -7969,5 +7969,25 @@
"userConsentDescription": "Allows the app to manage workforce integrations, to synchronize data from Microsoft Teams Shifts, on your behalf.",
"userConsentDisplayName": "Read and write workforce integrations",
"value": "WorkforceIntegration.ReadWrite.All"
+ },
+ {
+ "description": "Read and Modify Tenant-Acquired Telephone Number Details",
+ "displayName": "Read and Modify Tenant-Acquired Telephone Number Details",
+ "id": "424b07a8-1209-4d17-9fe4-9018a93a1024",
+ "isEnabled": true,
+ "Origin": "Delegated",
+ "userConsentDescription": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.",
+ "userConsentDisplayName": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.",
+ "value": "TeamsTelephoneNumber.ReadWrite.All"
+ },
+ {
+ "description": "Read and Modify Tenant-Acquired Telephone Number Details",
+ "displayName": "Read and Modify Tenant-Acquired Telephone Number Details",
+ "id": "0a42382f-155c-4eb1-9bdc-21548ccaa387",
+ "isEnabled": true,
+ "Origin": "Application",
+ "userConsentDescription": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.",
+ "userConsentDisplayName": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc.",
+ "value": "TeamsTelephoneNumber.ReadWrite.All"
}
]
diff --git a/public/reportImages/board.jpg b/public/reportImages/board.jpg
new file mode 100644
index 000000000000..05c3e51ddc34
Binary files /dev/null and b/public/reportImages/board.jpg differ
diff --git a/public/reportImages/city.jpg b/public/reportImages/city.jpg
new file mode 100644
index 000000000000..155439cf30f2
Binary files /dev/null and b/public/reportImages/city.jpg differ
diff --git a/public/reportImages/glasses.jpg b/public/reportImages/glasses.jpg
new file mode 100644
index 000000000000..b41d200ea650
Binary files /dev/null and b/public/reportImages/glasses.jpg differ
diff --git a/public/reportImages/laptop.jpg b/public/reportImages/laptop.jpg
new file mode 100644
index 000000000000..31a14fc4383e
Binary files /dev/null and b/public/reportImages/laptop.jpg differ
diff --git a/public/reportImages/soc.jpg b/public/reportImages/soc.jpg
new file mode 100644
index 000000000000..f8da4eba5139
Binary files /dev/null and b/public/reportImages/soc.jpg differ
diff --git a/public/reportImages/working.jpg b/public/reportImages/working.jpg
new file mode 100644
index 000000000000..c979c21b1ea0
Binary files /dev/null and b/public/reportImages/working.jpg differ
diff --git a/public/sponsors/domotz-dark.png b/public/sponsors/domotz-dark.png
new file mode 100644
index 000000000000..ac26f6f0f6ad
Binary files /dev/null and b/public/sponsors/domotz-dark.png differ
diff --git a/public/sponsors/domotz-light.png b/public/sponsors/domotz-light.png
new file mode 100644
index 000000000000..dab40b067807
Binary files /dev/null and b/public/sponsors/domotz-light.png differ
diff --git a/public/version.json b/public/version.json
index fea50d299123..571782873e0e 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "7.5.3"
-}
\ No newline at end of file
+ "version": "8.6.1"
+}
diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx
index 35554764168e..67f602114c3c 100644
--- a/src/api/ApiCall.jsx
+++ b/src/api/ApiCall.jsx
@@ -1,10 +1,4 @@
-import {
- keepPreviousData,
- useInfiniteQuery,
- useMutation,
- useQuery,
- useQueryClient,
-} from "@tanstack/react-query";
+import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios, { isAxiosError } from "axios";
import { useDispatch } from "react-redux";
import { showToast } from "../store/toasts";
@@ -26,6 +20,9 @@ export function ApiGetCall(props) {
refetchOnMount = true,
refetchOnReconnect = true,
keepPreviousData = false,
+ refetchInterval = false,
+ responseType = "json",
+ convertToDataUrl = false,
} = props;
const queryClient = useQueryClient();
const dispatch = useDispatch();
@@ -81,7 +78,25 @@ export function ApiGetCall(props) {
if (relatedQueryKeys) {
const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys];
setTimeout(() => {
- clearKeys.forEach((key) => {
+ // Separate wildcard patterns from exact keys
+ const wildcardPatterns = clearKeys
+ .filter((key) => key.endsWith("*"))
+ .map((key) => key.slice(0, -1));
+ const exactKeys = clearKeys.filter((key) => !key.endsWith("*"));
+
+ // Use single predicate call for all wildcard patterns
+ if (wildcardPatterns.length > 0) {
+ queryClient.invalidateQueries({
+ predicate: (query) => {
+ if (!query.queryKey || !query.queryKey[0]) return false;
+ const queryKeyStr = String(query.queryKey[0]);
+ return wildcardPatterns.some((pattern) => queryKeyStr.startsWith(pattern));
+ },
+ });
+ }
+
+ // Handle exact keys
+ exactKeys.forEach((key) => {
queryClient.invalidateQueries({ queryKey: [key] });
});
}, 1000);
@@ -94,19 +109,50 @@ export function ApiGetCall(props) {
headers: {
"Content-Type": "application/json",
},
+ responseType: responseType,
});
+
+ let responseData = response.data;
+
+ // Convert blob to data URL if requested
+ if (convertToDataUrl && responseType === "blob" && response.data) {
+ responseData = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsDataURL(response.data);
+ });
+ }
+
if (onResult) {
- onResult(response.data); // Emit each result as it arrives
+ onResult(responseData); // Emit each result as it arrives
}
if (relatedQueryKeys) {
const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys];
setTimeout(() => {
- clearKeys.forEach((key) => {
+ // Separate wildcard patterns from exact keys
+ const wildcardPatterns = clearKeys
+ .filter((key) => key.endsWith("*"))
+ .map((key) => key.slice(0, -1));
+ const exactKeys = clearKeys.filter((key) => !key.endsWith("*"));
+
+ // Use single predicate call for all wildcard patterns
+ if (wildcardPatterns.length > 0) {
+ queryClient.invalidateQueries({
+ predicate: (query) => {
+ if (!query.queryKey || !query.queryKey[0]) return false;
+ const queryKeyStr = String(query.queryKey[0]);
+ return wildcardPatterns.some((pattern) => queryKeyStr.startsWith(pattern));
+ },
+ });
+ }
+
+ // Handle exact keys
+ exactKeys.forEach((key) => {
queryClient.invalidateQueries({ queryKey: [key] });
});
}, 1000);
}
- return response.data;
+ return responseData;
}
},
staleTime: staleTime,
@@ -114,6 +160,7 @@ export function ApiGetCall(props) {
refetchOnMount: refetchOnMount,
refetchOnReconnect: refetchOnReconnect,
keepPreviousData: keepPreviousData,
+ refetchInterval: refetchInterval,
retry: retryFn,
});
return queryInfo;
@@ -121,6 +168,7 @@ export function ApiGetCall(props) {
export function ApiPostCall({ relatedQueryKeys, onResult }) {
const queryClient = useQueryClient();
+
const mutation = useMutation({
mutationFn: async (props) => {
const { url, data, bulkRequest } = props;
@@ -148,9 +196,43 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) {
const clearKeys = Array.isArray(relatedQueryKeys) ? relatedQueryKeys : [relatedQueryKeys];
setTimeout(() => {
if (relatedQueryKeys === "*") {
+ console.log("Invalidating all queries");
queryClient.invalidateQueries();
} else {
- clearKeys.forEach((key) => {
+ // Separate wildcard patterns from exact keys
+ const wildcardPatterns = clearKeys
+ .filter((key) => key.endsWith("*"))
+ .map((key) => key.slice(0, -1));
+ const exactKeys = clearKeys.filter((key) => !key.endsWith("*"));
+
+ // Use single predicate call for all wildcard patterns
+ if (wildcardPatterns.length > 0) {
+ queryClient.invalidateQueries({
+ predicate: (query) => {
+ if (!query.queryKey || !query.queryKey[0]) return false;
+ const queryKeyStr = String(query.queryKey[0]);
+ const matches = wildcardPatterns.some((pattern) =>
+ queryKeyStr.startsWith(pattern)
+ );
+
+ // Debug logging for each query check
+ if (matches) {
+ console.log("Invalidating query:", {
+ queryKey: query.queryKey,
+ queryKeyStr,
+ matchedPattern: wildcardPatterns.find((pattern) =>
+ queryKeyStr.startsWith(pattern)
+ ),
+ });
+ }
+
+ return matches;
+ },
+ });
+ }
+
+ // Handle exact keys
+ exactKeys.forEach((key) => {
queryClient.invalidateQueries({ queryKey: [key] });
});
}
diff --git a/src/components/CSVReader.jsx b/src/components/CSVReader.jsx
index d34d9ee79007..be3f6f67f02a 100644
--- a/src/components/CSVReader.jsx
+++ b/src/components/CSVReader.jsx
@@ -1,86 +1,67 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { useCSVReader, lightenDarkenColor, formatFileSize } from "react-papaparse";
+import { Box, Typography, useTheme } from "@mui/material";
+import { CloudUpload } from "@mui/icons-material";
-const GREY = "#CCC";
-const GREY_LIGHT = "rgba(255, 255, 255, 0.4)";
+/*
+ * These colors define our remove button states. The light version is
+ * calculated rather than hardcoded - a small touch that ensures
+ * consistent color relationships no matter what base color we use.
+ *
+ * Sometimes it's these little details that make a component feel polished.
+ */
const DEFAULT_REMOVE_HOVER_COLOR = "#A01919";
const REMOVE_HOVER_COLOR_LIGHT = lightenDarkenColor(DEFAULT_REMOVE_HOVER_COLOR, 40);
-const GREY_DIM = "#686868";
-const styles = {
- zone: {
- alignItems: "center",
- border: `2px dashed`,
- borderRadius: 20,
- display: "flex",
- flexDirection: "column",
- height: "100%",
- justifyContent: "center",
- padding: 20,
- },
- file: {
- background: "linear-gradient(to bottom, #aaa, #aaa)",
- borderRadius: 20,
- display: "flex",
- height: 60,
- width: 120,
- position: "relative",
- zIndex: 10,
- flexDirection: "column",
- justifyContent: "center",
- },
- info: {
- alignItems: "center",
- display: "flex",
- flexDirection: "column",
- paddingLeft: 10,
- paddingRight: 10,
- },
- size: {
- borderRadius: 3,
- marginBottom: "0.5em",
- justifyContent: "center",
- display: "flex",
- },
- name: {
- borderRadius: 3,
- fontSize: 12,
- marginBottom: "0.5em",
- },
- progressBar: {
- bottom: 14,
- position: "absolute",
- width: "100%",
- paddingLeft: 10,
- paddingRight: 10,
- },
- zoneHover: {
- borderColor: GREY_DIM,
- },
- default: {
- borderColor: GREY,
- },
- remove: {
- height: 23,
- position: "absolute",
- right: 6,
- top: 6,
- width: 23,
- },
-};
-
-export default function CSVReader(props) {
+/*
+ * This component has evolved from a simple file input to a polished
+ * upload zone that maintains state between wizard steps. It's a good
+ * example of how components grow with requirements while trying to
+ * keep their core purpose clear.
+ *
+ * The journey to this version taught us about:
+ * - Proper event handling with third-party libraries
+ * - State persistence in multi-step forms
+ * - The value of simple solutions (sessionStorage vs complex state)
+ */
+export default function CSVReader({ config, onDrop, onRemove }) {
const { CSVReader } = useCSVReader();
const [zoneHover, setZoneHover] = useState(false);
const [removeHoverColor, setRemoveHoverColor] = useState(DEFAULT_REMOVE_HOVER_COLOR);
+ const [storedFile, setStoredFile] = useState(null);
+ const theme = useTheme();
+
+ /*
+ * On mount, we check sessionStorage for file details. This lets us
+ * restore the preview when users navigate back to this step.
+ *
+ * It's a simple solution that works well - sometimes the best
+ * approaches don't need complex state management. The fact that
+ * it "just works" is a feature, not a bug.
+ */
+ useEffect(() => {
+ const fileName = sessionStorage.getItem('csvFileName');
+ const fileSize = sessionStorage.getItem('csvFileSize');
+ if (fileName && fileSize) {
+ console.log('Restoring file preview:', fileName);
+ setStoredFile({
+ name: fileName,
+ size: parseInt(fileSize, 10)
+ });
+ }
+ }, []);
return (
{
- //call the ondrop function from the props, passing the results.
- props.onDrop(results.data);
+ config={config}
+ onUploadAccepted={(results, file) => {
+ console.log('File accepted:', file.name);
+ onDrop(results.data);
setZoneHover(false);
+ setStoredFile(file);
+ // Store file details for persistence between steps
+ sessionStorage.setItem('csvFileName', file.name);
+ sessionStorage.setItem('csvFileSize', file.size.toString());
}}
onDragOver={(event) => {
event.preventDefault();
@@ -92,46 +73,91 @@ export default function CSVReader(props) {
}}
>
{({ getRootProps, acceptedFile, ProgressBar, getRemoveFileProps, Remove }) => (
- <>
-
+
+
+ {!appIdInfo.isLoading &&
+ !appIdInfo?.data?.applicationId && (
+
+ The Application ID is not valid. Please check your configuration.
+
+ )
+ }
+
+ {showResults && (
+
+ {deviceCodeInfo && authInProgress ? (
+
+ Device Code Authentication
+
+ To sign in, use a web browser to open the page {deviceCodeInfo.verification_uri} and enter the code {deviceCodeInfo.user_code} to authenticate.
+
+
+ Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes
+
+
+ ) : tokens.accessToken ? (
+
+ Authentication Successful
+
+ You've successfully refreshed your token using device code flow.
+
+ {tokens.tenantId && (
+
+ Tenant ID: {tokens.tenantId}
+
+ )}
+
+ ) : authError ? (
+
+ Authentication Error: {authError.errorCode}
+ {authError.errorMessage}
+
+ Time: {authError.timestamp}
+
+
+
+
+
+ ) : null}
+
+ )}
+
+ );
+};
+
+export default CIPPDeviceCodeButton;
\ No newline at end of file
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx
new file mode 100644
index 000000000000..88e517a2139d
--- /dev/null
+++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx
@@ -0,0 +1,683 @@
+import { useState, useEffect } from "react";
+import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material";
+import { ApiGetCall } from "../../api/ApiCall";
+import { CippCopyToClipBoard } from "./CippCopyToClipboard";
+
+export const CIPPM365OAuthButton = ({
+ onAuthSuccess,
+ onAuthError,
+ buttonText = "Login with Microsoft",
+ showResults = true,
+ showSuccessAlert = true,
+ scope = "https://graph.microsoft.com/.default offline_access profile openid",
+ useDeviceCode = false,
+ applicationId = null,
+ autoStartDeviceLogon = false,
+ validateServiceAccount = true,
+}) => {
+ const [authInProgress, setAuthInProgress] = useState(false);
+ const [authError, setAuthError] = useState(null);
+ const [deviceCodeInfo, setDeviceCodeInfo] = useState(null);
+ const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false);
+ const [isServiceAccount, setIsServiceAccount] = useState(true);
+ const [tokens, setTokens] = useState({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ const appIdInfo = ApiGetCall({
+ url: `/api/ExecListAppId`,
+ waiting: true,
+ });
+
+ useEffect(() => {
+ appIdInfo.refetch();
+ }, []);
+
+ const handleCloseError = () => {
+ setAuthError(null);
+ };
+
+ const checkIsServiceAccount = (username) => {
+ if (!username || !validateServiceAccount) return true; // If no username or validation disabled, don't show warning
+
+ const lowerUsername = username.toLowerCase();
+ return lowerUsername.includes("service") || lowerUsername.includes("cipp");
+ };
+
+ // Function to retrieve device code
+ const retrieveDeviceCode = async () => {
+ setCodeRetrievalInProgress(true);
+ setAuthError(null);
+
+ // Refetch appId to ensure we have the latest
+ await appIdInfo.refetch();
+
+ try {
+ // Get the application ID to use
+ const appId =
+ applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID
+
+ // Request device code from our API endpoint
+ const deviceCodeResponse = await fetch(
+ `/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(
+ scope
+ )}`
+ );
+ const deviceCodeData = await deviceCodeResponse.json();
+
+ if (deviceCodeResponse.ok && deviceCodeData.user_code) {
+ // Store device code info
+ setDeviceCodeInfo(deviceCodeData);
+ } else {
+ // Error getting device code
+ setAuthError({
+ errorCode: deviceCodeData.error || "device_code_error",
+ errorMessage: deviceCodeData.error_description || "Failed to get device code",
+ timestamp: new Date().toISOString(),
+ });
+ }
+ } catch (error) {
+ setAuthError({
+ errorCode: "device_code_error",
+ errorMessage: error.message || "An error occurred retrieving device code",
+ timestamp: new Date().toISOString(),
+ });
+ } finally {
+ setCodeRetrievalInProgress(false);
+ }
+ };
+
+ // Device code authentication function - opens popup and starts polling
+ const handleDeviceCodeAuthentication = async () => {
+ // Refetch appId to ensure we have the latest
+ await appIdInfo.refetch();
+
+ if (!deviceCodeInfo) {
+ // If we don't have a device code yet, retrieve it first
+ await retrieveDeviceCode();
+ return;
+ }
+
+ setAuthInProgress(true);
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ try {
+ // Get the application ID to use - refetch already happened at the start of this function
+ const appId =
+ applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID
+
+ // Open popup to device login page
+ const width = 500;
+ const height = 600;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+
+ const popup = window.open(
+ "https://microsoft.com/devicelogin",
+ "deviceLoginPopup",
+ `width=${width},height=${height},left=${left},top=${top}`
+ );
+
+ // Start polling for token
+ const pollInterval = deviceCodeInfo.interval || 5;
+ const expiresIn = deviceCodeInfo.expires_in || 900;
+ const startTime = Date.now();
+
+ const pollForToken = async () => {
+ // Check if we've exceeded the expiration time
+ if (Date.now() - startTime >= expiresIn * 1000) {
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ setAuthError({
+ errorCode: "timeout",
+ errorMessage: "Device code authentication timed out",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ return;
+ }
+
+ try {
+ // Poll for token using our API endpoint
+ const tokenResponse = await fetch(
+ `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}`
+ );
+ const tokenData = await tokenResponse.json();
+
+ if (tokenResponse.ok && tokenData.status === "success") {
+ // Successfully got token
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ handleTokenResponse(tokenData);
+ } else if (
+ tokenData.error === "authorization_pending" ||
+ tokenData.status === "pending"
+ ) {
+ // User hasn't completed authentication yet, continue polling
+ setTimeout(pollForToken, pollInterval * 1000);
+ } else if (tokenData.error === "slow_down") {
+ // Server asking us to slow down polling
+ setTimeout(pollForToken, (pollInterval + 5) * 1000);
+ } else {
+ // Other error
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+ setAuthError({
+ errorCode: tokenData.error || "token_error",
+ errorMessage: tokenData.error_description || "Failed to get token",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ }
+ } catch (error) {
+ setTimeout(pollForToken, pollInterval * 1000);
+ }
+ };
+
+ // Start polling
+ setTimeout(pollForToken, pollInterval * 1000);
+ } catch (error) {
+ setAuthError({
+ errorCode: "device_code_error",
+ errorMessage: error.message || "An error occurred during device code authentication",
+ timestamp: new Date().toISOString(),
+ });
+ setAuthInProgress(false);
+ }
+ };
+
+ // Process token response (common for both auth methods)
+ const handleTokenResponse = (tokenData) => {
+ // Extract token information
+ const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000);
+ // Refresh tokens typically last for 90 days, but this can vary
+ const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
+
+ // Extract information from ID token if available
+ let username = "unknown user";
+ let tenantId = "unknown tenant";
+ let onmicrosoftDomain = null;
+
+ if (tokenData.id_token) {
+ try {
+ const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1]));
+
+ username =
+ idTokenPayload.preferred_username ||
+ idTokenPayload.email ||
+ idTokenPayload.upn ||
+ idTokenPayload.name ||
+ "unknown user";
+
+ if (idTokenPayload.tid) {
+ tenantId = idTokenPayload.tid;
+ }
+
+ if (username && username.includes("@") && username.includes(".onmicrosoft.com")) {
+ onmicrosoftDomain = username.split("@")[1];
+ } else if (idTokenPayload.iss) {
+ const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//);
+ if (issuerMatch && issuerMatch[1]) {
+ }
+ }
+ setIsServiceAccount(checkIsServiceAccount(username));
+ } catch (error) {}
+ }
+
+ // Create token result object
+ const tokenResult = {
+ accessToken: tokenData.access_token,
+ refreshToken: tokenData.refresh_token,
+ accessTokenExpiresOn: accessTokenExpiresOn,
+ refreshTokenExpiresOn: refreshTokenExpiresOn,
+ username: username,
+ tenantId: tenantId,
+ onmicrosoftDomain: onmicrosoftDomain,
+ };
+
+ setTokens(tokenResult);
+ setDeviceCodeInfo(null);
+
+ if (onAuthSuccess) onAuthSuccess(tokenResult);
+
+ // Update UI state
+ setAuthInProgress(false);
+ setIsServiceAccount(checkIsServiceAccount(username));
+ };
+
+ // MSAL-like authentication function
+ const handleMsalAuthentication = async () => {
+ // Clear previous authentication state when starting a new authentication
+ setAuthInProgress(true);
+ setAuthError(null);
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+
+ // Refetch app ID info to ensure we have the latest
+ await appIdInfo.refetch();
+
+ // Get the application ID to use - now we're sure to have the latest after the await
+ const appId = applicationId || appIdInfo?.data?.applicationId;
+
+ // Generate MSAL-like authentication parameters
+ const msalConfig = {
+ auth: {
+ clientId: appId,
+ authority: `https://login.microsoftonline.com/common`,
+ redirectUri: `${window.location.origin}/authredirect`,
+ },
+ };
+
+ // Define the request object similar to MSAL
+ const loginRequest = {
+ scopes: [scope],
+ };
+
+ // Generate PKCE code verifier and challenge
+ const generateCodeVerifier = () => {
+ const array = new Uint8Array(32);
+ window.crypto.getRandomValues(array);
+ return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join("");
+ };
+
+ const codeVerifier = generateCodeVerifier();
+ const codeChallenge = codeVerifier;
+ const state = Math.random().toString(36).substring(2, 15);
+ const authUrl =
+ `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` +
+ `client_id=${appId}` +
+ `&response_type=code` +
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}/authredirect` +
+ `&scope=${encodeURIComponent(scope)}` +
+ `&code_challenge=${codeChallenge}` +
+ `&code_challenge_method=plain` +
+ `&state=${state}` +
+ `&prompt=select_account`;
+
+ // Open popup for authentication
+ const width = 500;
+ const height = 600;
+ const left = window.screen.width / 2 - width / 2;
+ const top = window.screen.height / 2 - height / 2;
+
+ const popup = window.open(
+ authUrl,
+ "msalAuthPopup",
+ `width=${width},height=${height},left=${left},top=${top}`
+ );
+
+ // Function to actually exchange the authorization code for tokens
+ const handleAuthorizationCode = async (code, receivedState) => {
+ // Verify the state parameter matches what we sent (security check)
+ if (receivedState !== state) {
+ const errorMessage = "State mismatch in auth response - possible CSRF attack";
+ const error = {
+ errorCode: "state_mismatch",
+ errorMessage: errorMessage,
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+ setAuthInProgress(false);
+ return;
+ }
+ try {
+ // Prepare the token request
+ const tokenRequest = {
+ grant_type: "authorization_code",
+ client_id: appId,
+ code: code,
+ redirect_uri: `${window.location.origin}/authredirect`,
+ code_verifier: codeVerifier,
+ };
+
+ // Make the token request through our API proxy to avoid origin header issues
+ const tokenResponse = await fetch(`/api/ExecTokenExchange`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ tokenRequest,
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ tenantId: appId, // Pass the tenant ID to retrieve the correct client secret
+ }),
+ });
+
+ // Parse the token response
+ const tokenData = await tokenResponse.json();
+
+ // Check if the response contains an error
+ if (tokenData.error) {
+ const error = {
+ errorCode: tokenData.error || "token_error",
+ errorMessage:
+ tokenData.error_description || "Failed to exchange authorization code for tokens",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+ setAuthInProgress(false);
+ return;
+ }
+
+ if (tokenResponse.ok) {
+ // If we have a refresh token, store it
+ if (tokenData.refresh_token) {
+ try {
+ // Extract tid from access_token jwt base64
+ const accessTokenParts = tokenData.access_token.split(".");
+ const accessTokenPayload = JSON.parse(atob(accessTokenParts[1] || ""));
+ tokenData.tid = accessTokenPayload.tid;
+ const refreshResponse = await fetch(`/api/ExecUpdateRefreshToken`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ tenantId: tokenData.tid,
+ refreshtoken: tokenData.refresh_token,
+ tenantMode: tokenData.tenantMode,
+ allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement,
+ }),
+ });
+
+ if (!refreshResponse.ok) {
+ console.warn("Failed to store refresh token, but continuing with authentication");
+ }
+ } catch (error) {
+ console.error("Failed to store refresh token:", error);
+ }
+ }
+
+ handleTokenResponse(tokenData);
+ } else {
+ // Handle token error - display in error box instead of throwing
+ const error = {
+ errorCode: tokenData.error || "token_error",
+ errorMessage:
+ tokenData.error_description || "Failed to exchange authorization code for tokens",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+ }
+ } catch (error) {
+ const errorObj = {
+ errorCode: "token_exchange_error",
+ errorMessage: error.message || "Failed to exchange authorization code for tokens",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(errorObj);
+ if (onAuthError) onAuthError(errorObj);
+ } finally {
+ // Close the popup window if it's still open
+ if (popup && !popup.closed) {
+ popup.close();
+ }
+
+ // Update UI state
+ setAuthInProgress(false);
+ }
+ };
+
+ // Monitor for the redirect with the authorization code
+ // This is what MSAL does internally
+ const checkPopupLocation = setInterval(() => {
+ if (!popup || popup.closed) {
+ clearInterval(checkPopupLocation);
+
+ // If authentication is still in progress when popup closes, it's an error
+ if (authInProgress) {
+ const errorMessage = "Authentication was cancelled. Please try again.";
+ const error = {
+ errorCode: "user_cancelled",
+ errorMessage: errorMessage,
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+
+ // Ensure we're not showing any previous success state
+ setTokens({
+ accessToken: null,
+ refreshToken: null,
+ accessTokenExpiresOn: null,
+ refreshTokenExpiresOn: null,
+ username: null,
+ tenantId: null,
+ onmicrosoftDomain: null,
+ });
+ }
+
+ setAuthInProgress(false);
+ return;
+ }
+
+ try {
+ // Try to access the popup location to check for the authorization code
+ const currentUrl = popup.location.href;
+
+ // Check if the URL contains a code parameter (authorization code)
+ if (currentUrl.includes("code=") && currentUrl.includes("state=")) {
+ clearInterval(checkPopupLocation);
+ // Parse the URL to extract the code and state
+ const urlParams = new URLSearchParams(popup.location.search);
+ const code = urlParams.get("code");
+ const receivedState = urlParams.get("state");
+
+ // Process the authorization code
+ handleAuthorizationCode(code, receivedState);
+ }
+
+ // Check for error in the URL
+ if (currentUrl.includes("error=")) {
+ clearInterval(checkPopupLocation);
+ // Parse the URL to extract the error details
+ const urlParams = new URLSearchParams(popup.location.search);
+ const errorCode = urlParams.get("error");
+ const errorDescription = urlParams.get("error_description");
+
+ // Set the error state
+ const error = {
+ errorCode: errorCode,
+ errorMessage: errorDescription || "Unknown authentication error",
+ timestamp: new Date().toISOString(),
+ };
+ setAuthError(error);
+ if (onAuthError) onAuthError(error);
+
+ // Close the popup
+ popup.close();
+ setAuthInProgress(false);
+ }
+ } catch (error) {
+ // This will throw an error when the popup is on a different domain
+ // due to cross-origin restrictions, which is normal during auth flow
+ // Just continue monitoring
+ }
+ }, 500);
+
+ // Also monitor for popup closing as a fallback
+ };
+
+ // Auto-start device code retrieval if requested
+ useEffect(() => {
+ if (
+ useDeviceCode &&
+ autoStartDeviceLogon &&
+ !codeRetrievalInProgress &&
+ !deviceCodeInfo &&
+ !tokens.accessToken &&
+ appIdInfo?.data
+ ) {
+ retrieveDeviceCode();
+ }
+ }, [
+ useDeviceCode,
+ autoStartDeviceLogon,
+ codeRetrievalInProgress,
+ deviceCodeInfo,
+ tokens.accessToken,
+ appIdInfo?.data,
+ ]);
+
+ return (
+
+ {!applicationId &&
+ !appIdInfo.isLoading &&
+ appIdInfo?.data && // Only check if data is available
+ !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
+ appIdInfo?.data?.applicationId
+ ) && (
+
+ The Application ID is not valid. Please check your configuration.
+
+ )}
+
+ {showResults && (
+
+ {deviceCodeInfo ? (
+
+ Application Creation
+
+ {authInProgress ? (
+ <>
+ When asked to log onto an account, please use a{" "}
+ CIPP Service Account. Enter this code to authenticate:{" "}
+ >
+ ) : (
+ <>
+ Click the button below to authenticate. When asked to log onto an account,
+ please use a CIPP Service Account. You will need to enter this
+ code:{" "}
+ >
+ )}
+
+
+
+ {authInProgress ? (
+ <>
+ If the popup was blocked or you closed it, you can also go to{" "}
+ microsoft.com/devicelogin manually and enter the code shown
+ above.
+ >
+ ) : (
+ <>
+ When you click the button below, a popup will open to{" "}
+ microsoft.com/devicelogin where you'll enter this code.
+ >
+ )}
+
+
+ Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes
+
+
+ ) : tokens.accessToken ? (
+ <>
+ {showSuccessAlert ? (
+
+ Authentication Successful
+
+ You've successfully refreshed your token. The account you're using for
+ authentication is: {tokens.username}
+
+
+ Tenant ID: {tokens.tenantId}
+ {tokens.onmicrosoftDomain && (
+ <>
+ {" "}
+ | Domain: {tokens.onmicrosoftDomain}
+ >
+ )}
+
+
+ Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()}
+
+
+ ) : null}
+
+ {!isServiceAccount && (
+
+ Service Account Required
+
+ CIPP requires a service account for authentication. The account you're using (
+ {tokens.username}) does not appear to be a service account.
+
+
+ Please redo authentication using an account with "service" or "cipp" in the
+ username.
+
+
+ )}
+ >
+ ) : authError ? (
+
+
+ Authentication Error: {authError.errorCode}
+
+ {authError.errorMessage}
+
+ Time: {authError.timestamp}
+
+
+
+
+
+ ) : null}
+
+ )}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ }
+ onClick={handleSubmit}
+ >
+ Save
+
+
+ >
+ );
+};
+
+export default CippRoleAddEdit;
diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx
new file mode 100644
index 000000000000..c155064b634a
--- /dev/null
+++ b/src/components/CippSettings/CippRoles.jsx
@@ -0,0 +1,143 @@
+import React from "react";
+import { Box, Button, SvgIcon } from "@mui/material";
+import { CippDataTable } from "../CippTable/CippDataTable";
+import { PencilIcon, TrashIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
+import NextLink from "next/link";
+import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard";
+import { getCippTranslation } from "../../utils/get-cipp-translation";
+import { getCippFormatting } from "../../utils/get-cipp-formatting";
+import { Stack } from "@mui/system";
+import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard";
+
+const CippRoles = () => {
+ const actions = [
+ {
+ label: "Edit",
+ icon: (
+
+
+
+ ),
+ link: "/cipp/super-admin/cipp-roles/edit?role=[RoleName]",
+ },
+ {
+ label: "Clone",
+ icon: (
+
+
+
+ ),
+ type: "POST",
+ url: "/api/ExecCustomRole",
+ data: {
+ Action: "Clone",
+ RoleName: "RoleName",
+ },
+ fields: [
+ {
+ label: "New Role Name",
+ name: "NewRoleName",
+ type: "textField",
+ required: true,
+ helperText:
+ "Enter a name for the new cloned role. This cannot be the same as an existing role.",
+ disableVariables: true,
+ },
+ ],
+ relatedQueryKeys: ["customRoleList"],
+ confirmText: "Are you sure you want to clone this custom role?",
+ condition: (row) => row?.Type === "Custom",
+ },
+ {
+ label: "Delete",
+ icon: (
+
+
+
+ ),
+ confirmText: "Are you sure you want to delete this custom role?",
+ url: "/api/ExecCustomRole",
+ type: "POST",
+ data: {
+ Action: "Delete",
+ RoleName: "RoleName",
+ },
+ condition: (row) => row?.Type === "Custom",
+ relatedQueryKeys: ["customRoleList"],
+ },
+ ];
+
+ const offCanvas = {
+ children: (data) => {
+ const includeProps = ["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"];
+ const keys = includeProps.filter((key) => Object.keys(data).includes(key));
+ const properties = [];
+ keys.forEach((key) => {
+ if (data[key] && data[key].length > 0) {
+ properties.push({
+ label: getCippTranslation(key),
+ value: getCippFormatting(data[key], key),
+ });
+ }
+ });
+
+ if (data["Permissions"] && Object.keys(data["Permissions"]).length > 0) {
+ properties.push({
+ label: "Permissions",
+ value: (
+
+ {Object.keys(data["Permissions"])
+ .sort()
+ .map((permission, idx) => (
+
+
+
+ ))}
+
+ ),
+ });
+ }
+
+ return (
+
+ );
+ },
+ };
+
+ return (
+
+
+
+
+ }
+ component={NextLink}
+ href="/cipp/super-admin/cipp-roles/add"
+ >
+ Add Role
+
+ }
+ api={{
+ url: "/api/ListCustomRole",
+ }}
+ queryKey="customRoleTable"
+ simpleColumns={["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]}
+ offCanvas={offCanvas}
+ />
+
+ );
+};
+
+export default CippRoles;
diff --git a/src/components/CippSettings/CippTenantResults.jsx b/src/components/CippSettings/CippTenantResults.jsx
index 9483a07f7a2e..dc79285ccb4e 100644
--- a/src/components/CippSettings/CippTenantResults.jsx
+++ b/src/components/CippSettings/CippTenantResults.jsx
@@ -1,5 +1,5 @@
import { CippDataTable } from "../CippTable/CippDataTable";
-import { Sync } from "@mui/icons-material";
+import { Plumbing, Sync } from "@mui/icons-material";
export const CippTenantResults = (props) => {
const { importReport = false } = props;
@@ -28,6 +28,9 @@ export const CippTenantResults = (props) => {
"LastRun",
"GraphTest",
"ExchangeTest",
+ "OrgManagementRepairNeeeded",
+ "OrgManagementRoles",
+ "OrgManagementRolesMissing",
],
}}
/>
@@ -54,6 +57,15 @@ export const CippTenantResults = (props) => {
relatedQueryKeys: "ExecAccessChecks-Tenants",
multiPost: false,
},
+ {
+ label: "Repair Exchange Roles",
+ type: "POST",
+ url: "/api/ExecExchangeRoleRepair",
+ data: { TenantId: "TenantId" },
+ icon: ,
+ confirmText: "Repair Exchange roles for [TenantName]?",
+ condition: (row) => row.OrgManagementRepairNeeded === true,
+ },
]}
simpleColumns={[
"TenantName",
diff --git a/src/components/CippSettings/CippVersionProperties.jsx b/src/components/CippSettings/CippVersionProperties.jsx
index dca288ede4dd..6fbf88a39311 100644
--- a/src/components/CippSettings/CippVersionProperties.jsx
+++ b/src/components/CippSettings/CippVersionProperties.jsx
@@ -1,4 +1,4 @@
-import { Box, Button, Skeleton, SvgIcon } from "@mui/material";
+import { Box, Button, SvgIcon } from "@mui/material";
import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard";
import { CheckCircle, SystemUpdateAlt, Warning } from "@mui/icons-material";
import { ApiGetCall } from "/src/api/ApiCall";
diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx
index 8037de3b2e2b..aec6d0fbda94 100644
--- a/src/components/CippStandards/CippStandardAccordion.jsx
+++ b/src/components/CippStandards/CippStandardAccordion.jsx
@@ -9,7 +9,6 @@ import {
SvgIcon,
Collapse,
Divider,
- Grid,
Tooltip,
Chip,
TextField,
@@ -25,7 +24,11 @@ import {
Search,
Close,
FilterAlt,
+ NotificationImportant,
+ Assignment,
+ Construction,
} from "@mui/icons-material";
+import { Grid } from "@mui/system";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { useWatch } from "react-hook-form";
import _ from "lodash";
@@ -37,6 +40,9 @@ import Intune from "../../icons/iconly/bulk/intune";
import GDAPRoles from "/src/data/GDAPRoles";
import timezoneList from "/src/data/timezoneList";
import standards from "/src/data/standards.json";
+import { CippFormCondition } from "../CippComponents/CippFormCondition";
+import { CippPolicyImportDrawer } from "../CippComponents/CippPolicyImportDrawer";
+import ReactMarkdown from "react-markdown";
const getAvailableActions = (disabledFeatures) => {
const allActions = [
@@ -68,7 +74,7 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl })
}
return (
-
+ {
const [configuredState, setConfiguredState] = useState({});
const [filter, setFilter] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
+ const [savedValues, setSavedValues] = useState({});
+ const [originalValues, setOriginalValues] = useState({});
const watchedValues = useWatch({
control: formControl.control,
});
- useEffect(() => {
- const newConfiguredState = { ...configuredState };
+ // Watch all trackDrift values for all standards at once
+ const allTrackDriftValues = useWatch({
+ control: formControl.control,
+ name: Object.keys(selectedStandards).map((standardName) => `${standardName}.trackDrift`),
+ });
- Object.keys(selectedStandards).forEach((standardName) => {
- const standard = providedStandards.find((s) => s.name === standardName.split("[")[0]);
- if (standard) {
- const actionFilled = !!_.get(watchedValues, `${standardName}.action`, false);
+ // Handle drift mode automatic action setting
+ useEffect(() => {
+ if (isDriftMode && selectedStandards) {
+ Object.keys(selectedStandards).forEach((standardName) => {
+ const currentValues = formControl.getValues(standardName) || {};
+ const autoRemediate = currentValues.autoRemediate;
+
+ // Set default action based on autoRemediate setting
+ const defaultAction = autoRemediate
+ ? [
+ { label: "Report", value: "Report" },
+ { label: "Remediate", value: "Remediate" },
+ ]
+ : [{ label: "Report", value: "Report" }];
+
+ // Only set if action is not already set
+ if (!currentValues.action) {
+ formControl.setValue(`${standardName}.action`, defaultAction);
+ }
- const addedComponentsFilled =
- standard.addedComponent?.every((component) => {
- const isRequired = component.required !== false && component.type !== "switch";
- if (!isRequired) return true;
- return !!_.get(watchedValues, `${standardName}.${component.name}`);
- }) ?? true;
+ // Set default autoRemediate if not set
+ if (currentValues.autoRemediate === undefined) {
+ formControl.setValue(`${standardName}.autoRemediate`, false);
+ formControl.setValue(`${standardName}.action`, [{ label: "Report", value: "Report" }]);
+ }
+ });
+ }
+ }, [isDriftMode, selectedStandards, formControl]);
+
+ // Check if a standard is configured based on its values
+ const isStandardConfigured = (standardName, standard, values) => {
+ if (!values) return false;
+
+ // ALWAYS require an action for any standard to be considered configured
+ // The action field should be an array with at least one element
+ const actionValue = _.get(values, "action");
+ if (!actionValue || (Array.isArray(actionValue) && actionValue.length === 0)) return false;
+
+ // Additional checks for required components
+ const hasRequiredComponents =
+ standard.addedComponent &&
+ standard.addedComponent.some((comp) => comp.type !== "switch" && comp.required !== false);
+ const actionRequired = standard.disabledFeatures !== undefined || hasRequiredComponents;
+
+ // Always require an action (should be an array with at least one element)
+ const actionFilled = actionValue && (!Array.isArray(actionValue) || actionValue.length > 0);
+
+ const addedComponentsFilled =
+ standard.addedComponent?.every((component) => {
+ // Always skip switches
+ if (component.type === "switch") return true;
+
+ // Handle conditional fields
+ if (component.condition) {
+ const conditionField = component.condition.field;
+ const conditionValue = _.get(values, conditionField);
+ const compareType = component.condition.compareType || "is";
+ const compareValue = component.condition.compareValue;
+ const propertyName = component.condition.propertyName || "value";
+
+ let conditionMet = false;
+ if (propertyName === "value") {
+ switch (compareType) {
+ case "is":
+ conditionMet = _.isEqual(conditionValue, compareValue);
+ break;
+ case "isNot":
+ conditionMet = !_.isEqual(conditionValue, compareValue);
+ break;
+ default:
+ conditionMet = false;
+ }
+ } else if (Array.isArray(conditionValue)) {
+ switch (compareType) {
+ case "valueEq":
+ conditionMet = conditionValue.some((item) => item?.[propertyName] === compareValue);
+ break;
+ default:
+ conditionMet = false;
+ }
+ }
- const isConfigured = actionFilled && addedComponentsFilled;
+ // If condition is not met, skip validation for this field
+ if (!conditionMet) return true;
+ }
- if (newConfiguredState[standardName] !== isConfigured) {
- newConfiguredState[standardName] = isConfigured;
+ // Check if field is required
+ const isRequired = component.required !== false;
+ if (!isRequired) return true;
+
+ // Get field value using lodash's get to properly handle nested properties
+ const fieldValue = _.get(values, component.name);
+
+ // Check if field has a value based on its type and multiple property
+ if (component.type === "autoComplete" || component.type === "select") {
+ if (component.multiple) {
+ // For multiple selection, check if array exists and has items
+ return Array.isArray(fieldValue) && fieldValue.length > 0;
+ } else {
+ // For single selection, check if value exists
+ return !!fieldValue;
+ }
}
+
+ // For other field types
+ return !!fieldValue;
+ }) ?? true;
+
+ return actionFilled && addedComponentsFilled;
+ };
+
+ // Initialize when watchedValues are available
+ useEffect(() => {
+ if (editMode) {
+ // Only run initialization if we have watchedValues and they contain data
+ if (!watchedValues || Object.keys(watchedValues).length === 0) {
+ return;
}
- });
- if (!_.isEqual(newConfiguredState, configuredState)) {
- setConfiguredState(newConfiguredState);
+ // Prevent re-initialization if we already have configuration state
+ const hasConfigState = Object.keys(configuredState).length > 0;
+ if (hasConfigState) {
+ return;
+ }
+
+ const initial = {};
+ const initialConfigured = {};
+
+ // For each standard, get its current values and determine if it's configured
+ Object.keys(selectedStandards).forEach((standardName) => {
+ const currentValues = _.get(watchedValues, standardName);
+ if (!currentValues) return;
+
+ initial[standardName] = _.cloneDeep(currentValues);
+
+ const baseStandardName = standardName.split("[")[0];
+ const standard = providedStandards.find((s) => s.name === baseStandardName);
+ if (standard) {
+ initialConfigured[standardName] = isStandardConfigured(
+ standardName,
+ standard,
+ currentValues
+ );
+ }
+ });
+
+ // Store both the initial values and set them as current saved values
+ setOriginalValues(initial);
+ setSavedValues(initial);
+ setConfiguredState(initialConfigured);
+ // Only depend on watchedValues and selectedStandards to avoid infinite loops
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}
- }, [watchedValues, providedStandards, selectedStandards]);
+ }, [watchedValues, selectedStandards, editMode]);
+
+ // Save changes for a standard
+ const handleSave = (standardName, standard, current) => {
+ // Clone the current values to avoid reference issues
+ const newValues = _.cloneDeep(current);
+
+ // Update saved values
+ setSavedValues((prev) => ({
+ ...prev,
+ [standardName]: newValues,
+ }));
+
+ // Update configured state right away
+ const isConfigured = isStandardConfigured(standardName, standard, newValues);
+
+ setConfiguredState((prev) => ({
+ ...prev,
+ [standardName]: isConfigured,
+ }));
+ // Collapse the accordion after saving
+ handleAccordionToggle(null);
+ };
+
+ // Handle auto-remediate toggle in drift mode
+ const handleAutoRemediateChange = (standardName, value) => {
+ const action = value
+ ? [
+ { label: "Report", value: "Report" },
+ { label: "Remediate", value: "Remediate" },
+ ]
+ : [{ label: "Report", value: "Report" }];
+
+ formControl.setValue(`${standardName}.autoRemediate`, value);
+ formControl.setValue(`${standardName}.action`, action);
+ };
+
+ // Cancel changes for a standard
+ const handleCancel = (standardName) => {
+ // Get the last saved values
+ const savedValue = _.get(savedValues, standardName);
+ if (!savedValue) return;
+
+ // Set the entire standard's value at once to ensure proper handling of nested objects and arrays
+ formControl.setValue(standardName, _.cloneDeep(savedValue));
+
+ // Find the original standard definition to get the base standard
+ const baseStandardName = standardName.split("[")[0];
+ const standard = providedStandards.find((s) => s.name === baseStandardName);
+
+ // Determine if the standard was configured with saved values
+ if (standard) {
+ const isConfigured = isStandardConfigured(standardName, standard, savedValue);
+
+ // Restore the previous configuration state
+ setConfiguredState((prev) => ({
+ ...prev,
+ [standardName]: isConfigured,
+ }));
+ }
+
+ // Collapse the accordion after canceling
+ handleAccordionToggle(null);
+ };
+
+ // Group standards by category
const groupedStandards = useMemo(() => {
const result = {};
@@ -154,6 +362,7 @@ const CippStandardAccordion = ({
return result;
}, [selectedStandards, providedStandards]);
+ // Filter standards based on search and filter selection
const filteredGroupedStandards = useMemo(() => {
if (!searchQuery && filter === "all") {
return groupedStandards;
@@ -166,6 +375,11 @@ const CippStandardAccordion = ({
const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower);
const filteredStandards = groupedStandards[category].filter(({ standardName, standard }) => {
+ // If this is the currently expanded standard, always include it in the result
+ if (standardName === expanded) {
+ return true;
+ }
+
const matchesSearch =
!searchQuery ||
categoryMatchesSearch ||
@@ -176,7 +390,7 @@ const CippStandardAccordion = ({
Array.isArray(standard.tag) &&
standard.tag.some((tag) => tag.toLowerCase().includes(searchLower)));
- const isConfigured = configuredState[standardName];
+ const isConfigured = _.get(configuredState, standardName);
const matchesFilter =
filter === "all" ||
(filter === "configured" && isConfigured) ||
@@ -193,6 +407,7 @@ const CippStandardAccordion = ({
return result;
}, [groupedStandards, searchQuery, filter, configuredState]);
+ // Count standards by configuration state
const standardCounts = useMemo(() => {
let allCount = 0;
let configuredCount = 0;
@@ -235,7 +450,13 @@ const CippStandardAccordion = ({
sx={{ width: { xs: "100%", sm: 350 } }}
placeholder="Search..."
value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
+ onChange={(e) => {
+ // Close any expanded accordion when changing search query
+ if (expanded && e.target.value !== searchQuery) {
+ handleAccordionToggle(null);
+ }
+ setSearchQuery(e.target.value);
+ }}
slotProps={{
input: {
startAdornment: (
@@ -248,7 +469,13 @@ const CippStandardAccordion = ({
setSearchQuery("")}
+ onClick={() => {
+ // Close any expanded accordion when clearing search
+ if (expanded) {
+ handleAccordionToggle(null);
+ }
+ setSearchQuery("");
+ }}
aria-label="Clear search"
>
@@ -268,19 +495,37 @@ const CippStandardAccordion = ({
setFilter("all")}
+ onClick={() => {
+ // Close any expanded accordion when changing filters
+ if (expanded) {
+ handleAccordionToggle(null);
+ }
+ setFilter("all");
+ }}
>
All ({standardCounts.allCount})
setFilter("configured")}
+ onClick={() => {
+ // Close any expanded accordion when changing filters
+ if (expanded) {
+ handleAccordionToggle(null);
+ }
+ setFilter("configured");
+ }}
>
Configured ({standardCounts.configuredCount})
setFilter("unconfigured")}
+ onClick={() => {
+ // Close any expanded accordion when changing filters
+ if (expanded) {
+ handleAccordionToggle(null);
+ }
+ setFilter("unconfigured");
+ }}
>
Unconfigured ({standardCounts.unconfiguredCount})
@@ -307,7 +552,7 @@ const CippStandardAccordion = ({
const isExpanded = expanded === standardName;
const hasAddedComponents =
standard.addedComponent && standard.addedComponent.length > 0;
- const isConfigured = configuredState[standardName];
+ const isConfigured = _.get(configuredState, standardName);
const disabledFeatures = standard.disabledFeatures || {};
let selectedActions = _.get(watchedValues, `${standardName}.action`);
@@ -315,22 +560,135 @@ const CippStandardAccordion = ({
selectedActions = [selectedActions];
}
+ // Get template name for Intune Templates
+ let templateDisplayName = "";
+ if (standardName.startsWith("standards.IntuneTemplate")) {
+ // Check for TemplateList selection
+ const templateList = _.get(watchedValues, `${standardName}.TemplateList`);
+ if (templateList && templateList.label) {
+ templateDisplayName = templateList.label;
+ }
+
+ // Check for TemplateList-Tags selection (takes priority)
+ const templateListTags = _.get(watchedValues, `${standardName}.TemplateList-Tags`);
+ if (templateListTags && templateListTags.label) {
+ templateDisplayName = templateListTags.label;
+ }
+ }
+
+ // For multiple standards, check the first added component
const selectedTemplateName = standard.multiple
? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`)
: "";
- const accordionTitle = selectedTemplateName
- ? `${standard.label} - ${selectedTemplateName.label}`
+
+ // Build accordion title with template name if available
+ const accordionTitle = templateDisplayName
+ ? `${standard.label} - ${templateDisplayName}`
+ : selectedTemplateName && _.get(selectedTemplateName, "label")
+ ? `${standard.label} - ${_.get(selectedTemplateName, "label")}`
: standard.label;
+ // Get current values and check if they differ from saved values
+ const current = _.get(watchedValues, standardName);
+ const saved = _.get(savedValues, standardName) || {};
+
+ const hasUnsaved = !_.isEqual(current, saved);
+
+ // Check if all required fields are filled
+ const requiredFieldsFilled = current
+ ? standard.addedComponent?.every((component) => {
+ // Always skip switches regardless of their required property
+ if (component.type === "switch") return true;
+
+ // Skip optional fields (not required)
+ const isRequired = component.required !== false;
+ if (!isRequired) return true;
+
+ // Handle conditional fields
+ if (component.condition) {
+ const conditionField = component.condition.field;
+ const conditionValue = _.get(current, conditionField);
+ const compareType = component.condition.compareType || "is";
+ const compareValue = component.condition.compareValue;
+ const propertyName = component.condition.propertyName || "value";
+
+ let conditionMet = false;
+ if (propertyName === "value") {
+ switch (compareType) {
+ case "is":
+ conditionMet = _.isEqual(conditionValue, compareValue);
+ break;
+ case "isNot":
+ conditionMet = !_.isEqual(conditionValue, compareValue);
+ break;
+ default:
+ conditionMet = false;
+ }
+ } else if (Array.isArray(conditionValue)) {
+ switch (compareType) {
+ case "valueEq":
+ conditionMet = conditionValue.some(
+ (item) => item?.[propertyName] === compareValue
+ );
+ break;
+ default:
+ conditionMet = false;
+ }
+ }
+
+ // If condition is not met, skip validation
+ if (!conditionMet) return true;
+ }
+
+ // Get field value for validation using lodash's get to properly handle nested properties
+ const fieldValue = _.get(current, component.name);
+
+ // Check if required field has a value based on its type and multiple property
+ if (component.type === "autoComplete" || component.type === "select") {
+ if (component.multiple) {
+ // For multiple selection, check if array exists and has items
+ return Array.isArray(fieldValue) && fieldValue.length > 0;
+ } else {
+ // For single selection, check if value exists
+ return !!fieldValue;
+ }
+ }
+
+ // For other field types
+ return !!fieldValue;
+ }) ?? true
+ : false;
+
+ // ALWAYS require an action for all standards
+ const actionRequired = true;
+
+ // Check if there are required non-switch components for UI display purposes
+ const hasRequiredComponents =
+ standard.addedComponent &&
+ standard.addedComponent.some(
+ (comp) => comp.type !== "switch" && comp.required !== false
+ );
+
+ // Action is always required and must be an array with at least one element
+ const actionValue = _.get(current, "action");
+ const hasAction =
+ actionValue && (!Array.isArray(actionValue) || actionValue.length > 0);
+
+ // Allow saving if:
+ // 1. Action is selected if required
+ // 2. All required fields are filled
+ // 3. There are unsaved changes
+ const canSave = hasAction && requiredFieldsFilled && hasUnsaved;
+
return (
-
+
{standard.cat === "Global Standards" ? (
@@ -348,31 +706,69 @@ const CippStandardAccordion = ({
{accordionTitle}
- {selectedActions && selectedActions?.length > 0 && (
-
- {selectedActions?.map((action, index) => (
-
-
-
- ))}
-
-
- )}
-
- {standard.helpText}
-
+
+ {/* Hide action chips in drift mode */}
+ {!isDriftMode && selectedActions && selectedActions?.length > 0 && (
+ <>
+ {selectedActions?.map((action, index) => (
+
+
+ {action.value === "Report" && }
+ {action.value === "warn" && }
+ {action.value === "Remediate" && }
+
+ }
+ />
+
+ ))}
+ >
+ )}
+
+
+ theme.palette.primary.main,
+ textDecoration: "underline",
+ "&:hover": {
+ textDecoration: "none",
+ },
+ },
+ color: "text.secondary",
+ fontSize: "0.875rem",
+ lineHeight: 1.43,
+ mr: 1,
+ }}
+ >
+ (
+
+ {children}
+
+ ),
+ // Convert paragraphs to spans to avoid unwanted spacing
+ p: ({ children }) => {children},
+ }}
+ >
+ {standard.helpText}
+
+
@@ -394,9 +790,11 @@ const CippStandardAccordion = ({
{isConfigured ? "Configured" : "Unconfigured"}
- handleRemoveStandard(standardName)}>
-
-
+
+ handleRemoveStandard(standardName)}>
+
+
+ handleAccordionToggle(standardName)}>
-
-
-
+ {isDriftMode ? (
+ /* Drift mode layout - full width with slider first */
+
+ {/* Auto-remediate switch takes full width and is first */}
+
+
+ handleAutoRemediateChange(standardName, e.target.checked)
+ }
+ fullWidth
+ />
+
+
+ {/* Additional components take full width */}
+ {hasAddedComponents && (
+ <>
+ {/* Add catalog button for Intune Template standard - appears first */}
+ {standardName.startsWith("standards.IntuneTemplate") && (
+
+
+
+
+
+ )}
+ {standard.addedComponent?.map((component, idx) =>
+ component?.condition ? (
+
+
+
+ ) : (
+
+ )
+ )}
+ >
+ )}
+ ) : (
+ /* Standard mode layout - original grid layout */
+
+
+
+
- {hasAddedComponents && (
-
-
- {standard.addedComponent?.map((component, idx) => (
-
- ))}
+ {hasAddedComponents && (
+
+
+ {/* Add catalog button for Intune Template standard - appears first */}
+ {standardName.startsWith("standards.IntuneTemplate") && (
+
+
+
+
+
+ )}
+ {standard.addedComponent?.map((component, idx) =>
+ component?.condition ? (
+
+
+
+ ) : (
+
+ )
+ )}
+
-
- )}
-
+ )}
+
+ )}
+
+
+
+
+ handleCancel(standardName)}
+ >
+ Cancel
+
+ handleSave(standardName, standard, current)}
+ >
+ Save
+
+
diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx
index 5bc741800718..d74d6f4d3630 100644
--- a/src/components/CippStandards/CippStandardDialog.jsx
+++ b/src/components/CippStandards/CippStandardDialog.jsx
@@ -1,11 +1,10 @@
-import { differenceInDays } from 'date-fns';
+import { differenceInDays } from "date-fns";
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
- Grid,
Card,
CardContent,
Typography,
@@ -15,10 +14,530 @@ import {
Switch,
Button,
IconButton,
+ CircularProgress,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ Stack,
+ Divider,
+ Collapse,
+ ToggleButton,
+ ToggleButtonGroup,
+ List,
+ ListItem,
+ ListItemText,
+ ListItemSecondaryAction,
} from "@mui/material";
-import { Add } from "@mui/icons-material";
-import { useState, useCallback } from "react";
+import { Grid } from "@mui/system";
+import {
+ Add,
+ Sort,
+ Clear,
+ FilterList,
+ ExpandMore,
+ ExpandLess,
+ ViewModule,
+ ViewList,
+} from "@mui/icons-material";
+import { useState, useCallback, useMemo, memo, useEffect } from "react";
import { debounce } from "lodash";
+import { Virtuoso } from "react-virtuoso";
+import ReactMarkdown from "react-markdown";
+
+// Memoized Standard Card component to prevent unnecessary re-renders
+const StandardCard = memo(
+ ({
+ standard,
+ category,
+ selectedStandards,
+ handleToggleSingleStandard,
+ handleAddClick,
+ isButtonDisabled,
+ }) => {
+ const isNewStandard = (dateAdded) => {
+ const currentDate = new Date();
+ const addedDate = new Date(dateAdded);
+ return differenceInDays(currentDate, addedDate) <= 30;
+ };
+
+ // Create a memoized handler for this specific standard to avoid recreation on each render
+ const handleToggle = useCallback(() => {
+ handleToggleSingleStandard(standard.name);
+ }, [handleToggleSingleStandard, standard.name]);
+
+ // Check if this standard is selected - memoize for better performance
+ const isSelected = useMemo(() => {
+ return !!selectedStandards[standard.name];
+ }, [selectedStandards, standard.name]);
+
+ // Lazily render complex parts of the card only when visible
+ const [expanded, setExpanded] = useState(false);
+
+ // Use intersection observer to detect when card is visible
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setExpanded(true);
+ observer.disconnect();
+ }
+ },
+ { threshold: 0.1 }
+ );
+
+ const currentRef = document.getElementById(`standard-card-${standard.name}`);
+ if (currentRef) {
+ observer.observe(currentRef);
+ }
+
+ return () => observer.disconnect();
+ }, [standard.name]);
+
+ return (
+
+
+ {isNewStandard(standard.addedDate) && (
+
+ )}
+
+
+
+ {standard.label}
+
+ {expanded && standard.helpText && (
+ <>
+
+ Description:
+
+ theme.palette.primary.main,
+ textDecoration: "underline",
+ "&:hover": {
+ textDecoration: "none",
+ },
+ },
+ color: "text.secondary",
+ fontSize: "0.875rem",
+ lineHeight: 1.43,
+ mb: 2,
+ }}
+ >
+ (
+
+ {children}
+
+ ),
+ // Convert paragraphs to spans to avoid unwanted spacing
+ p: ({ children }) => {children},
+ }}
+ >
+ {standard.helpText}
+
+
+ >
+ )}
+
+ Category:
+
+
+ {expanded &&
+ standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > 0 && (
+ <>
+
+ Tags:
+
+
+ {standard.tag
+ .filter((tag) => !tag.toLowerCase().includes("impact"))
+ .map((tag, idx) => (
+
+ ))}
+
+ >
+ )}
+
+ Impact:
+
+
+ {expanded && standard.recommendedBy?.length > 0 && (
+ <>
+
+ Recommended By:
+
+
+ {standard.recommendedBy.join(", ")}
+
+ >
+ )}
+ {expanded && standard.addedDate?.length > 0 && (
+ <>
+
+ Date Added:
+
+
+
+ {standard.addedDate}
+
+
+ >
+ )}
+
+
+
+ {standard.multiple ? (
+ handleAddClick(standard.name)}
+ >
+
+
+ ) : (
+
+ }
+ label="Add this standard to the template"
+ />
+ )}
+
+
+
+
+ );
+ },
+ // Custom equality function to prevent unnecessary re-renders
+ (prevProps, nextProps) => {
+ // Only re-render if one of these props changed
+ if (prevProps.isButtonDisabled !== nextProps.isButtonDisabled) return false;
+ if (prevProps.standard.name !== nextProps.standard.name) return false;
+
+ // Only check selected state for this specific standard
+ const prevSelected = !!prevProps.selectedStandards[prevProps.standard.name];
+ const nextSelected = !!nextProps.selectedStandards[nextProps.standard.name];
+ if (prevSelected !== nextSelected) return false;
+
+ // If we get here, nothing important changed, skip re-render
+ return true;
+ }
+);
+
+StandardCard.displayName = "StandardCard";
+
+// Virtualized grid to handle large numbers of standards efficiently
+const VirtualizedStandardGrid = memo(({ items, renderItem }) => {
+ const [itemsPerRow, setItemsPerRow] = useState(() =>
+ window.innerWidth > 960 ? 4 : window.innerWidth > 600 ? 2 : 1
+ );
+
+ // Handle window resize for responsive grid
+ useEffect(() => {
+ const handleResize = () => {
+ const newItemsPerRow = window.innerWidth > 960 ? 4 : window.innerWidth > 600 ? 2 : 1;
+ setItemsPerRow(newItemsPerRow);
+ };
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ const rows = useMemo(() => {
+ const rowCount = Math.ceil(items.length / itemsPerRow);
+ const rowsData = [];
+
+ for (let i = 0; i < rowCount; i++) {
+ const startIdx = i * itemsPerRow;
+ const rowItems = items.slice(startIdx, startIdx + itemsPerRow);
+ rowsData.push(rowItems);
+ }
+
+ return rowsData;
+ }, [items, itemsPerRow]);
+
+ return (
+ (
+
+
+ {rows[index].map(renderItem)}
+
+
+ )}
+ />
+ );
+});
+
+VirtualizedStandardGrid.displayName = "VirtualizedStandardGrid";
+
+// Compact List View component for standards
+const CompactStandardList = memo(
+ ({ items, selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled }) => {
+ return (
+
+ {items.map(({ standard, category }) => {
+ const isSelected = !!selectedStandards[standard.name];
+
+ const isNewStandard = (dateAdded) => {
+ if (!dateAdded) return false;
+ const currentDate = new Date();
+ const addedDate = new Date(dateAdded);
+ return differenceInDays(currentDate, addedDate) <= 30;
+ };
+
+ const handleToggle = () => {
+ handleToggleSingleStandard(standard.name);
+ };
+
+ return (
+
+
+
+ {standard.label}
+
+ {isNewStandard(standard.addedDate) && (
+
+ )}
+
+
+
+ }
+ secondary={
+
+ {standard.helpText && (
+ theme.palette.primary.main,
+ textDecoration: "underline",
+ "&:hover": {
+ textDecoration: "none",
+ },
+ },
+ color: "text.secondary",
+ fontSize: "0.875rem",
+ lineHeight: 1.43,
+ }}
+ >
+ (
+
+ {children}
+
+ ),
+ p: ({ children }) => (
+
+ {children}
+
+ ),
+ }}
+ >
+ {standard.helpText}
+
+
+ )}
+
+ {standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length >
+ 0 && (
+
+ {standard.tag
+ .filter((tag) => !tag.toLowerCase().includes("impact"))
+ .slice(0, 3) // Show only first 3 tags to save space
+ .map((tag, idx) => (
+
+ ))}
+ {standard.tag.filter((tag) => !tag.toLowerCase().includes("impact"))
+ .length > 3 && (
+
+ +
+ {standard.tag.filter((tag) => !tag.toLowerCase().includes("impact"))
+ .length - 3}{" "}
+ more
+
+ )}
+
+ )}
+ {standard.recommendedBy?.length > 0 && (
+
+ β’ Recommended by: {standard.recommendedBy.join(", ")}
+
+ )}
+ {standard.addedDate && (
+
+ β’ Added: {standard.addedDate}
+
+ )}
+
+
+ }
+ />
+
+ {standard.multiple ? (
+ handleAddClick(standard.name)}
+ sx={{ mr: 1 }}
+ >
+
+
+ ) : (
+
+ }
+ label=""
+ sx={{ mr: 1 }}
+ />
+ )}
+
+
+ );
+ })}
+
+ );
+ }
+);
+
+CompactStandardList.displayName = "CompactStandardList";
const CippStandardDialog = ({
dialogOpen,
@@ -31,192 +550,772 @@ const CippStandardDialog = ({
handleAddMultipleStandard,
}) => {
const [isButtonDisabled, setButtonDisabled] = useState(false);
+ const [localSearchQuery, setLocalSearchQuery] = useState("");
+ const [isInitialLoading, setIsInitialLoading] = useState(true);
+ const [viewMode, setViewMode] = useState("card"); // "card" or "list"
+
+ // Enhanced filtering and sorting state
+ const [sortBy, setSortBy] = useState("addedDate"); // Default sort by date added
+ const [sortOrder, setSortOrder] = useState("desc"); // desc to show newest first
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const [selectedImpacts, setSelectedImpacts] = useState([]);
+ const [selectedRecommendedBy, setSelectedRecommendedBy] = useState([]);
+ const [selectedTagFrameworks, setSelectedTagFrameworks] = useState([]);
+ const [showOnlyNew, setShowOnlyNew] = useState(false); // Show only standards added in last 30 days
+ const [filtersExpanded, setFiltersExpanded] = useState(false); // Control filter section collapse/expand
+
+ // Auto-adjust sort order when sort type changes
+ useEffect(() => {
+ if (sortBy === "label") {
+ setSortOrder("asc"); // Names: A-Z
+ } else if (sortBy === "addedDate") {
+ setSortOrder("desc"); // Dates: Newest first
+ } else if (sortBy === "impact") {
+ setSortOrder("desc"); // Impact: High to Low
+ }
+ }, [sortBy]);
+
+ // Get all unique values for filters
+ const { allCategories, allImpacts, allRecommendedBy, allTagFrameworks } = useMemo(() => {
+ const categorySet = new Set();
+ const impactSet = new Set();
+ const recommendedBySet = new Set();
+ const tagFrameworkSet = new Set();
+
+ // Function to extract base framework from tag
+ const extractTagFramework = (tag) => {
+ // Compliance Frameworks - extract version dynamically
+ if (tag.startsWith("CIS M365")) {
+ const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/);
+ return versionMatch ? `CIS M365 ${versionMatch[1]}` : "CIS M365";
+ }
+ if (tag.startsWith("CISA ")) return "CISA";
+ if (tag.startsWith("EIDSCA.")) return "EIDSCA";
+ if (tag.startsWith("Essential 8")) return "Essential 8";
+ if (tag.startsWith("NIST CSF")) {
+ const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/);
+ return versionMatch ? `NIST CSF ${versionMatch[1]}` : "NIST CSF";
+ }
+
+ // Microsoft Secure Score Categories
+ if (tag.startsWith("exo_")) return "Secure Score - Exchange";
+ if (tag.startsWith("mdo_")) return "Secure Score - Defender";
+ if (tag.startsWith("spo_")) return "Secure Score - SharePoint";
+ if (tag.startsWith("mip_")) return "Secure Score - Purview";
+
+ // For any other tags, return null to exclude them
+ return null;
+ };
+
+ Object.keys(categories).forEach((category) => {
+ categorySet.add(category);
+ categories[category].forEach((standard) => {
+ if (standard.impact) impactSet.add(standard.impact);
+ if (standard.recommendedBy && Array.isArray(standard.recommendedBy)) {
+ standard.recommendedBy.forEach((rec) => recommendedBySet.add(rec));
+ }
+ // Process tags to extract frameworks
+ if (standard.tag && Array.isArray(standard.tag)) {
+ standard.tag.forEach((tag) => {
+ const framework = extractTagFramework(tag);
+ if (framework) {
+ // Only add non-null frameworks
+ tagFrameworkSet.add(framework);
+ }
+ });
+ }
+ });
+ });
+
+ // Custom sort order for impacts: Low -> Medium -> High
+ const impactOrder = ["Low Impact", "Medium Impact", "High Impact"];
+ const sortedImpacts = Array.from(impactSet).sort((a, b) => {
+ const aIndex = impactOrder.indexOf(a);
+ const bIndex = impactOrder.indexOf(b);
+ return aIndex - bIndex;
+ });
+
+ // Sort tag frameworks with compliance frameworks first, then service categories
+ const sortedTagFrameworks = Array.from(tagFrameworkSet).sort((a, b) => {
+ // Define priority groups
+ const getFrameworkPriority = (framework) => {
+ if (framework.startsWith("CIS M365")) return 1;
+ if (framework === "CISA") return 2;
+ if (framework === "EIDSCA") return 3;
+ if (framework === "Essential 8") return 4;
+ if (framework.startsWith("NIST CSF")) return 5;
+ if (framework.startsWith("Secure Score -")) return 6;
+ return 999; // Other tags go last
+ };
+
+ const aPriority = getFrameworkPriority(a);
+ const bPriority = getFrameworkPriority(b);
+
+ // If different priorities, sort by priority
+ if (aPriority !== bPriority) {
+ return aPriority - bPriority;
+ }
+
+ // If same priority, sort alphabetically
+ return a.localeCompare(b);
+ });
+
+ return {
+ allCategories: Array.from(categorySet).sort(),
+ allImpacts: sortedImpacts,
+ allRecommendedBy: Array.from(recommendedBySet).sort(),
+ allTagFrameworks: sortedTagFrameworks,
+ };
+ }, [categories]);
+
+ // Enhanced filter function
+ const enhancedFilterStandards = useCallback(
+ (standardsList) => {
+ // Function to extract base framework from tag (same as in useMemo)
+ const extractTagFramework = (tag) => {
+ // Compliance Frameworks - extract version dynamically
+ if (tag.startsWith("CIS M365")) {
+ const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/);
+ return versionMatch ? `CIS M365 ${versionMatch[1]}` : "CIS M365";
+ }
+ if (tag.startsWith("CISA ")) return "CISA";
+ if (tag.startsWith("EIDSCA.")) return "EIDSCA";
+ if (tag.startsWith("Essential 8")) return "Essential 8";
+ if (tag.startsWith("NIST CSF")) {
+ const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/);
+ return versionMatch ? `NIST CSF ${versionMatch[1]}` : "NIST CSF";
+ }
+
+ // Microsoft Secure Score Categories
+ if (tag.startsWith("exo_")) return "Secure Score - Exchange";
+ if (tag.startsWith("mdo_")) return "Secure Score - Defender";
+ if (tag.startsWith("spo_")) return "Secure Score - SharePoint";
+ if (tag.startsWith("mip_")) return "Secure Score - Purview";
+
+ // For any other tags, return null to exclude them
+ return null;
+ };
+
+ return standardsList.filter((standard) => {
+ // Original text search
+ const matchesSearch =
+ !localSearchQuery ||
+ standard.label.toLowerCase().includes(localSearchQuery.toLowerCase()) ||
+ standard.helpText.toLowerCase().includes(localSearchQuery.toLowerCase()) ||
+ (standard.tag &&
+ standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase())));
+
+ // Category filter
+ const matchesCategory =
+ selectedCategories.length === 0 || selectedCategories.includes(standard.cat);
- const handleAddClick = (standardName) => {
- setButtonDisabled(true);
- handleAddMultipleStandard(standardName);
+ // Impact filter
+ const matchesImpact =
+ selectedImpacts.length === 0 || selectedImpacts.includes(standard.impact);
- setTimeout(() => {
- setButtonDisabled(false);
- }, 100);
- };
+ // Recommended by filter
+ const matchesRecommendedBy =
+ selectedRecommendedBy.length === 0 ||
+ (standard.recommendedBy &&
+ Array.isArray(standard.recommendedBy) &&
+ standard.recommendedBy.some((rec) => selectedRecommendedBy.includes(rec)));
+ // Tag framework filter
+ const matchesTagFramework =
+ selectedTagFrameworks.length === 0 ||
+ (standard.tag &&
+ Array.isArray(standard.tag) &&
+ standard.tag.some((tag) => {
+ const framework = extractTagFramework(tag);
+ return framework && selectedTagFrameworks.includes(framework);
+ }));
+
+ // New standards filter (last 30 days)
+ const isNewStandard = (dateAdded) => {
+ if (!dateAdded) return false;
+ const currentDate = new Date();
+ const addedDate = new Date(dateAdded);
+ return differenceInDays(currentDate, addedDate) <= 30;
+ };
+ const matchesNewFilter = !showOnlyNew || isNewStandard(standard.addedDate);
+
+ return (
+ matchesSearch &&
+ matchesCategory &&
+ matchesImpact &&
+ matchesRecommendedBy &&
+ matchesTagFramework &&
+ matchesNewFilter
+ );
+ });
+ },
+ [
+ localSearchQuery,
+ selectedCategories,
+ selectedImpacts,
+ selectedRecommendedBy,
+ selectedTagFrameworks,
+ showOnlyNew,
+ ]
+ );
+
+ // Enhanced sort function
+ const sortStandards = useCallback(
+ (standardsList) => {
+ return [...standardsList].sort((a, b) => {
+ let aValue, bValue;
+
+ switch (sortBy) {
+ case "label":
+ aValue = a.label.toLowerCase();
+ bValue = b.label.toLowerCase();
+ break;
+ case "addedDate":
+ aValue = new Date(a.addedDate || "1900-01-01");
+ bValue = new Date(b.addedDate || "1900-01-01");
+ break;
+ case "category":
+ aValue = a.cat?.toLowerCase() || "";
+ bValue = b.cat?.toLowerCase() || "";
+ break;
+ case "impact":
+ // Sort by impact priority: High > Medium > Low
+ const impactOrder = { "High Impact": 3, "Medium Impact": 2, "Low Impact": 1 };
+ aValue = impactOrder[a.impact] || 0;
+ bValue = impactOrder[b.impact] || 0;
+ break;
+ case "recommendedBy":
+ aValue =
+ a.recommendedBy && a.recommendedBy.length > 0
+ ? a.recommendedBy.join(", ").toLowerCase()
+ : "";
+ bValue =
+ b.recommendedBy && b.recommendedBy.length > 0
+ ? b.recommendedBy.join(", ").toLowerCase()
+ : "";
+ break;
+ default:
+ aValue = a.label.toLowerCase();
+ bValue = b.label.toLowerCase();
+ }
+
+ if (aValue < bValue) return sortOrder === "asc" ? -1 : 1;
+ if (aValue > bValue) return sortOrder === "asc" ? 1 : -1;
+ return 0;
+ });
+ },
+ [sortBy, sortOrder]
+ );
+
+ // Optimize handleAddClick to be more performant
+ const handleAddClick = useCallback(
+ (standardName) => {
+ setButtonDisabled(true);
+ handleAddMultipleStandard(standardName);
+ // Use requestAnimationFrame for smoother UI updates
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ setButtonDisabled(false);
+ }, 100);
+ });
+ },
+ [handleAddMultipleStandard]
+ );
+
+ // Optimize search debounce with a higher timeout for better performance
const handleSearchQueryChange = useCallback(
debounce((query) => {
setSearchQuery(query.trim());
- }, 50),
- []
+ }, 350), // Increased debounce time for better performance
+ [setSearchQuery]
);
- const isNewStandard = (dateAdded) => {
- const currentDate = new Date();
- const addedDate = new Date(dateAdded);
- return differenceInDays(currentDate, addedDate) <= 30;
- };
+ // Only process visible categories on demand to improve performance
+ const [processedItems, setProcessedItems] = useState([]);
+
+ // Handle search input change locally
+ const handleLocalSearchChange = useCallback(
+ (e) => {
+ const value = e.target.value;
+ setLocalSearchQuery(value);
+ handleSearchQueryChange(value);
+ },
+ [handleSearchQueryChange]
+ );
+
+ // Clear all filters
+ const clearAllFilters = useCallback(() => {
+ setLocalSearchQuery("");
+ setSelectedCategories([]);
+ setSelectedImpacts([]);
+ setSelectedRecommendedBy([]);
+ setSelectedTagFrameworks([]);
+ setShowOnlyNew(false);
+ setSortBy("addedDate");
+ setSortOrder("desc");
+ setViewMode("card"); // Reset to card view
+ handleSearchQueryChange("");
+ }, [handleSearchQueryChange]);
+
+ // Clear dialog state on close
+ const handleClose = useCallback(() => {
+ setLocalSearchQuery(""); // Clear local search state
+ setSelectedCategories([]);
+ setSelectedImpacts([]);
+ setSelectedRecommendedBy([]);
+ setSelectedTagFrameworks([]);
+ setShowOnlyNew(false);
+ setViewMode("card"); // Reset to card view
+ handleSearchQueryChange(""); // Clear parent search state
+ handleCloseDialog();
+ }, [handleCloseDialog, handleSearchQueryChange]);
+
+ // Process standards data only when dialog is opened, to improve performance
+ useEffect(() => {
+ if (dialogOpen) {
+ // Use requestIdleCallback if available, or setTimeout as fallback
+ const processStandards = () => {
+ // Create a flattened list of all standards for virtualized rendering
+ const allItems = [];
+
+ Object.keys(categories).forEach((category) => {
+ const categoryStandards = categories[category];
+ const filteredStandards = enhancedFilterStandards(categoryStandards);
+
+ filteredStandards.forEach((standard) => {
+ allItems.push({
+ standard,
+ category,
+ });
+ });
+ });
+
+ // Apply sorting to the final combined array instead of per-category
+ const sortedAllItems = sortStandards(allItems.map((item) => item.standard)).map(
+ (standard) => {
+ const item = allItems.find((item) => item.standard.name === standard.name);
+ return item;
+ }
+ );
+
+ setProcessedItems(sortedAllItems);
+ setIsInitialLoading(false);
+ };
+ if (window.requestIdleCallback) {
+ window.requestIdleCallback(processStandards, { timeout: 500 });
+ } else {
+ setTimeout(processStandards, 100);
+ }
+
+ return () => {
+ if (window.cancelIdleCallback) {
+ window.cancelIdleCallback(processStandards);
+ }
+ };
+ } else {
+ setIsInitialLoading(true);
+ }
+ }, [dialogOpen, categories, enhancedFilterStandards, sortStandards]);
+
+ // Render individual standard card
+ const renderStandardCard = useCallback(
+ ({ standard, category }) => (
+
+ ),
+ [selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled]
+ );
+
+ // Count active filters
+ const activeFiltersCount =
+ selectedCategories.length +
+ selectedImpacts.length +
+ selectedRecommendedBy.length +
+ selectedTagFrameworks.length +
+ (showOnlyNew ? 1 : 0);
+
+ // Don't render dialog contents until it's actually open (improves performance)
return (
diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js
index 1b77a2a55216..ad1315667b35 100644
--- a/src/components/CippTable/CippGraphExplorerFilter.js
+++ b/src/components/CippTable/CippGraphExplorerFilter.js
@@ -1,5 +1,5 @@
-import React, { useState, useEffect, useCallback } from "react";
-import { Button, Typography } from "@mui/material";
+import { useState, useEffect, useCallback } from "react";
+import { Button, Link, Typography } from "@mui/material";
import {
Save as SaveIcon,
Delete,
@@ -28,6 +28,7 @@ const CippGraphExplorerFilter = ({
onSubmitFilter,
onPresetChange,
component = "accordion",
+ relatedQueryKeys = [],
}) => {
const [offCanvasOpen, setOffCanvasOpen] = useState(false);
const [cardExpanded, setCardExpanded] = useState(true);
@@ -162,7 +163,7 @@ const CippGraphExplorerFilter = ({
}, [currentEndpoint, debouncedRefetch]);
const savePresetApi = ApiPostCall({
- relatedQueryKeys: ["ListGraphExplorerPresets", "ListGraphRequest"],
+ relatedQueryKeys: ["ListGraphExplorerPresets*", "ListGraphRequest", ...relatedQueryKeys],
});
// Save preset function
@@ -397,6 +398,9 @@ const CippGraphExplorerFilter = ({
Import / Export Graph Explorer Preset
+
+ Copy the JSON below to export your preset, or paste a preset JSON to import it.
+ setEditorValues(JSON.parse(value))}
@@ -411,6 +415,7 @@ const CippGraphExplorerFilter = ({
}}
variant="contained"
color="primary"
+ sx={{ mt: 2 }}
>
Import Template
@@ -541,7 +546,7 @@ const CippGraphExplorerFilter = ({
}
>
-
+
)}
placeholder="Select a preset"
+ helperText="Select an existing preset to load its parameters"
/>
{/* Preset Name Field */}
-
+
-
+
+ The{" "}
+
+ Graph endpoint
+ {" "}
+ to query (e.g. https://graph.microsoft.com/beta/$Endpoint)
+ >
+ }
/>
-
+
{/* Filter Field */}
-
+
+ Graph $filter query
+
+ }
/>
{/* Expand Field */}
-
+
{/* Top Field */}
-
+
{/* Search Field */}
-
+
{/* Format Field */}
-
+
{/* Reverse Tenant Lookup Switch */}
-
+
{/* Reverse Tenant Lookup Property Field */}
-
+
{/* No Pagination Switch */}
-
+
{/* $count Switch */}
-
+
{/* AsApp switch */}
-
+ {
+ const queryClient = useQueryClient();
+ const [queueCanvasVisible, setQueueCanvasVisible] = useState(false);
+ const [persistentQueueData, setPersistentQueueData] = useState(null);
+ const [lastProcessedQueueId, setLastProcessedQueueId] = useState(null);
+ const [queueQueryKey, setQueueQueryKey] = useState(null);
+ const [hasAutoRefreshed, setHasAutoRefreshed] = useState(false);
+
+ const hasQueueData = !!queueId;
+ const currentQueryKey = queryKey || title;
+
+ // Show queue if we have current queue data OR persistent queue data from the same query key
+ // If query key changed and we don't have an active queueId, don't show the tracker
+ const shouldShowQueue =
+ hasQueueData || (!!persistentQueueData && queueQueryKey === currentQueryKey);
+
+ // Check if queue is in a completed state based on persistent data only (to avoid circular dependency)
+ const isQueueCompleted =
+ persistentQueueData?.Status === "Completed" ||
+ persistentQueueData?.Status === "Failed" ||
+ persistentQueueData?.Status === "Completed (with errors)";
+
+ const effectiveQueueId = queueId || lastProcessedQueueId;
+
+ const queuePolling = ApiGetCall({
+ url: `/api/ListCippQueue`,
+ data: { QueueId: effectiveQueueId },
+ queryKey: `CippQueue-${effectiveQueueId || "unknown"}`,
+ waiting: shouldShowQueue && !!effectiveQueueId && !isQueueCompleted,
+ refetchInterval: (data) => {
+ // Check if the current data shows completion
+ const currentData = data?.[0];
+ const isCurrentCompleted =
+ currentData?.Status === "Completed" ||
+ currentData?.Status === "Failed" ||
+ currentData?.Status === "Completed (with errors)";
+
+ // Also check persistent data
+ const isPersistentCompleted =
+ persistentQueueData?.Status === "Completed" ||
+ persistentQueueData?.Status === "Failed" ||
+ persistentQueueData?.Status === "Completed (with errors)";
+
+ // Stop polling if either shows completion
+ if (isCurrentCompleted || isPersistentCompleted || !shouldShowQueue || !effectiveQueueId) {
+ return false;
+ }
+
+ return 3000;
+ },
+ refetchOnMount: true,
+ refetchOnWindowFocus: false,
+ });
+
+ const queueData = queuePolling.data?.[0];
+
+ // Handle queue data persistence - only update persistent queue data when we get a new QueueId
+ // and ensure it's pinned to the current query key
+ useEffect(() => {
+ const currentQueryKey = queryKey || title;
+
+ // If query key changed, clear all queue data
+ if (queueQueryKey && queueQueryKey !== currentQueryKey) {
+ setPersistentQueueData(null);
+ setLastProcessedQueueId(null);
+ setQueueQueryKey(currentQueryKey);
+ setHasAutoRefreshed(false);
+ return;
+ }
+
+ // Set query key if not set
+ if (!queueQueryKey) {
+ setQueueQueryKey(currentQueryKey);
+ }
+
+ // Only process new QueueId if we actually have one and it's different
+ if (queueId && queueId !== lastProcessedQueueId) {
+ // New QueueId detected, clear old persistent data and set new QueueId
+ setPersistentQueueData(null);
+ setLastProcessedQueueId(queueId);
+ setHasAutoRefreshed(false); // Reset auto-refresh flag for new queue
+ }
+
+ // Don't clear persistent data if queueId is temporarily null (during table refresh)
+ // Only clear if we explicitly get a different QueueId or change query/page
+ }, [queueId, lastProcessedQueueId, queryKey, title, queueQueryKey]);
+
+ // Update persistent queue data when new queue data is available
+ useEffect(() => {
+ const currentQueryKey = queryKey || title;
+
+ // Only update if we're on the same query key where the queue was initiated
+ if (queueData && queueId === lastProcessedQueueId && queueQueryKey === currentQueryKey) {
+ setPersistentQueueData(queueData);
+ }
+ }, [queueData, queueId, lastProcessedQueueId, queryKey, title, queueQueryKey]);
+
+ // Auto-refresh table when queue reaches 100% completion
+ useEffect(() => {
+ const currentQueryKey = queryKey || title;
+
+ // Only auto-refresh if we're on the same query key where the queue was initiated
+ // and we haven't already auto-refreshed for this queue completion
+ if (
+ !hasAutoRefreshed &&
+ (persistentQueueData?.Status === "Completed" ||
+ persistentQueueData?.Status === "Failed" ||
+ persistentQueueData?.Status === "Completed (with errors)") &&
+ queueQueryKey === currentQueryKey
+ ) {
+ // Queue is complete, invalidate the table query to refresh data
+ if (currentQueryKey) {
+ queryClient.invalidateQueries({ queryKey: [currentQueryKey] });
+ setHasAutoRefreshed(true); // Mark that we've auto-refreshed
+ // Call callback if provided
+ if (onQueueComplete) {
+ onQueueComplete();
+ }
+ }
+ }
+ }, [
+ hasAutoRefreshed,
+ persistentQueueData?.PercentComplete,
+ persistentQueueData?.Status,
+ queryKey,
+ title,
+ queryClient,
+ queueQueryKey,
+ onQueueComplete,
+ ]);
+
+ // Don't render anything if we don't have queue data to show
+ // Check for valid queueId or persistent queue data
+ if (!shouldShowQueue || (!queueId && !lastProcessedQueueId && !persistentQueueData)) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ ) : (persistentQueueData || queueData)?.Status === "Completed (with errors)" ? (
+
+ ) : (persistentQueueData || queueData)?.Status === "Failed" ? (
+
+ ) : (persistentQueueData || queueData)?.RunningTasks > 0 ? (
+
+ ) : (
+
+ )
+ }
+ overlap="circular"
+ anchorOrigin={{
+ vertical: "top",
+ horizontal: "right",
+ }}
+ >
+ setQueueCanvasVisible(true)}
+ sx={{
+ animation:
+ (persistentQueueData || queueData)?.Status !== "Completed" &&
+ (persistentQueueData || queueData)?.Status !== "Completed (with errors)" &&
+ (persistentQueueData || queueData)?.Status !== "Failed"
+ ? "pulse 2s infinite"
+ : "none",
+ "@keyframes pulse": {
+ "0%": {
+ transform: "scale(1)",
+ opacity: 1,
+ },
+ "50%": {
+ transform: "scale(1.1)",
+ opacity: 0.8,
+ },
+ "100%": {
+ transform: "scale(1)",
+ opacity: 1,
+ },
+ },
+ color:
+ (persistentQueueData || queueData)?.Status === "Completed"
+ ? "success.main"
+ : (persistentQueueData || queueData)?.Status === "Completed (with errors)"
+ ? "warning.main"
+ : (persistentQueueData || queueData)?.Status === "Failed"
+ ? "error.main"
+ : (persistentQueueData || queueData)?.RunningTasks > 0
+ ? "warning.main"
+ : "primary.main",
+ }}
+ >
+
+
+
+
+
+ {/* Queue Status OffCanvas */}
+ setQueueCanvasVisible(false)}
+ >
+
+ {persistentQueueData || queueData ? (
+ <>
+ {(persistentQueueData || queueData).Name}
+
+
+
+ Progress: {(persistentQueueData || queueData).PercentComplete?.toFixed(1)}%
+ complete
+
+
+
+
+
+
+ Total Tasks: {(persistentQueueData || queueData).TotalTasks || 0}
+
+
+ Completed:{" "}
+ {(persistentQueueData || queueData).CompletedTasks || 0}
+
+
+ Running: {(persistentQueueData || queueData).RunningTasks || 0}
+
+
+ Failed: {(persistentQueueData || queueData).FailedTasks || 0}
+
+
+
+
+ Status: {(persistentQueueData || queueData).Status}
+
+
+ {(persistentQueueData || queueData).Tasks &&
+ (persistentQueueData || queueData).Tasks.length > 0 && (
+ <>
+
+ Task Details
+
+
+
+ theme.palette.mode === "dark"
+ ? "rgba(255,255,255,0.1)"
+ : "rgba(0,0,0,0.1)",
+ borderRadius: 4,
+ },
+ "&::-webkit-scrollbar-thumb": {
+ backgroundColor: (theme) =>
+ theme.palette.mode === "dark"
+ ? "rgba(255,255,255,0.3)"
+ : "rgba(0,0,0,0.3)",
+ borderRadius: 4,
+ "&:hover": {
+ backgroundColor: (theme) =>
+ theme.palette.mode === "dark"
+ ? "rgba(255,255,255,0.5)"
+ : "rgba(0,0,0,0.5)",
+ },
+ },
+ }}
+ >
+ {(persistentQueueData || queueData).Tasks.map((task, index) => (
+ ({
+ p: 2,
+ border: 1,
+ borderColor:
+ theme.palette.mode === "dark"
+ ? "rgba(255,255,255,0.12)"
+ : "divider",
+ borderRadius: 1,
+ backgroundColor:
+ task.Status === "Completed"
+ ? theme.palette.mode === "dark"
+ ? "rgba(102, 187, 106, 0.15)"
+ : "success.light"
+ : task.Status === "Failed"
+ ? theme.palette.mode === "dark"
+ ? "rgba(244, 67, 54, 0.15)"
+ : "error.light"
+ : task.Status === "Running"
+ ? theme.palette.mode === "dark"
+ ? "rgba(255, 152, 0, 0.15)"
+ : "warning.light"
+ : theme.palette.mode === "dark"
+ ? "rgba(255,255,255,0.05)"
+ : "grey.100",
+ transition: "all 0.2s ease-in-out",
+ "&:hover": {
+ transform: "translateY(-1px)",
+ boxShadow:
+ theme.palette.mode === "dark"
+ ? "0 4px 8px rgba(0,0,0,0.3)"
+ : "0 4px 8px rgba(0,0,0,0.1)",
+ },
+ })}
+ >
+
+
+ {task.Name}
+
+ ({
+ px: 1.5,
+ py: 0.5,
+ borderRadius: 2,
+ backgroundColor:
+ theme.palette.mode === "dark"
+ ? "rgba(255,255,255,0.1)"
+ : "background.paper",
+ border:
+ theme.palette.mode === "dark"
+ ? "1px solid rgba(255,255,255,0.2)"
+ : "none",
+ fontWeight: "medium",
+ textTransform: "uppercase",
+ fontSize: "0.7rem",
+ letterSpacing: "0.5px",
+ color:
+ task.Status === "Completed"
+ ? "success.main"
+ : task.Status === "Failed"
+ ? "error.main"
+ : task.Status === "Running"
+ ? "warning.main"
+ : "text.secondary",
+ })}
+ >
+ {task.Status}
+
+
+ {task.Timestamp && (
+
+ {new Date(task.Timestamp).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ })}{" "}
+ {new Date(task.Timestamp).toLocaleTimeString(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ })}
+
+ )}
+
+ ))}
+
+
+ >
+ )}
+ >
+ ) : queuePolling.isLoading ? (
+ Loading queue data...
+ ) : queuePolling.isError ? (
+
+ Error loading queue data: {queuePolling.error?.message}
+
+ ) : (
+ No queue data available
+ )}
+
+
+ >
+ );
+};
diff --git a/src/components/CippTable/util-columnsFromAPI.js b/src/components/CippTable/util-columnsFromAPI.js
index 058db11048d3..8d93ccc19c18 100644
--- a/src/components/CippTable/util-columnsFromAPI.js
+++ b/src/components/CippTable/util-columnsFromAPI.js
@@ -2,11 +2,48 @@ import { getCippFilterVariant } from "../../utils/get-cipp-filter-variant";
import { getCippFormatting } from "../../utils/get-cipp-formatting";
import { getCippTranslation } from "../../utils/get-cipp-translation";
-const skipRecursion = ["location", "ScheduledBackupValues"];
+const skipRecursion = ["location", "ScheduledBackupValues", "Tenant"];
+
+// Variable replacement patterns - maps variable names to property patterns
+const variableReplacements = {
+ cippuserschema: (dataSample) => {
+ // Find the first property that contains "_cippUser"
+ const cippUserProp = Object.keys(dataSample).find((key) => key.includes("_cippUser"));
+ return cippUserProp || "cippuserschema"; // fallback to original if not found
+ },
+};
+
+// Function to resolve variable replacements in column names
+const resolveVariables = (columnName, dataSample) => {
+ return columnName.replace(/%(\w+)%/g, (match, variableName) => {
+ const resolver = variableReplacements[variableName.toLowerCase()];
+ if (resolver && typeof resolver === "function") {
+ const resolved = resolver(dataSample);
+ console.log("resolving " + match + " to " + resolved);
+ return resolved;
+ }
+ return match; // return original if no resolver found
+ });
+};
+
+const getAtPath = (obj, path) => {
+ const parts = path.split(".");
+ return parts.reduce((acc, part) => {
+ if (acc && typeof acc === "object") {
+ return acc[part];
+ }
+ return undefined;
+ }, obj);
+};
+
// Function to merge keys from all objects in the array
const mergeKeys = (dataArray) => {
return dataArray.reduce((acc, item) => {
const mergeRecursive = (obj, base = {}) => {
+ // Add null/undefined check before calling Object.keys
+ if (!obj || typeof obj !== 'object') {
+ return base;
+ }
Object.keys(obj).forEach((key) => {
if (
typeof obj[key] === "object" &&
@@ -14,37 +51,42 @@ const mergeKeys = (dataArray) => {
!Array.isArray(obj[key]) &&
!skipRecursion.includes(key)
) {
- if (typeof base[key] === "boolean") {
- // Skip merging if base[key] is a boolean
- return;
- }
- if (typeof base[key] !== "object" || Array.isArray(base[key])) {
- // Re-initialize base[key] if it's not an object
- base[key] = {};
- }
+ if (typeof base[key] === "boolean") return; // don't merge into a boolean
+ if (typeof base[key] !== "object" || Array.isArray(base[key])) base[key] = {};
base[key] = mergeRecursive(obj[key], base[key]);
} else if (typeof obj[key] === "boolean") {
base[key] = obj[key];
} else if (typeof obj[key] === "string" && obj[key].toUpperCase() === "FAILED") {
- base[key] = base[key]; // Keep existing value if it's 'FAILED'
+ // keep existing value if it's 'FAILED'
+ base[key] = base[key];
} else if (obj[key] !== undefined && obj[key] !== null) {
- base[key] = obj[key]; // Assign valid primitive values
+ base[key] = obj[key];
}
});
return base;
};
+ // Add null/undefined check before calling mergeRecursive
+ if (!item || typeof item !== 'object') {
+ return acc;
+ }
return mergeRecursive(item, acc);
}, {});
};
export const utilColumnsFromAPI = (dataArray) => {
+ // Add safety check for dataArray
+ if (!dataArray || !Array.isArray(dataArray) || dataArray.length === 0) {
+ return [];
+ }
+
const dataSample = mergeKeys(dataArray);
const generateColumns = (obj, parentKey = "") => {
return Object.keys(obj)
.map((key) => {
const accessorKey = parentKey ? `${parentKey}.${key}` : key;
+
if (
typeof obj[key] === "object" &&
obj[key] !== null &&
@@ -54,32 +96,58 @@ export const utilColumnsFromAPI = (dataArray) => {
return generateColumns(obj[key], accessorKey);
}
- return {
+ // Build a value resolver usable by both accessorFn/Cell and the filter util
+ const resolveValue = (rowLike) =>
+ accessorKey.includes("@odata") ? rowLike?.[accessorKey] : getAtPath(rowLike, accessorKey);
+
+ // Pre-compute some sample values for filter heuristics (optional)
+ const valuesForColumn = (Array.isArray(dataArray) ? dataArray : [])
+ .map((r) => resolveValue(r))
+ .filter((v) => v !== undefined && v !== null);
+
+ const sampleValue = valuesForColumn.length ? valuesForColumn[0] : undefined;
+
+ const column = {
header: getCippTranslation(accessorKey),
id: accessorKey,
accessorFn: (row) => {
- let value;
- if (accessorKey.includes("@odata")) {
- value = row[accessorKey];
- } else {
- value = accessorKey.split(".").reduce((acc, part) => acc && acc[part], row);
- }
+ const value = resolveValue(row);
return getCippFormatting(value, accessorKey, "text");
},
- ...getCippFilterVariant(key),
+ ...getCippFilterVariant(accessorKey, {
+ sampleValue,
+ values: valuesForColumn,
+ getValue: (row) => resolveValue(row),
+ dataArray: dataArray, // Pass the full data array for processing if needed
+ }),
Cell: ({ row }) => {
- let value;
- if (accessorKey.includes("@odata")) {
- value = row.original[accessorKey];
- } else {
- value = accessorKey.split(".").reduce((acc, part) => acc && acc[part], row.original);
- }
+ const value = resolveValue(row.original);
return getCippFormatting(value, accessorKey);
},
};
+
+ return column;
})
.flat();
};
return generateColumns(dataSample);
};
+
+// Helper function to resolve variables in simple column names
+export const resolveSimpleColumnVariables = (simpleColumns, dataArray) => {
+ if (!simpleColumns || !Array.isArray(dataArray) || dataArray.length === 0) {
+ return simpleColumns;
+ }
+
+ const dataSample = mergeKeys(dataArray);
+
+ return simpleColumns.map((columnName) => {
+ if (typeof columnName === "string" && columnName.includes("%")) {
+ const resolved = resolveVariables(columnName, dataSample);
+ console.log(`Resolving simple column: ${columnName} -> ${resolved}`);
+ return resolved;
+ }
+ return columnName;
+ });
+};
diff --git a/src/components/CippTable/util-tablemode.js b/src/components/CippTable/util-tablemode.js
index 17cd92006a7e..3914ee4ebd6a 100644
--- a/src/components/CippTable/util-tablemode.js
+++ b/src/components/CippTable/util-tablemode.js
@@ -53,7 +53,6 @@ export const utilTableMode = (
enableStickyHeader: true,
selectAllMode: "all",
enableColumnPinning: true,
- enableStickyHeader: true,
muiPaginationProps: {
rowsPerPageOptions: [25, 50, 100, 250, 500],
},
diff --git a/src/components/CippWizard/CIPPDeploymentStep.js b/src/components/CippWizard/CIPPDeploymentStep.js
deleted file mode 100644
index 070b38811dbf..000000000000
--- a/src/components/CippWizard/CIPPDeploymentStep.js
+++ /dev/null
@@ -1,379 +0,0 @@
-import { useEffect, useState } from "react";
-import {
- Alert,
- Button,
- Grid,
- Link,
- Stack,
- Typography,
- Skeleton,
- Box,
- CircularProgress,
- SvgIcon,
-} from "@mui/material";
-import CippFormComponent from "../CippComponents/CippFormComponent";
-import { CippWizardStepButtons } from "./CippWizardStepButtons";
-import { ApiGetCall } from "../../api/ApiCall";
-import CippButtonCard from "../CippCards/CippButtonCard";
-import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard";
-import { CheckCircle, OpenInNew, Sync } from "@mui/icons-material";
-import CippPermissionCheck from "../CippSettings/CippPermissionCheck";
-import { useQueryClient } from "@tanstack/react-query";
-import { CippApiResults } from "../CippComponents/CippApiResults";
-
-export const CippDeploymentStep = (props) => {
- const queryClient = useQueryClient();
- const { formControl, onPreviousStep, onNextStep, currentStep } = props;
- const values = formControl.getValues();
-
- const [currentStepState, setCurrentStepState] = useState(1);
- const [pollingStep, setPollingStep] = useState(1);
- const [approvalUrl, setApprovalUrl] = useState(true);
-
- const startSetupApi = ApiGetCall({
- url: "/api/ExecSAMSetup?CreateSAM=true&partnersetup=true",
- queryKey: "startSAMSetup",
- });
-
- const checkSetupStatusApi = ApiGetCall({
- url: `/api/ExecSAMSetup?CheckSetupProcess=true&step=${pollingStep}`,
- queryKey: `checkSetupStep${pollingStep}`,
- waiting: !pollingStep,
- });
- const appId = ApiGetCall({
- url: `/api/ExecListAppId`,
- queryKey: `ExecListAppId`,
- waiting: true,
- });
- useEffect(() => {
- if (
- startSetupApi.data &&
- startSetupApi.data.step === 1 &&
- values.selectedOption === "CreateApp"
- ) {
- formControl.register("wizardStatus", {
- required: true,
- });
- formControl.setValue("noSubmitButton", true);
- setPollingStep(1);
- setCurrentStepState(1);
- }
- }, [startSetupApi.data]);
-
- useEffect(() => {
- if (pollingStep && values.selectedOption === "CreateApp") {
- const intervalId = setInterval(() => {
- if (!checkSetupStatusApi.isFetching) {
- checkSetupStatusApi.refetch();
- }
- }, 5000);
- return () => clearInterval(intervalId);
- }
- }, [pollingStep, checkSetupStatusApi]);
-
- useEffect(() => {
- if (checkSetupStatusApi.data) {
- const { step, message, url, code } = checkSetupStatusApi.data;
- if (url) {
- setApprovalUrl(url);
- }
- if (step === 2) {
- setCurrentStepState(2);
- setPollingStep(2);
- } else if (step >= 3) {
- setCurrentStepState(4);
- setPollingStep(null);
- formControl.setValue(
- "wizardStatus",
- "You've executed the Setup Wizard. You may now navigate away from this wizard."
- );
- formControl.trigger();
- }
- }
- }, [checkSetupStatusApi.data, currentStepState]);
-
- const openPopup = (url) => {
- const width = 500;
- const height = 500;
- const left = window.screen.width / 2 - width / 2;
- const top = window.screen.height / 2 - height / 2;
- window.open(url, "_blank", `width=${width},height=${height},left=${left},top=${top}`);
- };
- return (
-
-
- {values.selectedOption === "CreateApp" && (
- <>
-
- To run this setup you will need the following prerequisites:
-
- A CIPP Service Account. For more information on how to create a service account,
- click{" "}
-
- here
-
-
-
(Temporary) Global Administrator permissions for the CIPP Service Account
-
- Multi-factor authentication enabled for the CIPP Service Account, with no trusted
- locations or other exclusions.
-
-
- {currentStepState >= 1 && (
-
- Step 1: Create Application
-
- {currentStepState <= 1 ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- variant="outlined"
- isFetching={startSetupApi.isLoading}
- CardButton={
- = 2}
- variant="contained"
- color="primary"
- onClick={() => openPopup("https://microsoft.com/devicelogin")}
- >
- Login to Microsoft
-
- }
- >
-
- Click the button below and enter the provided code. This creates the CIPP
- Application Registration in your tenant that allows you to access the Graph API.
- Login using your CIPP Service Account.
-
- {startSetupApi.isLoading ? (
-
- ) : (
-
- )}
-
- )}
- {currentStepState >= 2 && (
-
- Step 2: Approve Permissions
-
- {currentStepState <= 2 ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- CardButton={
- = 4 ||
- !approvalUrl ||
- !approvalUrl.startsWith("https://login") ||
- typeof approvalUrl !== "string"
- }
- onClick={() => openPopup(approvalUrl)}
- >
- Open Approval Link
-
- }
- >
-
- Step 2: Approvals Required
-
-
- Please open the link below and provide the required approval, this allows the app
- specific permissions shown in the next screen. Login using your CIPP Service
- Account.
-
-
- )}
-
- {/* Final Step 4 Card */}
- {currentStepState >= 4 && }
-
- {
- setPollingStep(1);
- setCurrentStepState(1);
- setApprovalUrl(null);
- queryClient.removeQueries("startSAMSetup");
- queryClient.removeQueries("checkSetupStep1");
- setTimeout(() => {
- startSetupApi.refetch();
- }, 200);
- }}
- >
- Start Over
-
-
-
-
-
-
- >
- )}
-
- {values.selectedOption === "UpdateTokens" && (
-
- Update Tokens
-
- {appId.isLoading ? (
-
- ) : (
-
-
-
- )}
-
-
- }
- CardButton={
- <>
- openPopup(appId?.data?.refreshUrl)}
- color="primary"
- startIcon={
-
- }
- >
- Refresh Graph Token
-
- appId.refetch()}
- variant="outlined"
- color="primary"
- startIcon={}
- disabled={appId.isFetching}
- >
- Check Application ID
-
- {!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
- appId?.data?.applicationId
- ) && (
-
- The Application ID is not valid. Please return to the first page of the SAM
- wizard and use the Manual .
-
- )}
- >
- }
- >
-
- Click the button below to refresh your token.
-
- {formControl.setValue("noSubmitButton", true)}
-
-
- )}
-
- {values.selectedOption === "Manual" && (
- <>
- {formControl.setValue("setKeys", true)}
-
- You may enter your secrets below. Leave fields blank to retain existing values.
-
- {
- const guidRegex =
- /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
- return value === "" || guidRegex.test(value) || "Invalid Tenant ID";
- },
- }}
- />
- {
- const guidRegex =
- /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
- return value === "" || guidRegex.test(value) || "Invalid Application ID";
- },
- }}
- />
- {
- const secretRegex = /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/;
- return (
- value === "" ||
- secretRegex.test(value) ||
- "This should be the secret value, not the secret ID"
- );
- },
- }}
- />
- {
- const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
- return value === "" || jwtRegex.test(value) || "Invalid Refresh Token";
- },
- }}
- />
- >
- )}
-
-
-
- );
-};
diff --git a/src/components/CippWizard/CIPPDeploymentStep.jsx b/src/components/CippWizard/CIPPDeploymentStep.jsx
new file mode 100644
index 000000000000..7a6553b9323c
--- /dev/null
+++ b/src/components/CippWizard/CIPPDeploymentStep.jsx
@@ -0,0 +1,101 @@
+import { useEffect } from "react";
+import { Stack, Typography } from "@mui/material";
+import CippFormComponent from "../CippComponents/CippFormComponent";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CIPPDeploymentUpdateTokens } from "./CIPPDeploymentUpdateTokens";
+
+export const CippDeploymentStep = (props) => {
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+ const values = formControl.getValues();
+
+ // Use useEffect to set form values instead of doing it during render
+ useEffect(() => {
+ if (values.selectedOption === "Manual") {
+ formControl.setValue("setKeys", true);
+ }
+ }, [values.selectedOption, formControl]);
+
+ return (
+
+
+ {values.selectedOption === "UpdateTokens" && (
+
+ )}
+
+ {values.selectedOption === "Manual" && (
+ <>
+
+ You may enter your secrets below. Leave fields blank to retain existing values.
+
+ {
+ const guidRegex =
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
+ return value === "" || guidRegex.test(value) || "Invalid Tenant ID";
+ },
+ }}
+ />
+ {
+ const guidRegex =
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
+ return value === "" || guidRegex.test(value) || "Invalid Application ID";
+ },
+ }}
+ />
+ {
+ const secretRegex =
+ /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/;
+ return (
+ value === "" ||
+ secretRegex.test(value) ||
+ "This should be the secret value, not the secret ID"
+ );
+ },
+ }}
+ />
+ {
+ const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
+ return value === "" || jwtRegex.test(value) || "Invalid Refresh Token";
+ },
+ }}
+ />
+ >
+ )}
+
+
+
+ );
+};
diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
new file mode 100644
index 000000000000..50fef63317f2
--- /dev/null
+++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx
@@ -0,0 +1,59 @@
+import { useState } from "react";
+import { Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material";
+import { CheckCircle } from "@mui/icons-material";
+import CippButtonCard from "../CippCards/CippButtonCard";
+import { ApiGetCall } from "../../api/ApiCall";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
+
+export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
+ const [tokens, setTokens] = useState(null);
+
+ // Get application ID information for the card header
+ const appId = ApiGetCall({
+ url: `/api/ExecListAppId`,
+ queryKey: `ExecListAppId`,
+ waiting: true,
+ });
+
+ // Handle successful authentication
+ const handleAuthSuccess = (tokenData) => {
+ setTokens(tokenData);
+ };
+
+ return (
+
+
+ Update Tokens
+
+ {appId.isLoading ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ }
+ CardButton={
+
+ }
+ >
+
+ Click the button to refresh the Graph token for your tenants using popup authentication.
+ This method opens a popup window where you can sign in to your Microsoft account.
+
+
+
+
+ );
+};
+
+export default CIPPDeploymentUpdateTokens;
diff --git a/src/components/CippWizard/CippAddTenantForm.jsx b/src/components/CippWizard/CippAddTenantForm.jsx
index a23291dd54c7..2e302bb02944 100644
--- a/src/components/CippWizard/CippAddTenantForm.jsx
+++ b/src/components/CippWizard/CippAddTenantForm.jsx
@@ -182,13 +182,13 @@ export const CippAddTenantForm = (props) => {
{field.type === "header" ? (
<>
-
+ {field.label}
>
) : (
-
+
)}
diff --git a/src/components/CippWizard/CippAlertsStep.jsx b/src/components/CippWizard/CippAlertsStep.jsx
new file mode 100644
index 000000000000..ba4e62c7f9f5
--- /dev/null
+++ b/src/components/CippWizard/CippAlertsStep.jsx
@@ -0,0 +1,89 @@
+import { Alert, Stack, Typography } from "@mui/material";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+
+export const CippAlertsStep = (props) => {
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+
+ const postExecutionOptions = [
+ { label: "Webhook", value: "Webhook" },
+ { label: "Email", value: "Email" },
+ { label: "PSA", value: "PSA" },
+ ];
+
+ const recurrenceOptions = [
+ { value: "30m", label: "Every 30 minutes" },
+ { value: "1h", label: "Every hour" },
+ { value: "4h", label: "Every 4 hours" },
+ { value: "1d", label: "Every 1 day" },
+ { value: "7d", label: "Every 7 days" },
+ { value: "30d", label: "Every 30 days" },
+ { value: "365d", label: "Every 365 days" },
+ ];
+
+ return (
+
+
+ Almost done
+
+
+ There's a couple more things that you can configure outside of the wizard, let's list
+ some of them;
+
+
+
+ CIPP has the ability to send alerts to your PSA, Webhook or Email. You can configure
+ these settings under > Tenant Administration > Alert Configuration.
+
+
+
+
+ If you imported baselines, or want to set tenants to your own baseline, you should
+ check out our standards under these settings under > Tenant Administration >
+ Standards.
+
+
+
+
+ If you want to use our integrations, you should set these up under > CIPP >
+ Integrations. Some examples are CSP integrations, Password Pusher, PSA, and more.
+
+
+
+
+ Adding more users to CIPP? you can do this via CIPP > Advanced > Super Admin.
+
+
+
+
+ You can deploy Windows Applications too, directly using intune. We have Chocolately,
+ WinGet, and RMM apps under > Intune > Applications. Some examples are CSP
+ integrations, Password Pusher, PSA, and more.
+
+
+
+
+ Tenants can be grouped, and you can implement custom variables for your tenants under
+ WinGet, and RMM apps under Tenant Administrator > Administration > Tenants.
+
+
+
+
+ Have an enterprise app you want to deploy? Check out our tools{" "}
+ section. This menu also contains useful things such as our geo-ip lookup, and more.
+
+
+
+
+
+
+
+ );
+};
+
+export default CippAlertsStep;
diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx
new file mode 100644
index 000000000000..0343e8a33f1c
--- /dev/null
+++ b/src/components/CippWizard/CippBaselinesStep.jsx
@@ -0,0 +1,105 @@
+import { Alert, Stack, Typography, FormControl, FormLabel, Box } from "@mui/material";
+import CippFormComponent from "../CippComponents/CippFormComponent";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippFormCondition } from "../CippComponents/CippFormCondition";
+
+export const CippBaselinesStep = (props) => {
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+
+ return (
+
+
+
+
+ Baselines are template configurations that can be used as examples for setting up your
+ environment. Don't want to configure these yet? No problem! You can find the templates
+ at Tools - Community Repositories
+
+
+ Downloading these baselines will create templates in your CIPP instance. These templates
+ won't make any changes to your environment, but can be used as examples on how to setup
+ environments. Each template library contains multiple templates,
+
+
+ CIPP Templates by CyberDrain contain several example standards, including low,
+ medium, and high priority standards
+
+
+ JoeyV's Conditional Access Baseline contains a Microsoft approved baseline for
+ Conditional Access, following the Microsoft best practices.
+
+
+ OpenIntuneBaseline contains Intune templates, the baseline is a community driven
+ baseline for Intune, based on CIS, NIST, and more benchmarks. It's considered the
+ leading baseline for Intune.
+
+
+
+
+
+
+ Baseline Configuration
+
+
+
+
+
+
+
+
+ Select baselines to download:
+
+ `${option.Name} (${option.Owner})`,
+ valueField: "FullName",
+ addedFields: {
+ templateRepoBranch: "main",
+ },
+ }}
+ multiple={true}
+ placeholder="Select one or more baselines"
+ />
+
+
+
+
+
+
+ );
+};
+
+export default CippBaselinesStep;
diff --git a/src/components/CippWizard/CippCAForm.jsx b/src/components/CippWizard/CippCAForm.jsx
deleted file mode 100644
index b99d69c8e55c..000000000000
--- a/src/components/CippWizard/CippCAForm.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Grid, Stack } from "@mui/material";
-import { CippWizardStepButtons } from "./CippWizardStepButtons";
-import CippJsonView from "../CippFormPages/CippJSONView";
-import CippFormComponent from "../CippComponents/CippFormComponent";
-import { ApiGetCall } from "../../api/ApiCall";
-import { useEffect, useState } from "react";
-import { useWatch } from "react-hook-form";
-
-export const CippCAForm = (props) => {
- const { formControl, onPreviousStep, onNextStep, currentStep } = props;
- const values = formControl.getValues();
- const CATemplates = ApiGetCall({ url: "/api/ListCATemplates" });
- const [JSONData, setJSONData] = useState();
- const watcher = useWatch({ control: formControl.control, name: "TemplateList" });
- useEffect(() => {
- if (CATemplates.isSuccess && watcher?.value) {
- const template = CATemplates.data.find((template) => template.GUID === watcher.value);
- if (template) {
- setJSONData(template);
- formControl.setValue("rawjson", JSON.stringify(template, null));
- }
- }
- }, [CATemplates, watcher]);
-
- return (
-
-
- ({
- label: template.displayName,
- value: template.GUID,
- }))
- : []
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/src/components/CippWizard/CippIntunePolicy.jsx b/src/components/CippWizard/CippIntunePolicy.jsx
index e21491751546..c94d4ef93864 100644
--- a/src/components/CippWizard/CippIntunePolicy.jsx
+++ b/src/components/CippWizard/CippIntunePolicy.jsx
@@ -1,9 +1,10 @@
-import { Grid, Stack } from "@mui/material";
+import { Stack } from "@mui/material";
+import { Grid } from "@mui/system";
import { CippWizardStepButtons } from "./CippWizardStepButtons";
import CippJsonView from "../CippFormPages/CippJSONView";
import CippFormComponent from "../CippComponents/CippFormComponent";
import { ApiGetCall } from "../../api/ApiCall";
-import { use, useEffect, useState } from "react";
+import { useEffect, useState } from "react";
import { useWatch } from "react-hook-form";
import { CippFormCondition } from "../CippComponents/CippFormCondition";
@@ -15,6 +16,28 @@ export const CippIntunePolicy = (props) => {
const watcher = useWatch({ control: formControl.control, name: "TemplateList" });
const jsonWatch = useWatch({ control: formControl.control, name: "RAWJson" });
const selectedTenants = useWatch({ control: formControl.control, name: "tenantFilter" });
+
+ // do not provide inputs for reserved placeholders
+ const reservedPlaceholders = [
+ "%serial%",
+ "%systemroot%",
+ "%systemdrive%",
+ "%temp%",
+ "%tenantid%",
+ "%tenantfilter%",
+ "%initialdomain%",
+ "%tenantname%",
+ "%partnertenantid%",
+ "%samappid%",
+ "%userprofile%",
+ "%username%",
+ "%userdomain%",
+ "%windir%",
+ "%programfiles%",
+ "%programfiles(x86)%",
+ "%programdata%",
+ ];
+
useEffect(() => {
if (CATemplates.isSuccess && watcher?.value) {
const template = CATemplates.data.find((template) => template.GUID === watcher.value);
@@ -58,7 +81,7 @@ export const CippIntunePolicy = (props) => {
/>
-
+ {
compareType="is"
compareValue="customGroup"
>
-
+ {
const rawJson = jsonWatch ? jsonWatch : "";
const placeholderMatches = [...rawJson.matchAll(/%(\w+)%/g)].map((m) => m[1]);
const uniquePlaceholders = Array.from(new Set(placeholderMatches));
- if (uniquePlaceholders.length === 0 || selectedTenants.length === 0) {
+ // Filter out reserved placeholders
+ const filteredPlaceholders = uniquePlaceholders.filter(
+ (placeholder) => !reservedPlaceholders.includes(`%${placeholder.toLowerCase()}%`)
+ );
+ if (filteredPlaceholders.length === 0 || selectedTenants.length === 0) {
return null;
}
- return uniquePlaceholders.map((placeholder) => (
-
+ return filteredPlaceholders.map((placeholder) => (
+
{selectedTenants.map((tenant, idx) => (
{
+ const { formControl, onPreviousStep, onNextStep, currentStep } = props;
+
+ return (
+
+
+ Notification Settings
+
+ Configure your notification settings. These settings will determine how you receive alerts
+ from CIPP. You can test your configuration using the "Send Test Alert" button. Don't want
+ to setup notifications yet? You can skip this step and configure it later via Application
+ Settings - Notifications
+
+ {/* Use the reusable notification form component */}
+
+
+
+ {/* Use the wizard step buttons for navigation */}
+
+
+ );
+};
+
+export default CippNotificationsStep;
diff --git a/src/components/CippWizard/CippPSACredentialsStep.js b/src/components/CippWizard/CippPSACredentialsStep.jsx
similarity index 100%
rename from src/components/CippWizard/CippPSACredentialsStep.js
rename to src/components/CippWizard/CippPSACredentialsStep.jsx
diff --git a/src/components/CippWizard/CippPSASyncOptions.js b/src/components/CippWizard/CippPSASyncOptions.jsx
similarity index 100%
rename from src/components/CippWizard/CippPSASyncOptions.js
rename to src/components/CippWizard/CippPSASyncOptions.jsx
diff --git a/src/components/CippWizard/CippSAMDeploy.jsx b/src/components/CippWizard/CippSAMDeploy.jsx
new file mode 100644
index 000000000000..2cb619fef7aa
--- /dev/null
+++ b/src/components/CippWizard/CippSAMDeploy.jsx
@@ -0,0 +1,133 @@
+import { useEffect, useState } from "react";
+import { Alert, Stack, Box, Link } from "@mui/material";
+import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import { ApiPostCall } from "../../api/ApiCall";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+
+export const CippSAMDeploy = (props) => {
+ const { formControl, currentStep, onPreviousStep, onNextStep } = props;
+ const [authStatus, setAuthStatus] = useState({
+ success: false,
+ error: null,
+ loading: false,
+ });
+
+ // Block next step until SAM app is created
+ formControl.register("SAMWizard", {
+ required: true,
+ });
+
+ // Set SAMWizard = true if auth is successful
+ useEffect(() => {
+ if (authStatus.success) {
+ formControl.setValue("SAMWizard", true);
+ formControl.trigger("SAMWizard");
+ }
+ }, [authStatus, formControl]);
+
+ const createSamApp = ApiPostCall({ urlfromdata: true });
+
+ const handleAuthSuccess = (tokenData) => {
+ setAuthStatus({
+ success: false,
+ error: null,
+ loading: true,
+ });
+
+ createSamApp.mutate({
+ url: "/api/ExecCreateSamApp",
+ data: { access_token: tokenData.accessToken },
+ });
+ };
+
+ const handleAuthError = (error) => {
+ setAuthStatus({
+ success: false,
+ error: error.errorMessage || "Authentication failed",
+ loading: false,
+ });
+ };
+
+ useEffect(() => {
+ if (createSamApp.isSuccess && authStatus.loading && createSamApp.data) {
+ const data = createSamApp.data?.data;
+ if (data.severity === "error") {
+ setAuthStatus({
+ success: false,
+ error: data.message || "Failed to create SAM application",
+ loading: false,
+ });
+ } else if (data.severity === "success") {
+ setAuthStatus({
+ success: true,
+ error: null,
+ loading: false,
+ });
+ }
+ }
+ }, [createSamApp, authStatus]);
+
+ useEffect(() => {
+ if (createSamApp.isError && authStatus.loading) {
+ setAuthStatus({
+ success: false,
+ error: "An error occurred while creating the SAM application",
+ loading: false,
+ });
+ }
+ }, [createSamApp, authStatus]);
+
+ return (
+
+
+ To run this setup you will need the following prerequisites:
+
+ A CIPP Service Account. For more information on how to create a service account, click{" "}
+
+ here
+
+
+
(Temporary) Global Administrator permissions for the CIPP Service Account
+
+ Multi-factor authentication enabled for the CIPP Service Account, with no trusted
+ locations or other exclusions.
+
+
+
+ {authStatus.error && (
+
+ {authStatus.error}
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CippSAMDeploy;
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
new file mode 100644
index 000000000000..8f8683af405e
--- /dev/null
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -0,0 +1,130 @@
+import { useEffect } from "react";
+import { Stack, Box, Typography, Link } from "@mui/material";
+import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
+import { CippApiResults } from "../CippComponents/CippApiResults";
+import { ApiPostCall } from "../../api/ApiCall";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippTenantTable } from "./CippTenantTable";
+
+export const CippTenantModeDeploy = (props) => {
+ const { formControl, currentStep, onPreviousStep, onNextStep } = props;
+
+ formControl.register("GDAPAuth", {
+ required: true,
+ });
+
+ const updateRefreshToken = ApiPostCall({ urlfromdata: true });
+ const addTenant = ApiPostCall({ urlfromdata: true });
+
+ useEffect(() => {
+ if (updateRefreshToken.isSuccess) {
+ formControl.setValue("GDAPAuth", true);
+ formControl.trigger("GDAPAuth");
+ }
+ if (addTenant.isSuccess) {
+ // Reset the form control for the next tenant addition
+ formControl.setValue("GDAPAuth", true);
+ formControl.trigger("GDAPAuth");
+ }
+ }, [updateRefreshToken.isSuccess, formControl, addTenant.isSuccess]);
+
+ return (
+
+
+
+ {/* Partner Tenant (GDAP) */}
+
+
+ Partner Tenant
+
+
+ Using GDAP is recommended for CIPP, however you can also authenticate to individual
+ tenants. It is still highly recommended to connect to your partner tenant first, even if
+ you are not a Microsoft CSP. This allows CIPP to send notifications, perform permission
+ checks, and update permissions when required.
+
+
+ Please remember to log onto a service account dedicated for CIPP. More info? Check out the{" "}
+
+ service account documentation
+
+ .
+
+
+
+
+ {
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: "GDAP",
+ };
+ updateRefreshToken.mutate({
+ url: "/api/ExecUpdateRefreshToken",
+ data: updatedTokenData,
+ });
+ }}
+ buttonText="Connect to Partner Tenant (Recommended)"
+ showSuccessAlert={false}
+ />
+
+
+
+
+ {/* Per-Tenant */}
+
+
+ Per-Tenant Authentication
+
+
+ Click the button below to connect to individual tenants. You can authenticate to multiple
+ tenants by repeating this step for each tenant you want to add. Accidentally added the
+ wrong tenant? Use the table below to remove it.
+
+
+
+
+ {
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: "perTenant",
+ };
+ addTenant.mutate({
+ url: "/api/ExecAddTenant",
+ data: updatedTokenData,
+ });
+ }}
+ buttonText="Connect to Separate Tenants"
+ showSuccessAlert={false}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CippTenantModeDeploy;
diff --git a/src/components/CippWizard/CippTenantStep.jsx b/src/components/CippWizard/CippTenantStep.jsx
index 90399921d0cd..5c5d9c1be708 100644
--- a/src/components/CippWizard/CippTenantStep.jsx
+++ b/src/components/CippWizard/CippTenantStep.jsx
@@ -12,6 +12,7 @@ export const CippTenantStep = (props) => {
currentStep,
onPreviousStep,
preText,
+ includeOffboardingDefaults = false,
} = props;
return (
@@ -23,6 +24,7 @@ export const CippTenantStep = (props) => {
formControl={formControl}
allTenants={allTenants}
type={type}
+ includeOffboardingDefaults={includeOffboardingDefaults}
preselectedEnabled={true}
/>
{
+ const createDialog = useDialog();
+
+ // Actions formatted as per your guidelines
+ const actions = [
+ {
+ label: "Exclude Tenants",
+ type: "POST",
+ url: `/api/ExecExcludeTenant?AddExclusion=true`,
+ icon: ,
+ data: { value: "customerId" },
+ confirmText: "Are you sure you want to exclude [displayName]?",
+ multiPost: false,
+ condition: (row) => row.displayName !== "*Partner Tenant",
+ },
+ {
+ label: "Include Tenants",
+ type: "POST",
+ url: `/api/ExecExcludeTenant?RemoveExclusion=true`,
+ icon: ,
+ data: { value: "customerId" },
+ confirmText: "Are you sure you want to include [displayName]?",
+ multiPost: false,
+ condition: (row) => row.displayName !== "*Partner Tenant",
+ },
+ {
+ label: "Refresh CPV Permissions",
+ type: "POST",
+ url: `/api/ExecCPVPermissions`,
+ icon: ,
+ data: { tenantFilter: "customerId" },
+ confirmText: "Are you sure you want to refresh the CPV permissions for [displayName]?",
+ multiPost: false,
+ },
+ {
+ label: "Reset CPV Permissions",
+ type: "POST",
+ url: `/api/ExecCPVPermissions?&ResetSP=true`,
+ icon: ,
+ data: { tenantFilter: "customerId" },
+ confirmText:
+ "Are you sure you want to reset the CPV permissions for [displayName]? (This will delete the Service Principal and re-add it.)",
+ multiPost: false,
+ condition: (row) =>
+ row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant",
+ },
+ {
+ label: "Remove Tenant",
+ type: "POST",
+ url: `/api/ExecRemoveTenant`,
+ icon: ,
+ data: { TenantID: "customerId" },
+ confirmText:
+ "Are you sure you want to remove [displayName]? If this is a Direct Tenant, this will no longer be accessible until you add it via the Setup Wizard.",
+ multiPost: false,
+ condition: (row) => row.displayName !== "*Partner Tenant",
+ },
+ ];
+
+ // Offcanvas details
+ const offCanvas = {
+ extendedInfoFields: [
+ "displayName",
+ "defaultDomainName",
+ "delegatedPrivilegeStatus",
+ "Excluded",
+ "ExcludeDate",
+ "ExcludeUser",
+ ],
+ actions: actions,
+ };
+
+ // Columns for the table
+ const columns = customColumns || [
+ "displayName", // Tenant Name
+ "defaultDomainName", // Default Domain
+ "delegatedPrivilegeStatus", // Delegated Privilege Status
+ "Excluded", // Excluded Status
+ "ExcludeDate", // Exclude Date
+ "ExcludeUser", // Exclude User
+ ];
+
+ // Default filters
+ const defaultFilters = [
+ {
+ filterName: "Included tenants",
+ value: [{ id: "Excluded", value: "No" }],
+ type: "column",
+ },
+ {
+ filterName: "Excluded tenants",
+ value: [{ id: "Excluded", value: "Yes" }],
+ type: "column",
+ },
+ ];
+
+ const filters = customFilters || defaultFilters;
+
+ return (
+ <>
+
+
+
+
+ Force Refresh
+
+ ) : null
+ }
+ tenantInTitle={tenantInTitle}
+ apiUrl="/api/ExecExcludeTenant?ListAll=True"
+ actions={actions}
+ offCanvas={offCanvas}
+ simpleColumns={columns}
+ filters={filters}
+ showTenantSelector={showTenantSelector}
+ showAllTenantsSelector={showAllTenantsSelector}
+ />
+ {showCardButton && !onRefreshButtonClick && (
+
+ )}
+ >
+ );
+};
+
+export default CippTenantTable;
diff --git a/src/components/CippWizard/CippWizard.jsx b/src/components/CippWizard/CippWizard.jsx
index f0cc5f3013b3..22f24de234bc 100644
--- a/src/components/CippWizard/CippWizard.jsx
+++ b/src/components/CippWizard/CippWizard.jsx
@@ -1,11 +1,34 @@
import { useCallback, useMemo, useState } from "react";
import { Card, CardContent, Container, Stack } from "@mui/material";
-import Grid from "@mui/material/Grid2";
+import { Grid } from "@mui/system";
import { WizardSteps } from "./wizard-steps";
-import { useForm } from "react-hook-form";
+import { useForm, useWatch } from "react-hook-form";
export const CippWizard = (props) => {
- const { postUrl, orientation = "horizontal", steps } = props;
+ const {
+ postUrl,
+ orientation = "horizontal",
+ steps,
+ contentMaxWidth = "md",
+ } = props;
+
+ const formControl = useForm({ mode: "onChange", defaultValues: props.initialState });
+ const formWatcher = useWatch({
+ control: formControl.control,
+ });
+
+ const stepsWithVisibility = useMemo(() => {
+ return steps.filter((step) => {
+ if (step.hideStepWhen) {
+ return !step.hideStepWhen(formWatcher);
+ }
+ if (step.showStepWhen) {
+ return step.showStepWhen(formWatcher);
+ }
+ return true;
+ });
+ }, [steps, formWatcher]);
+
const [activeStep, setActiveStep] = useState(0);
const handleBack = useCallback(() => {
setActiveStep((prevState) => (prevState > 0 ? prevState - 1 : prevState));
@@ -14,40 +37,48 @@ export const CippWizard = (props) => {
const handleNext = useCallback(() => {
setActiveStep((prevState) => (prevState < steps.length - 1 ? prevState + 1 : prevState));
}, []);
- const formControl = useForm({ mode: "onChange", defaultValues: props.initialState });
+
const content = useMemo(() => {
- const StepComponent = steps[activeStep].component;
+ const currentStep = stepsWithVisibility[activeStep];
+ const StepComponent = currentStep.component;
+
return (
);
- }, [activeStep, handleNext, handleBack, steps, formControl]);
+ }, [activeStep, handleNext, handleBack, stepsWithVisibility, formControl]);
+
+ // Get the maxWidth for the current step, fallback to global setting
+ const currentStepMaxWidth = useMemo(() => {
+ const currentStep = stepsWithVisibility[activeStep];
+ return currentStep.maxWidth ?? contentMaxWidth;
+ }, [activeStep, stepsWithVisibility, contentMaxWidth]);
return (
{orientation === "vertical" ? (
-
+
-
+
{content}
@@ -59,10 +90,10 @@ export const CippWizard = (props) => {
postUrl={postUrl}
activeStep={activeStep}
orientation={orientation}
- steps={steps}
+ steps={stepsWithVisibility}
/>
- {content}
+ {content}
diff --git a/src/components/CippWizard/CippWizardAppApproval.jsx b/src/components/CippWizard/CippWizardAppApproval.jsx
index a32eef3a32de..ba53800a2cb2 100644
--- a/src/components/CippWizard/CippWizardAppApproval.jsx
+++ b/src/components/CippWizard/CippWizardAppApproval.jsx
@@ -1,51 +1,205 @@
-import { Stack } from "@mui/material";
+import { Stack, Alert } from "@mui/material";
import CippWizardStepButtons from "./CippWizardStepButtons";
import { Grid } from "@mui/system";
import CippFormComponent from "../CippComponents/CippFormComponent";
import { getCippValidator } from "../../utils/get-cipp-validator";
import { CippFormCondition } from "../CippComponents/CippFormCondition";
+import CippPermissionPreview from "../CippComponents/CippPermissionPreview";
+import { useWatch } from "react-hook-form";
+import { CippPropertyListCard } from "../CippCards/CippPropertyListCard";
export const CippWizardAppApproval = (props) => {
const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props;
+ // Watch for the selected template to access permissions and type
+ const selectedTemplate = useWatch({
+ control: formControl.control,
+ name: "selectedTemplate",
+ });
+
return (
-
-
-
+
+ {/* Mode Selector */}
+
+
+ {/* Template Mode */}
+
+
+
+ Select an app approval template to deploy. Templates contain predefined permissions that
+ will be applied to the application.
+ getCippValidator(value, "guid"),
+ type="autoComplete"
+ name="selectedTemplate"
+ label="Select App Template"
+ api={{
+ url: "/api/ListAppApprovalTemplates",
+ queryKey: "appApprovalTemplates",
+ labelField: (item) => `${item.TemplateName}`,
+ valueField: "TemplateId",
+ addedField: {
+ AppId: "AppId",
+ AppName: "AppName",
+ AppType: "AppType",
+ GalleryTemplateId: "GalleryTemplateId",
+ GalleryInformation: "GalleryInformation",
+ PermissionSetId: "PermissionSetId",
+ PermissionSetName: "PermissionSetName",
+ Permissions: "Permissions",
+ ApplicationManifest: "ApplicationManifest",
+ },
+ showRefresh: true,
}}
- name="AppId"
+ validators={{ required: "A template is required" }}
formControl={formControl}
+ multiple={false}
/>
-
-
-
+
+ {selectedTemplate?.addedFields?.AppName && (
+
+
+ {(selectedTemplate.addedFields.AppType || "EnterpriseApp") === "EnterpriseApp" ? (
+
+ ) : (selectedTemplate.addedFields.AppType || "EnterpriseApp") ===
+ "ApplicationManifest" ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {/* Manual Mode */}
+
+
+ getCippValidator(value, "guid"),
+ }}
+ name="AppId"
+ formControl={formControl}
+ />
+
+
+
+
+
+
+
+
{
+ const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props;
+ const templateSelection = useWatch({ control: formControl.control, name: "TemplateList" });
+ const assignmentFilterManagementType =
+ useWatch({
+ control: formControl?.control ?? formControl,
+ name: "assignmentFilterManagementType",
+ defaultValue: "devices",
+ }) ?? "devices";
+ const platformOptions =
+ assignmentFilterManagementType === "apps" ? APP_PLATFORM_OPTIONS : DEVICE_PLATFORM_OPTIONS;
+
+ useEffect(() => {
+ if (templateSelection?.value) {
+ const { addedFields } = templateSelection;
+
+ formControl.setValue(
+ "assignmentFilterManagementType",
+ addedFields.assignmentFilterManagementType || "devices"
+ );
+ formControl.setValue("platform", addedFields.platform || "");
+ formControl.setValue("displayName", addedFields.displayName || "");
+ formControl.setValue("description", addedFields.description || "");
+ formControl.setValue("rule", addedFields.rule || "");
+ }
+ }, [templateSelection, formControl]);
+
+ return (
+
+
+
+
+ `${option.Displayname || option.displayName} (${option.platform})`,
+ valueField: "GUID",
+ addedField: {
+ platform: "platform",
+ displayName: "displayName",
+ description: "description",
+ rule: "rule",
+ assignmentFilterManagementType: "assignmentFilterManagementType",
+ },
+ showRefresh: true,
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/CippWizard/CippWizardAutoComplete.jsx b/src/components/CippWizard/CippWizardAutoComplete.jsx
index a4e8f237f911..7012b80c5aba 100644
--- a/src/components/CippWizard/CippWizardAutoComplete.jsx
+++ b/src/components/CippWizard/CippWizardAutoComplete.jsx
@@ -29,7 +29,7 @@ export const CippWizardAutoComplete = (props) => {
api={{
...api,
tenantFilter: currentTenant ? currentTenant.value : undefined,
- queryKey: `${api.url}-${currentTenant ? currentTenant.value : "default"}`,
+ queryKey: api.queryKey ? api.queryKey.replace('{tenant}', currentTenant ? currentTenant.value : "default") : `${api.url}-${currentTenant ? currentTenant.value : "default"}`,
}}
multiple={type === "single" ? false : true}
disableClearable={true}
diff --git a/src/components/CippWizard/CippWizardAutopilotImport.jsx b/src/components/CippWizard/CippWizardAutopilotImport.jsx
new file mode 100644
index 000000000000..fc7876e34698
--- /dev/null
+++ b/src/components/CippWizard/CippWizardAutopilotImport.jsx
@@ -0,0 +1,483 @@
+import {
+ Button,
+ Link,
+ Stack,
+ Box,
+ Typography,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Alert,
+} from "@mui/material";
+import { Grid } from "@mui/system";
+import { CippWizardStepButtons } from "./CippWizardStepButtons";
+import { CippDataTable } from "../CippTable/CippDataTable";
+import { useWatch } from "react-hook-form";
+import { Delete, FileDownload, Upload, Add } from "@mui/icons-material";
+import { useEffect, useState } from "react";
+import React from "react";
+
+export const CippWizardAutopilotImport = (props) => {
+ const {
+ onNextStep,
+ formControl,
+ currentStep,
+ onPreviousStep,
+ fields,
+ name,
+ nameToCSVMapping,
+ fileName = "template",
+ } = props;
+ const tableData = useWatch({ control: formControl.control, name: name });
+ const [newTableData, setTableData] = useState([]);
+ const fileInputRef = React.useRef(null);
+ const [manualDialogOpen, setManualDialogOpen] = useState(false);
+ const [manualInputs, setManualInputs] = useState([{}]);
+ const inputRefs = React.useRef([]);
+ const [validationErrors, setValidationErrors] = useState([]);
+
+ const handleRemoveItem = (row) => {
+ if (row === undefined) return false;
+ const index = tableData?.findIndex((item) => item === row);
+ const newTableData = [...tableData];
+ newTableData.splice(index, 1);
+ setTableData(newTableData);
+ };
+
+ const handleFileSelect = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const text = e.target.result;
+ const lines = text.split('\n');
+ const firstLine = lines[0].split(',').map(header => header.trim());
+
+ // Check if this is a headerless CSV (no recognizable headers)
+ const hasHeaders = firstLine.some(header => {
+ // Check if any header matches our expected field names
+ return fields.some(field =>
+ header === field.propertyName ||
+ header === field.friendlyName ||
+ (field.alternativePropertyNames && field.alternativePropertyNames.includes(header))
+ );
+ });
+
+ let headers, headerMapping;
+
+ if (hasHeaders) {
+ // Normal CSV with headers
+ headers = firstLine;
+
+ // Create mapping for property names and alternative property names
+ headerMapping = {};
+ fields.forEach(field => {
+ // Map primary property name to itself
+ headerMapping[field.propertyName] = field.propertyName;
+ // Map friendly name to property name
+ headerMapping[field.friendlyName] = field.propertyName;
+ // Map alternative property names to the primary property name
+ if (field.alternativePropertyNames) {
+ field.alternativePropertyNames.forEach(altName => {
+ headerMapping[altName] = field.propertyName;
+ });
+ }
+ });
+
+ // Check if all required columns are present (using any of the supported formats)
+ const missingColumns = fields.filter(field => {
+ // Only serial number is required
+ if (field.propertyName !== 'SerialNumber') {
+ return false; // Skip non-required fields
+ }
+
+ const hasPropertyName = headers.includes(field.propertyName);
+ const hasFriendlyName = headers.includes(field.friendlyName);
+ const hasAlternativeName = field.alternativePropertyNames ?
+ field.alternativePropertyNames.some(altName => headers.includes(altName)) : false;
+ return !hasPropertyName && !hasFriendlyName && !hasAlternativeName;
+ });
+
+ if (missingColumns.length > 0) {
+ const missingFormats = missingColumns.map(f => {
+ const formats = [f.propertyName, f.friendlyName];
+ if (f.alternativePropertyNames) {
+ formats.push(...f.alternativePropertyNames);
+ }
+ return `"${formats.join('" or "')}"`;
+ }).join(', ');
+ console.error(`CSV is missing required columns: ${missingFormats}`);
+ return;
+ }
+ } else {
+ // Headerless CSV - assume order: serial, productid, hash
+ headers = ['SerialNumber', 'productKey', 'hardwareHash'];
+ headerMapping = {
+ 'SerialNumber': 'SerialNumber',
+ 'productKey': 'productKey',
+ 'hardwareHash': 'hardwareHash'
+ };
+
+ // Check if we have at least 3 columns for the expected order
+ if (firstLine.length < 3) {
+ console.error('Headerless CSV must have at least 3 columns in order: Serial Number, Product ID, Hardware Hash');
+ return;
+ }
+ }
+
+ const data = lines.slice(hasHeaders ? 1 : 0) // Skip first line only if it has headers
+ .filter(line => line.trim() !== '') // Remove empty lines
+ .map(line => {
+ const values = line.split(',');
+ // Initialize with all fields as empty strings
+ const row = fields.reduce((obj, field) => {
+ obj[field.propertyName] = '';
+ return obj;
+ }, {});
+ // Fill in the values from the CSV
+ headers.forEach((header, i) => {
+ const propertyName = headerMapping[header];
+ if (propertyName) {
+ row[propertyName] = values[i]?.trim() || '';
+ }
+ });
+ return row;
+ });
+
+ setTableData(data);
+ formControl.setValue(name, data, { shouldValidate: true });
+ };
+ reader.readAsText(file);
+ }
+ };
+
+ const handleManualInputChange = (rowIndex, field, value) => {
+ setManualInputs(prev => {
+ const newInputs = [...prev];
+ if (!newInputs[rowIndex]) {
+ newInputs[rowIndex] = {};
+ }
+ newInputs[rowIndex][field] = value;
+ return newInputs;
+ });
+ };
+
+ const handleAddRow = () => {
+ setManualInputs(prev => [...prev, {}]);
+ };
+
+ const validateRows = (rows) => {
+ const errors = [];
+ const seenSerials = new Set();
+ const seenProductKeys = new Set();
+
+ rows.forEach((row, index) => {
+ const serialField = fields.find(f => f.propertyName === 'SerialNumber');
+ const productKeyField = fields.find(f => f.propertyName === 'productKey');
+ const manufacturerField = fields.find(f => f.propertyName === 'oemManufacturerName');
+ const modelField = fields.find(f => f.propertyName === 'modelName');
+ const hardwareHashField = fields.find(f => f.propertyName === 'hardwareHash');
+
+ if (serialField && row[serialField.propertyName] && seenSerials.has(row[serialField.propertyName])) {
+ errors.push(`Row ${index + 1}: Duplicate serial number "${row[serialField.propertyName]}"`);
+ }
+ if (serialField && row[serialField.propertyName]) {
+ seenSerials.add(row[serialField.propertyName]);
+ }
+
+ if (productKeyField && row[productKeyField.propertyName] && seenProductKeys.has(row[productKeyField.propertyName])) {
+ errors.push(`Row ${index + 1}: Duplicate product key "${row[productKeyField.propertyName]}"`);
+ }
+ if (productKeyField && row[productKeyField.propertyName]) {
+ seenProductKeys.add(row[productKeyField.propertyName]);
+ }
+
+ // Validate Product ID length (must be exactly 13 characters)
+ if (productKeyField && row[productKeyField.propertyName] && row[productKeyField.propertyName].length !== 13) {
+ errors.push(`Row ${index + 1}: Product ID must be exactly 13 characters long`);
+ }
+
+ // Validate Serial Number requirements: must have either Manufacturer+Model OR Hardware Hash
+ if (serialField && row[serialField.propertyName] && row[serialField.propertyName].trim() !== '') {
+ const hasManufacturer = manufacturerField && row[manufacturerField.propertyName] && row[manufacturerField.propertyName].trim() !== '';
+ const hasModel = modelField && row[modelField.propertyName] && row[modelField.propertyName].trim() !== '';
+ const hasHardwareHash = hardwareHashField && row[hardwareHashField.propertyName] && row[hardwareHashField.propertyName].trim() !== '';
+
+ const hasManufacturerAndModel = hasManufacturer && hasModel;
+ const hasHash = hasHardwareHash;
+
+ if (!hasManufacturerAndModel && !hasHash) {
+ errors.push(`Row ${index + 1}: Serial Number must be accompanied by either both Manufacturer and Model, or Hardware Hash`);
+ }
+ }
+ });
+
+ setValidationErrors(errors);
+ return errors.length === 0;
+ };
+
+ const handleManualAdd = () => {
+ const newRows = manualInputs.filter(row =>
+ Object.values(row).some(value => value && value.trim() !== '')
+ ).map(row => {
+ // Ensure all fields exist in the row
+ return fields.reduce((obj, field) => {
+ obj[field.propertyName] = row[field.propertyName] || '';
+ return obj;
+ }, {});
+ });
+
+ if (newRows.length === 0) {
+ setManualDialogOpen(false);
+ setManualInputs([{}]);
+ return;
+ }
+
+ if (!validateRows(newRows)) {
+ return;
+ }
+
+ const updatedData = [...tableData, ...newRows];
+ setTableData(updatedData);
+ formControl.setValue(name, updatedData, { shouldValidate: true });
+ setManualInputs([{}]);
+ setManualDialogOpen(false);
+ };
+
+ const handleDialogClose = () => {
+ setManualDialogOpen(false);
+ setManualInputs([{}]);
+ };
+
+ const handleKeyPress = (event, rowIndex) => {
+ const productKeyField = fields.find(f => f.propertyName === 'productKey');
+ if (event.key === 'Enter' && productKeyField && manualInputs[rowIndex]?.[productKeyField.propertyName]) {
+ if (rowIndex === manualInputs.length - 1) {
+ const newRowIndex = manualInputs.length;
+ setManualInputs(prev => [...prev, {}]);
+ // Wait for the next render cycle to set focus
+ setTimeout(() => {
+ const newInput = inputRefs.current[newRowIndex]?.[productKeyField.propertyName];
+ if (newInput) {
+ newInput.focus();
+ }
+ }, 0);
+ }
+ }
+ };
+
+ const handleRemoveRow = (rowIndex) => {
+ setManualInputs(prev => prev.filter((_, index) => index !== rowIndex));
+ };
+
+ useEffect(() => {
+ console.log('Table Data:', newTableData);
+ formControl.setValue(name, newTableData, {
+ shouldValidate: true,
+ });
+ }, [newTableData]);
+
+ // Add effect to validate rows when manualInputs changes
+ useEffect(() => {
+ validateRows(manualInputs);
+ }, [manualInputs]);
+
+ const actions = [
+ {
+ icon: ,
+ label: "Delete Row",
+ confirmText: "Are you sure you want to delete this row?",
+ customFunction: handleRemoveItem,
+ noConfirm: true,
+ },
+ ];
+
+ return (
+
+ f.propertyName)}
+ cardButton={
+
+ f.propertyName).join(",") + "\n")}`}
+ download={`${fileName}.csv`}
+ startIcon={}
+ size="small"
+ >
+ Download Template
+
+
+ }
+ onClick={() => fileInputRef.current?.click()}
+ size="small"
+ >
+ Import from CSV
+
+ }
+ onClick={() => setManualDialogOpen(true)}
+ size="small"
+ >
+ Manual Import
+
+
+ }
+ />
+
+
+ Manual Import
+
+
+ {validationErrors.length > 0 && (
+
+
+ Please fix the following validation errors:
+
+ {validationErrors.map((error, index) => (
+
+ β’ {error}
+
+ ))}
+
+ )}
+ {manualInputs.map((row, rowIndex) => (
+
+ {/* Row identifier */}
+
+ {rowIndex + 1}
+
+ {fields.map((field) => (
+
+ {
+ if (!inputRefs.current[rowIndex]) {
+ inputRefs.current[rowIndex] = {};
+ }
+ inputRefs.current[rowIndex][field.propertyName] = el;
+ }}
+ label={field.friendlyName}
+ value={row[field.propertyName] || ''}
+ onChange={(e) => handleManualInputChange(rowIndex, field.propertyName, e.target.value)}
+ onKeyDown={(e) => field.propertyName === 'productKey' && handleKeyPress(e, rowIndex)}
+ fullWidth
+ size="small"
+ />
+
+ ))}
+ handleRemoveRow(rowIndex)}
+ disabled={manualInputs.length === 1}
+ sx={{
+ minWidth: '48px',
+ height: '40px',
+ fontSize: '24px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ alignSelf: 'center',
+ mr: 2
+ }}
+ color="error"
+ >
+ Γ
+
+
+ ))}
+
+ value && value.trim() !== '')}
+ sx={{
+ minWidth: '48px',
+ height: '40px',
+ fontSize: '24px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ alignSelf: 'center',
+ mr: 2
+ }}
+ >
+ +
+
+
+
+
+
+ Cancel
+ 0 || !Object.values(manualInputs[manualInputs.length - 1]).some(value => value && value.trim() !== '')}
+ >
+ Add
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/CippWizard/CippWizardAutopilotOptions.jsx b/src/components/CippWizard/CippWizardAutopilotOptions.jsx
index 154eebbab74b..a289a0589c89 100644
--- a/src/components/CippWizard/CippWizardAutopilotOptions.jsx
+++ b/src/components/CippWizard/CippWizardAutopilotOptions.jsx
@@ -1,4 +1,5 @@
-import { Grid, Stack } from "@mui/material";
+import { Stack } from "@mui/material";
+import { Grid } from "@mui/system";
import CippWizardStepButtons from "./CippWizardStepButtons";
import CippFormComponent from "../CippComponents/CippFormComponent";
export const CippWizardAutopilotOptions = (props) => {
@@ -8,7 +9,7 @@ export const CippWizardAutopilotOptions = (props) => {
<>
-
+ {
<>
-
+ {
formControl={formControl}
/>
-
+ {
{fields.map((field) => (
<>
-
+ {
>
))}
-
+ handleAddItem()}>
Add Item
@@ -116,7 +108,7 @@ export const CippWizardCSVImport = (props) => {
{!manualFields && (
<>
-
+ setOpen(true)}>
Add Item
@@ -127,7 +119,7 @@ export const CippWizardCSVImport = (props) => {
{fields.map((field) => (
-
+ {
/>
);
-};
+};
\ No newline at end of file
diff --git a/src/components/CippWizard/CippWizardConfirmation.js b/src/components/CippWizard/CippWizardConfirmation.js
deleted file mode 100644
index 3e8ae6c78bff..000000000000
--- a/src/components/CippWizard/CippWizardConfirmation.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { Card, Stack, Grid } from "@mui/material";
-import { PropertyList } from "../property-list";
-import CippWizardStepButtons from "./CippWizardStepButtons";
-import { PropertyListItem } from "../property-list-item";
-import { getCippTranslation } from "../../utils/get-cipp-translation";
-import { getCippFormatting } from "../../utils/get-cipp-formatting";
-
-export const CippWizardConfirmation = (props) => {
- const { postUrl, lastStep, formControl, onPreviousStep, onNextStep, currentStep } = props;
- const formValues = formControl.getValues();
- const formEntries = Object.entries(formValues);
- //remove all entries in "blacklist" from showing on confirmation page
- const blacklist = [
- "selectedOption",
- "GUID",
- "ID",
- "noSubmitButton",
- "RAWJson",
- "TemplateList",
- "addrow",
- ];
-
- const tenantEntry = formEntries.find(([key]) => key === "tenantFilter" || key === "tenant");
- const userEntry = formEntries.find(([key]) =>
- ["user", "userPrincipalName", "username"].includes(key)
- );
- const filteredEntries = formEntries.filter(
- ([key]) =>
- !blacklist.includes(key) &&
- key !== "tenantFilter" &&
- key !== "tenant" &&
- !["user", "userPrincipalName", "username"].includes(key)
- );
-
- const halfIndex = Math.ceil(filteredEntries.length / 2);
- const firstHalf = filteredEntries.slice(0, halfIndex);
- const secondHalf = filteredEntries.slice(halfIndex);
-
- if (tenantEntry) {
- firstHalf.unshift(tenantEntry);
- }
-
- if (userEntry) {
- secondHalf.unshift(userEntry);
- }
-
- return (
-
-
-
-
-
- {firstHalf.map(([key, value]) => {
- const formattedValue = getCippFormatting(value, key);
- const label = getCippTranslation(key);
- return ;
- })}
-
-
-
-
- {secondHalf.map(([key, value]) => {
- const formattedValue = getCippFormatting(value, key);
- const label = getCippTranslation(key);
- return ;
- })}
-
-
-
-
-
-
-
- );
-};
-
-export default CippWizardConfirmation;
diff --git a/src/components/CippWizard/CippWizardConfirmation.jsx b/src/components/CippWizard/CippWizardConfirmation.jsx
new file mode 100644
index 000000000000..3f8ecd288590
--- /dev/null
+++ b/src/components/CippWizard/CippWizardConfirmation.jsx
@@ -0,0 +1,150 @@
+import { Card, Stack, Typography } from "@mui/material";
+import { Grid } from "@mui/system";
+import { PropertyList } from "../property-list";
+import { PropertyListItem } from "../property-list-item";
+import CippWizardStepButtons from "./CippWizardStepButtons";
+import { getCippTranslation } from "../../utils/get-cipp-translation";
+import { getCippFormatting } from "../../utils/get-cipp-formatting";
+
+export const CippWizardConfirmation = (props) => {
+ const {
+ postUrl,
+ lastStep,
+ formControl,
+ onPreviousStep,
+ onNextStep,
+ currentStep,
+ columns = 2 // Default to 2 columns for backward compatibility
+ } = props;
+
+ const formValues = formControl.getValues();
+ const formEntries = Object.entries(formValues);
+
+ const blacklist = [
+ "selectedOption",
+ "GDAPAuth",
+ "SAMWizard",
+ "GUID",
+ "ID",
+ "noSubmitButton",
+ "RAWJson",
+ "TemplateList",
+ "addrow",
+ ];
+
+ // Filter out null values and undefined values which could be from hidden conditional fields
+ const filteredFormEntries = formEntries.filter(
+ ([_, value]) => value !== null && value !== undefined
+ );
+
+ const tenantEntry = filteredFormEntries.find(
+ ([key]) => key === "tenantFilter" || key === "tenant"
+ );
+ const userEntry = filteredFormEntries.find(([key]) =>
+ ["user", "userPrincipalName", "username"].includes(key)
+ );
+
+ const filteredEntries = formEntries.filter(
+ ([key]) =>
+ !blacklist.includes(key) &&
+ key !== "tenantFilter" &&
+ key !== "tenant" &&
+ !["user", "userPrincipalName", "username"].includes(key) &&
+ !key.startsWith('HIDDEN_')
+ );
+
+ // Calculate total entries including special ones for even distribution
+ const totalEntries = filteredEntries.length + (tenantEntry ? 1 : 0) + (userEntry ? 1 : 0);
+
+ // Dynamically split entries based on columns prop with special entries distributed
+ const splitEntries = () => {
+ const result = Array.from({ length: columns }, () => []);
+
+ // Add special entries to different columns first
+ if (tenantEntry) {
+ result[0].push(tenantEntry);
+ }
+ if (userEntry && result[1]) {
+ result[1].push(userEntry);
+ }
+
+ // Distribute remaining entries across columns to balance them
+ filteredEntries.forEach((entry) => {
+ // Find the column with the fewest entries
+ let targetColumn = 0;
+ let minLength = result[0].length;
+
+ for (let i = 1; i < columns; i++) {
+ if (result[i].length < minLength) {
+ minLength = result[i].length;
+ targetColumn = i;
+ }
+ }
+
+ result[targetColumn].push(entry);
+ });
+
+ return result;
+ };
+
+ const columnEntries = splitEntries();
+
+ // Calculate Grid sizes based on number of columns
+ const getGridSize = () => {
+ const sizes = {
+ 1: { lg: 12, md: 12, xs: 12 },
+ 2: { lg: 6, md: 6, xs: 12 },
+ 3: { lg: 4, md: 6, xs: 12 },
+ 4: { lg: 3, md: 6, xs: 12 },
+ 6: { lg: 2, md: 4, xs: 12 },
+ };
+
+ return sizes[columns] || sizes[2]; // Default to 2 columns
+ };
+
+ const gridSize = getGridSize();
+
+ return (
+
+ {filteredEntries.length === 0 ? (
+
+
+
+ You've completed the steps in this wizard. Hit submit to save your changes.
+
+
+
+ ) : (
+
+
+ {columnEntries.map((columnData, index) => (
+
+
+ {columnData.map(([key, value]) => (
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+export default CippWizardConfirmation;
diff --git a/src/components/CippWizard/CippWizardGroupTemplates.jsx b/src/components/CippWizard/CippWizardGroupTemplates.jsx
index 215d22a0509f..16e1bd61ef0d 100644
--- a/src/components/CippWizard/CippWizardGroupTemplates.jsx
+++ b/src/components/CippWizard/CippWizardGroupTemplates.jsx
@@ -11,26 +11,35 @@ export const CippWizardGroupTemplates = (props) => {
const watcher = useWatch({ control: formControl.control, name: "TemplateList" });
const groupOptions = [
{ label: "Dynamic Group", value: "dynamic" },
- { label: "Dynamic Distribution Group", value: "dynamicdistribution" },
+ { label: "Dynamic Distribution Group", value: "dynamicDistribution" },
{ label: "Security Group", value: "generic" },
{ label: "Distribution Group", value: "distribution" },
- { label: "Azure Role Group", value: "azurerole" },
+ { label: "Azure Role Group", value: "azureRole" },
{ label: "Mail Enabled Security Group", value: "security" },
];
useEffect(() => {
if (watcher?.value) {
+ console.log("Loading template:", watcher);
+
+ // Set groupType first to ensure conditional fields are visible
formControl.setValue("groupType", watcher.addedFields.groupType);
- formControl.setValue("Displayname", watcher.addedFields.Displayname);
- formControl.setValue("Description", watcher.addedFields.Description);
- formControl.setValue("username", watcher.addedFields.username);
- formControl.setValue("allowExternal", watcher.addedFields.allowExternal);
- formControl.setValue("MembershipRules", watcher.addedFields.MembershipRules);
+
+ // Use setTimeout to ensure the DOM updates with the groupType before setting other fields
+ setTimeout(() => {
+ formControl.setValue("displayName", watcher.addedFields.displayName);
+ formControl.setValue("description", watcher.addedFields.description);
+ formControl.setValue("username", watcher.addedFields.username);
+ formControl.setValue("allowExternal", watcher.addedFields.allowExternal);
+ formControl.setValue("membershipRules", watcher.addedFields.membershipRules);
+
+ console.log("Set membershipRules to:", watcher.addedFields.membershipRules);
+ }, 100);
}
}, [watcher]);
return (
-
+ {
excludeTenantFilter: true,
url: "/api/ListGroupTemplates",
queryKey: "ListGroupTemplates",
- labelField: (option) => `${option.Displayname} (${option.groupType})`,
+ labelField: (option) =>
+ `${option.Displayname || option.displayName} (${option.groupType})`,
valueField: "GUID",
addedField: {
groupType: "groupType",
- Displayname: "Displayname",
- Description: "Description",
+ displayName: "displayName",
+ description: "description",
username: "username",
allowExternal: "allowExternal",
- MembershipRules: "MembershipRules",
+ membershipRules: "membershipRules",
},
+ showRefresh: true,
}}
/>
-
+ {
validators={{ required: "Please select a group type" }}
/>
-
+
-
+
-
+ {
formControl={formControl}
/>
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
{
}
}, [selectedUsers]);
+ // Set initial defaults source on component mount if not already set
useEffect(() => {
- if (userSettingsDefaults?.offboardingDefaults) {
- userSettingsDefaults.offboardingDefaults.forEach((setting) => {
- formControl.setValue(setting.name, setting.value);
- });
+ const currentDefaultsSource = formControl.getValues("HIDDEN_defaultsSource");
+ if (!currentDefaultsSource) {
+ // Default to user defaults since form starts with user defaults from initialState within the wizard component
+ formControl.setValue("HIDDEN_defaultsSource", "user");
}
- }, [userSettingsDefaults]);
+ }, [formControl]);
+
+ // Apply defaults only once per tenant or when tenant changes
+ useEffect(() => {
+ const currentTenantId = currentTenant?.value;
+ const appliedDefaultsForTenant = formControl.getValues("HIDDEN_appliedDefaultsForTenant");
+
+ // Only apply defaults if we haven't applied them for this tenant yet
+ if (currentTenantId && appliedDefaultsForTenant !== currentTenantId) {
+ const tenantDefaults = currentTenant?.addedFields?.offboardingDefaults;
+
+ if (tenantDefaults) {
+ // Apply tenant defaults
+ Object.entries(tenantDefaults).forEach(([key, value]) => {
+ formControl.setValue(key, value);
+ });
+ // Set the source indicator
+ formControl.setValue("HIDDEN_defaultsSource", "tenant");
+ } else if (userSettingsDefaults?.offboardingDefaults) {
+ // Apply user defaults if no tenant defaults
+ userSettingsDefaults.offboardingDefaults.forEach((setting) => {
+ formControl.setValue(setting.name, setting.value);
+ });
+ // Set the source indicator
+ formControl.setValue("HIDDEN_defaultsSource", "user");
+ }
+
+ // Mark that we've applied defaults for this tenant
+ formControl.setValue("HIDDEN_appliedDefaultsForTenant", currentTenantId);
+ }
+ }, [currentTenant?.value, userSettingsDefaults, formControl]);
useEffect(() => {
if (disableForwarding) {
formControl.setValue("forward", null);
- formControl.setValue("keepCopy", false);
+ formControl.setValue("KeepCopy", false);
}
}, [disableForwarding, formControl]);
+ const getDefaultsSource = () => {
+ return formControl.getValues("HIDDEN_defaultsSource") || "user";
+ };
+
return (
-
+
+
+ {getDefaultsSource() === "tenant" ? "Using Tenant Defaults" : "Using User Defaults"}
+ {
/>
@@ -123,6 +164,12 @@ export const CippWizardOffboarding = (props) => {
type="switch"
formControl={formControl}
/>
+ {
-
+
@@ -155,6 +202,7 @@ export const CippWizardOffboarding = (props) => {
dataKey: "Results",
labelField: (option) => `${option.displayName} (${option.userPrincipalName})`,
valueField: "id",
+ queryKey: "Offboarding-Users",
data: {
Endpoint: "users",
manualPagination: true,
@@ -179,6 +227,7 @@ export const CippWizardOffboarding = (props) => {
url: "/api/ListGraphRequest",
dataKey: "Results",
tenantFilter: currentTenant ? currentTenant.value : undefined,
+ queryKey: "Offboarding-Users",
data: {
Endpoint: "users",
manualPagination: true,
@@ -203,6 +252,7 @@ export const CippWizardOffboarding = (props) => {
valueField: "id",
url: "/api/ListGraphRequest",
dataKey: "Results",
+ queryKey: "Offboarding-Users",
data: {
Endpoint: "users",
manualPagination: true,
@@ -244,6 +294,7 @@ export const CippWizardOffboarding = (props) => {
valueField: "id",
url: "/api/ListGraphRequest",
dataKey: "Results",
+ queryKey: "Offboarding-Users",
data: {
Endpoint: "users",
manualPagination: true,
@@ -256,7 +307,7 @@ export const CippWizardOffboarding = (props) => {
/>
{
-
+ {
compareType="is"
compareValue={true}
>
-
+ Scheduled Offboarding Date {
/>
-
+ Send results to: {
wizardTitle,
backButton = true,
wizardOrientation = "horizontal",
+ maxWidth = "xl",
...other
} = props;
return (
@@ -26,7 +26,7 @@ const CippWizardPage = (props) => {
py: 4,
}}
>
-
+
{backButton && (
{
const newData = {};
Object.keys(values).forEach((key) => {
const value = values[key];
- if (replacementBehaviour !== "removeNulls") {
+ // Only add non-null values if removeNulls is specified
+ if (replacementBehaviour !== "removeNulls" || value !== null) {
newData[key] = value;
- } else if (row[value] !== undefined) {
- newData[key] = row[value];
}
});
sendForm.mutate({ url: postUrl, data: newData });
diff --git a/src/components/CippWizard/CustomerForm.jsx b/src/components/CippWizard/CustomerForm.jsx
index e441faa0d932..7eedc7a7e456 100644
--- a/src/components/CippWizard/CustomerForm.jsx
+++ b/src/components/CippWizard/CustomerForm.jsx
@@ -1,4 +1,5 @@
-import { Grid } from "@mui/material";
+import "@mui/material";
+import { Grid } from "@mui/system";
import CippFormComponent from "../CippComponents/CippFormComponent";
export const CustomerForm = (props) => {
@@ -69,7 +70,7 @@ export const CustomerForm = (props) => {
return (
{fields.map((field, index) => (
-
+ {
{steps.map((step) => (
- {step.title}
+
+ {`Step ${steps.indexOf(step) ? steps.indexOf(step) + 1 : 1}`}
+
{step.description}
diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js
new file mode 100644
index 000000000000..e7d0cdde65de
--- /dev/null
+++ b/src/components/ExecutiveReportButton.js
@@ -0,0 +1,3059 @@
+import { useState, useMemo } from "react";
+import {
+ Button,
+ Tooltip,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Box,
+ Typography,
+ FormControlLabel,
+ Switch,
+ Grid,
+ Paper,
+ IconButton,
+} from "@mui/material";
+import { PictureAsPdf, Download, Close, Settings } from "@mui/icons-material";
+import {
+ Document,
+ Page,
+ Text,
+ View,
+ StyleSheet,
+ PDFViewer,
+ Image,
+ Svg,
+ Path,
+ Circle,
+ Line,
+ Rect,
+} from "@react-pdf/renderer";
+import { useSettings } from "../hooks/use-settings";
+import { useSecureScore } from "../hooks/use-securescore";
+import { ApiGetCall } from "../api/ApiCall";
+
+// PRODUCTION-GRADE PDF SYSTEM WITH CONDITIONAL RENDERING
+const ExecutiveReportDocument = ({
+ tenantName,
+ userStats,
+ brandingSettings,
+ secureScoreData,
+ licensingData,
+ deviceData,
+ conditionalAccessData,
+ standardsCompareData,
+ driftComplianceData,
+ sectionConfig = {
+ executiveSummary: true,
+ securityStandards: true,
+ driftCompliance: false,
+ secureScore: true,
+ licenseManagement: true,
+ deviceManagement: true,
+ conditionalAccess: true,
+ infographics: true,
+ },
+}) => {
+ const currentDate = new Date().toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+ const brandColor = brandingSettings?.colour || "#F77F00";
+
+ // ENTERPRISE DESIGN SYSTEM - JOBS/RAMS/IVE PRINCIPLES
+ const styles = StyleSheet.create({
+ // FOUNDATION - CONSISTENT STATE OWNERSHIP (FLORENCE)
+ page: {
+ flexDirection: "column",
+ backgroundColor: "#FFFFFF",
+ fontFamily: "Helvetica",
+ fontSize: 10,
+ lineHeight: 1.4,
+ color: "#2D3748",
+ padding: 40, // Consistent base padding
+ },
+
+ // COVER PAGE - PROPORTIONAL & INTENTIONAL (JOBS/RAMS/IVE)
+ coverPage: {
+ flexDirection: "column",
+ backgroundColor: "#FFFFFF",
+ fontFamily: "Helvetica",
+ padding: 60,
+ justifyContent: "space-between",
+ minHeight: "100%",
+ },
+
+ coverHeader: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 80,
+ },
+
+ logoSection: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+
+ logo: {
+ height: 100,
+ marginRight: 12,
+ },
+
+ headerLogo: {
+ height: 30,
+ },
+
+ brandName: {
+ fontSize: 12,
+ fontWeight: "bold",
+ color: brandColor,
+ letterSpacing: 1,
+ textTransform: "uppercase",
+ },
+
+ dateStamp: {
+ fontSize: 9,
+ color: "#000000",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ },
+
+ // MODERN HERO SECTION
+ coverHero: {
+ flex: 1,
+ justifyContent: "flex-start",
+ alignItems: "flex-start",
+ paddingTop: 40,
+ },
+
+ coverLabel: {
+ backgroundColor: brandColor,
+ color: "#FFFFFF",
+ fontSize: 10,
+ fontWeight: "bold",
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 20,
+ marginBottom: 30,
+ alignSelf: "flex-start",
+ },
+
+ mainTitle: {
+ fontSize: 48,
+ fontWeight: "bold",
+ color: "#1A202C",
+ lineHeight: 1.1,
+ marginBottom: 20,
+ letterSpacing: -1,
+ textTransform: "uppercase",
+ },
+
+ titleAccent: {
+ color: brandColor,
+ },
+
+ subtitle: {
+ fontSize: 14,
+ color: "#000000",
+ fontWeight: "normal",
+ lineHeight: 1.5,
+ marginBottom: 40,
+ maxWidth: 400,
+ },
+
+ tenantCard: {
+ backgroundColor: "transparent",
+ padding: 0,
+ maxWidth: 400,
+ },
+
+ tenantName: {
+ fontSize: 18,
+ fontWeight: "bold",
+ color: "#000000",
+ marginBottom: 8,
+ textAlign: "center",
+ },
+
+ tenantMeta: {
+ fontSize: 11,
+ color: "#333333",
+ textAlign: "center",
+ },
+
+ coverFooter: {
+ textAlign: "center",
+ marginTop: 60,
+ },
+
+ confidential: {
+ fontSize: 9,
+ color: "#A0AEC0",
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ },
+
+ // CONTENT PAGES - MODULAR COMPOSITION (FROST)
+ pageHeader: {
+ borderBottom: `1px solid ${brandColor}`,
+ paddingBottom: 12,
+ marginBottom: 24,
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "flex-start",
+ pageBreakAfter: "avoid",
+ breakAfter: "avoid",
+ },
+
+ pageHeaderContent: {
+ flex: 1,
+ },
+
+ pageTitle: {
+ fontSize: 20,
+ fontWeight: "bold",
+ color: "#1A202C",
+ marginBottom: 8,
+ },
+
+ pageSubtitle: {
+ fontSize: 11,
+ color: "#4A5568",
+ fontWeight: "normal",
+ },
+
+ // SECTIONS - REPEATABLE PATTERNS (FROST)
+ section: {
+ marginBottom: 24,
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ },
+
+ sectionTitle: {
+ fontSize: 14,
+ fontWeight: "bold",
+ color: brandColor,
+ marginBottom: 12,
+ pageBreakAfter: "avoid",
+ breakAfter: "avoid",
+ orphans: 3,
+ widows: 3,
+ },
+
+ bodyText: {
+ fontSize: 9,
+ color: "#2D3748",
+ lineHeight: 1.5,
+ marginBottom: 12,
+ textAlign: "justify",
+ },
+
+ // STATS GRID - PERFECT ALIGNMENT (SPOOL)
+ statsGrid: {
+ flexDirection: "row",
+ gap: 12,
+ marginBottom: 20,
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ },
+
+ statCard: {
+ flex: 1,
+ backgroundColor: "#FFFFFF",
+ border: `1px solid #E2E8F0`,
+ borderRadius: 6,
+ padding: 16,
+ alignItems: "center",
+ borderTop: `3px solid ${brandColor}`,
+ },
+
+ statNumber: {
+ fontSize: 16,
+ fontWeight: "bold",
+ color: brandColor,
+ marginBottom: 4,
+ },
+
+ statLabel: {
+ fontSize: 7,
+ color: "#4A5568",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ textAlign: "center",
+ fontWeight: "bold",
+ },
+
+ // COMPLIANCE BARS - VISUAL CONFIDENCE (SPOOL)
+ complianceList: {
+ gap: 8,
+ },
+
+ complianceItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ backgroundColor: "#FFFFFF",
+ padding: 10,
+ borderRadius: 4,
+ border: `1px solid #F0F0F0`,
+ },
+
+ complianceLabel: {
+ fontSize: 8,
+ color: "#2D3748",
+ width: 80,
+ fontWeight: "bold",
+ },
+
+ complianceBarContainer: {
+ flex: 1,
+ height: 6,
+ backgroundColor: "#E2E8F0",
+ marginHorizontal: 10,
+ borderRadius: 3,
+ overflow: "hidden",
+ },
+
+ complianceBar: {
+ height: 6,
+ backgroundColor: brandColor,
+ borderRadius: 3,
+ },
+
+ complianceValue: {
+ fontSize: 8,
+ color: "#2D3748",
+ width: 25,
+ textAlign: "right",
+ fontWeight: "bold",
+ },
+
+ // SECURE SCORE CARDS - ENTERPRISE GRADE
+ scoreGrid: {
+ flexDirection: "row",
+ gap: 12,
+ marginBottom: 20,
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ },
+
+ scoreCard: {
+ flex: 1,
+ backgroundColor: "#FFFFFF",
+ border: `1px solid #E2E8F0`,
+ borderRadius: 6,
+ padding: 16,
+ alignItems: "center",
+ borderTop: `3px solid ${brandColor}`,
+ },
+
+ scoreNumber: {
+ fontSize: 20,
+ fontWeight: "bold",
+ color: brandColor,
+ marginBottom: 8,
+ },
+
+ scoreLabel: {
+ fontSize: 7,
+ color: "#4A5568",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ textAlign: "center",
+ fontWeight: "bold",
+ },
+
+ // CHART AREA - BROWSER CONSTRAINTS (RAUCH)
+ chartContainer: {
+ backgroundColor: "#FFFFFF",
+ border: `1px solid #E2E8F0`,
+ borderRadius: 6,
+ padding: 16,
+ marginBottom: 20,
+ alignItems: "center",
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ },
+
+ chartTitle: {
+ fontSize: 10,
+ fontWeight: "bold",
+ color: "#2D3748",
+ marginBottom: 12,
+ },
+
+ chartData: {
+ fontSize: 9,
+ color: "#4A5568",
+ textAlign: "center",
+ lineHeight: 1.4,
+ },
+
+ // CONTROLS TABLE - HIGH PERFORMANCE (RAUCH)
+ controlsTable: {
+ border: `1px solid #E2E8F0`,
+ borderRadius: 6,
+ overflow: "hidden",
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ },
+
+ tableHeader: {
+ flexDirection: "row",
+ backgroundColor: brandColor,
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ },
+
+ headerCell: {
+ fontSize: 7,
+ fontWeight: "bold",
+ color: "#FFFFFF",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ },
+
+ headerName: {
+ flex: 2,
+ },
+
+ headerDesc: {
+ flex: 1,
+ marginLeft: 12,
+ },
+
+ headerStatus: {
+ width: 60,
+ textAlign: "center",
+ marginLeft: 12,
+ },
+
+ tableRow: {
+ flexDirection: "row",
+ borderBottomWidth: 1,
+ borderBottomColor: "#F7FAFC",
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ alignItems: "center",
+ },
+
+ cellName: {
+ flex: 1,
+ fontSize: 8,
+ fontWeight: "bold",
+ color: "#2D3748",
+ },
+
+ cellDesc: {
+ flex: 1,
+ marginLeft: 12,
+ fontSize: 7,
+ color: "#4A5568",
+ lineHeight: 1.3,
+ },
+
+ cellStatus: {
+ width: 60,
+ marginLeft: 12,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+
+ // STATUS TEXT - SIMPLE APPROACH
+ statusText: {
+ fontSize: 7,
+ fontWeight: "bold",
+ textAlign: "center",
+ textTransform: "uppercase",
+ letterSpacing: 0.3,
+ },
+
+ statusCompliant: {
+ color: "#22543D",
+ },
+
+ statusPartial: {
+ color: "#744210",
+ },
+
+ statusReview: {
+ color: "#742A2A",
+ },
+
+ // INFO BOXES - CONSISTENT PATTERNS (FROST)
+ infoBox: {
+ backgroundColor: "#FFFFFF",
+ border: `1px solid #E2E8F0`,
+ borderLeft: `4px solid ${brandColor}`,
+ borderRadius: 4,
+ padding: 12,
+ marginBottom: 12,
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ orphans: 3,
+ widows: 3,
+ },
+
+ infoTitle: {
+ fontSize: 9,
+ fontWeight: "bold",
+ color: "#2D3748",
+ marginBottom: 6,
+ },
+
+ infoText: {
+ fontSize: 8,
+ color: "#4A5568",
+ lineHeight: 1.4,
+ },
+
+ // RECOMMENDATIONS - SCALABLE SECTIONS (FROST)
+ recommendationsList: {
+ gap: 8,
+ pageBreakInside: "avoid",
+ breakInside: "avoid",
+ },
+
+ recommendationItem: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ },
+
+ recommendationBullet: {
+ fontSize: 8,
+ color: brandColor,
+ marginRight: 6,
+ fontWeight: "bold",
+ marginTop: 1,
+ },
+
+ recommendationText: {
+ fontSize: 8,
+ color: "#2D3748",
+ lineHeight: 1.4,
+ flex: 1,
+ },
+
+ recommendationLabel: {
+ fontWeight: "bold",
+ },
+
+ // FOOTER - DETERMINISTIC PAGINATION (FLORENCE)
+ footer: {
+ position: "absolute",
+ bottom: 20,
+ left: 40,
+ right: 40,
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ borderTop: "1px solid #E2E8F0",
+ paddingTop: 8,
+ },
+
+ footerText: {
+ fontSize: 7,
+ color: "#718096",
+ },
+
+ pageNumber: {
+ fontSize: 7,
+ color: "#718096",
+ fontWeight: "bold",
+ },
+
+ // BLACK STATISTIC PAGES - MODERN DESIGN
+ statPage: {
+ flexDirection: "column",
+ backgroundColor: "#000000",
+ fontFamily: "Helvetica",
+ padding: 0,
+ justifyContent: "center",
+ alignItems: "flex-start",
+ minHeight: "100%",
+ position: "relative",
+ },
+
+ statOverlay: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 60,
+ justifyContent: "center",
+ alignItems: "flex-start",
+ zIndex: 10,
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
+ },
+
+ statMainText: {
+ fontSize: 18,
+ color: "#FFFFFF",
+ fontWeight: "bold",
+ lineHeight: 1.4,
+ marginBottom: 8,
+ },
+
+ statHighlight: {
+ fontSize: 72,
+ color: brandColor,
+ fontWeight: "900",
+ lineHeight: 1,
+ marginBottom: 8,
+ },
+
+ statBackground: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ opacity: 0.5,
+ },
+
+ statSubText: {
+ fontSize: 14,
+ color: "#FFFFFF",
+ fontWeight: "bold",
+ lineHeight: 1.3,
+ marginBottom: 40,
+ },
+
+ statFooterText: {
+ position: "absolute",
+ bottom: 60,
+ right: 60,
+ fontSize: 12,
+ color: "#FFFFFF",
+ fontWeight: "bold",
+ textAlign: "right",
+ lineHeight: 1.3,
+ },
+
+ statBrandFooter: {
+ position: "absolute",
+ bottom: 60,
+ left: 60,
+ fontSize: 8,
+ color: "#666666",
+ textTransform: "uppercase",
+ letterSpacing: 1,
+ },
+
+ // CENTERED IMAGE STYLE
+ centeredImage: {
+ width: 300,
+ height: 200,
+ alignSelf: "center",
+ marginVertical: 20,
+ borderRadius: 8,
+ },
+
+ // SVG CHART STYLES
+ svgChartContainer: {
+ alignItems: "center",
+ marginVertical: 12,
+ },
+
+ svgChart: {
+ width: 400,
+ height: 200,
+ marginBottom: 8,
+ },
+
+ chartSummaryText: {
+ fontSize: 8,
+ fontWeight: "bold",
+ color: brandColor,
+ textAlign: "center",
+ marginTop: 8,
+ },
+ });
+
+ // PROCESS REAL STANDARDS DATA
+ const processStandardsData = (apiData) => {
+ // Try to fetch standards data dynamically
+ let standardsData = null;
+ try {
+ standardsData = require("../data/standards.json");
+ } catch (error) {}
+
+ if (!apiData || !Array.isArray(apiData) || apiData.length === 0) {
+ return [];
+ }
+
+ const processedStandards = [];
+ const tenantData = apiData[0]; // Get the first tenant's data
+
+ // Process each standard from the API response
+ Object.keys(tenantData).forEach((key) => {
+ if (key.startsWith("standards.") && key !== "tenantFilter") {
+ const standardKey = key;
+ const standardValue = tenantData[key];
+ const standardDef = standardsData?.find((std) => std.name === standardKey);
+
+ if (standardDef) {
+ // Determine compliance status
+ let status = "Review";
+ if (standardValue && typeof standardValue === "object" && standardValue.Value === true) {
+ status = "Compliant";
+ } else if (standardValue && standardValue.Value === true) {
+ status = "Compliant";
+ }
+ // Get tags for display - fix the tags access
+ const tags =
+ standardDef.tag && Array.isArray(standardDef.tag) && standardDef.tag.length > 0
+ ? standardDef.tag.slice(0, 2).join(", ") // Show first 2 tags
+ : "No tags";
+ processedStandards.push({
+ name: standardDef.label,
+ description:
+ standardDef.executiveText || standardDef.helpText || "No description available",
+ status: status,
+ tags: tags,
+ });
+ } else {
+ // If no definition found, still add it with basic info
+ let status = "Review";
+ if (standardValue && typeof standardValue === "object" && standardValue.Value === true) {
+ status = "Compliant";
+ } else if (standardValue && standardValue.Value === true) {
+ status = "Compliant";
+ }
+
+ // Create a proper name from the key
+ const displayName = standardKey
+ .replace("standards.", "")
+ .replace(/([A-Z])/g, " $1") // Add space before capital letters
+ .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter
+ .trim();
+
+ processedStandards.push({
+ name: displayName,
+ description: "Security standard implementation",
+ status: status,
+ tags: "No tags",
+ });
+ }
+ }
+ });
+
+ return processedStandards;
+ };
+
+ // PROCESS DRIFT COMPLIANCE DATA
+ const processDriftComplianceData = (driftData, standardsCompareData) => {
+ if (!driftData || !Array.isArray(driftData) || driftData.length === 0) {
+ return {
+ acceptedDeviationsCount: 0,
+ currentDeviationsCount: 0,
+ deniedDeviationsCount: 0,
+ customerSpecificDeviationsCount: 0,
+ alignedCount: 0,
+ acceptedDeviations: [],
+ currentDeviations: [],
+ deniedDeviations: [],
+ customerSpecificDeviations: [],
+ appliedStandards: [],
+ };
+ }
+
+ // Get standards data for pretty names
+ let standardsData = null;
+ try {
+ standardsData = require("../data/standards.json");
+ } catch (error) {}
+
+ // Helper function to get pretty name from standards.json (same as manage-drift)
+ const getStandardPrettyName = (standardName) => {
+ if (!standardName) return "Unknown Standard";
+ const standard = standardsData?.find((s) => s.name === standardName);
+ if (standard && standard.label) {
+ return standard.label;
+ }
+ return null;
+ };
+
+ // Helper function to process deviations with pretty names
+ const processDeviations = (deviations) => {
+ return (deviations || []).map((deviation) => ({
+ ...deviation,
+ prettyName:
+ deviation.standardDisplayName ||
+ getStandardPrettyName(deviation.standardName) ||
+ deviation.standardName ||
+ "Unknown Standard",
+ }));
+ };
+
+ // Aggregate data across all standards for this tenant
+ const aggregatedData = driftData.reduce(
+ (acc, item) => {
+ acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0;
+ acc.currentDeviationsCount += item.currentDeviationsCount || 0;
+ acc.alignedCount += item.alignedCount || 0;
+ acc.customerSpecificDeviationsCount += item.customerSpecificDeviationsCount || 0;
+ acc.deniedDeviationsCount += item.deniedDeviationsCount || 0;
+
+ // Collect deviations with pretty names
+ if (item.currentDeviations && Array.isArray(item.currentDeviations)) {
+ acc.currentDeviations.push(
+ ...processDeviations(item.currentDeviations.filter((dev) => dev !== null))
+ );
+ }
+ if (item.acceptedDeviations && Array.isArray(item.acceptedDeviations)) {
+ acc.acceptedDeviations.push(
+ ...processDeviations(item.acceptedDeviations.filter((dev) => dev !== null))
+ );
+ }
+ if (item.customerSpecificDeviations && Array.isArray(item.customerSpecificDeviations)) {
+ acc.customerSpecificDeviations.push(
+ ...processDeviations(item.customerSpecificDeviations.filter((dev) => dev !== null))
+ );
+ }
+ if (item.deniedDeviations && Array.isArray(item.deniedDeviations)) {
+ acc.deniedDeviations.push(
+ ...processDeviations(item.deniedDeviations.filter((dev) => dev !== null))
+ );
+ }
+
+ return acc;
+ },
+ {
+ acceptedDeviationsCount: 0,
+ currentDeviationsCount: 0,
+ alignedCount: 0,
+ customerSpecificDeviationsCount: 0,
+ deniedDeviationsCount: 0,
+ currentDeviations: [],
+ acceptedDeviations: [],
+ customerSpecificDeviations: [],
+ deniedDeviations: [],
+ appliedStandards: [],
+ }
+ );
+
+ // Get complete list of applied standards from standards comparison data (like policies-deployed)
+ if (
+ standardsData &&
+ standardsCompareData &&
+ Array.isArray(standardsCompareData) &&
+ standardsCompareData.length > 0
+ ) {
+ const tenantData = standardsCompareData[0];
+ const appliedStandards = [];
+
+ // Process each standard from the API response
+ Object.keys(tenantData).forEach((key) => {
+ if (key.startsWith("standards.") && key !== "tenantFilter") {
+ const standardKey = key;
+ const standardDef = standardsData.find((std) => std.name === standardKey);
+
+ if (standardDef) {
+ appliedStandards.push({
+ name: standardDef.label || standardKey,
+ executiveDescription:
+ standardDef.executiveText || standardDef.helpText || "No description available",
+ category: standardDef.cat || "General",
+ });
+ }
+ }
+ });
+
+ aggregatedData.appliedStandards = appliedStandards;
+ }
+
+ return aggregatedData;
+ };
+
+ let securityControls = processStandardsData(standardsCompareData);
+ let driftComplianceInfo = processDriftComplianceData(driftComplianceData, standardsCompareData);
+
+ const getBadgeStyle = (status) => {
+ switch (status) {
+ case "Compliant":
+ return [styles.statusText, styles.statusCompliant];
+ case "Partial":
+ return [styles.statusText, styles.statusPartial];
+ case "Review":
+ case "Review Required":
+ return [styles.statusText, styles.statusReview];
+ default:
+ return styles.statusText;
+ }
+ };
+
+ return (
+
+ {/* COVER PAGE - JOBS/RAMS/IVE PERFECTION */}
+
+
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+ {currentDate}
+
+
+
+ SECURITY ASSESSMENT
+
+
+ Executive{"\n"}
+ Summary
+
+
+
+ Security & Compliance Assessment for {tenantName || "your organization"}
+
+
+
+ {tenantName || "Organization Name"}
+
+
+
+
+ Confidential & Proprietary
+
+
+
+ {/* EXECUTIVE SUMMARY - MODULAR COMPOSITION (FROST) */}
+ {sectionConfig.executiveSummary && (
+
+
+
+ Executive Summary
+
+ Strategic overview of your Microsoft 365 security posture
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ This security assessment for{" "}
+ {tenantName || "your organization"}{" "}
+ provides a clear picture of your organization's cybersecurity posture and readiness
+ against modern threats. We've evaluated your current security measures against
+ industry best practices to identify strengths and opportunities for improvement.
+
+
+
+ Our assessment follows globally recognized security standards to ensure your
+ organization meets regulatory requirements and industry benchmarks. This approach
+ helps protect your business assets, maintain customer trust, and reduce operational
+ risks from cyber threats.
+
+
+
+
+ Environment Overview
+
+
+
+ {userStats?.licensedUsers || "0"}
+ Licensed Users
+
+
+ {userStats?.unlicensedUsers || "0"}
+ Unlicensed Users
+
+
+ {userStats?.guests || "0"}
+ Guest Users
+
+
+ {userStats?.globalAdmins || "0"}
+ Global Admins
+
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ )}
+
+ {/* STATISTIC PAGE 1 - CHAPTER SPLITTER */}
+ {sectionConfig.infographics && (
+
+
+
+ 83%
+
+ of organizations experienced{"\n"}
+ more than one cyberattack
+ {"\n"}
+ in the past year
+
+
+
+ Proactive security prevents{"\n"}
+ repeated attacks
+
+
+ )}
+
+ {/* SECURITY CONTROLS - Only show if standards data is available and enabled and drift compliance is disabled */}
+ {sectionConfig.securityStandards &&
+ !sectionConfig.driftCompliance &&
+ (() => {
+ return securityControls && securityControls.length > 0;
+ })() && (
+
+
+
+ Security Standards Assessment
+
+ Detailed evaluation of implemented security standards
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ Your security standards have been carefully evaluated against industry best
+ practices to protect your business from cyber threats while ensuring smooth daily
+ operations. These standards help maintain business continuity, protect sensitive
+ data, and meet regulatory requirements that are essential for your industry.
+
+
+
+
+ Security Standards Status
+
+
+
+ Standard
+ Description
+ Tags
+
+ Status
+
+
+
+ {securityControls.map((control, index) => (
+
+
+ {control.name.length > 100
+ ? control.name.substring(0, 100) + "..."
+ : control.name}
+
+
+ {control.description}
+
+
+ {control.tags.length > 0 ? control.tags : "No tags"}
+
+
+ {control.status}
+
+
+ ))}
+
+
+
+
+ Key Recommendations
+
+
+
+ β’
+
+ Immediate Actions: Address
+ standards marked as "Review" to enhance security posture
+
+
+
+ β’
+
+ Compliance: Ensure all security
+ standards are properly implemented and maintained
+
+
+
+ β’
+
+ Monitoring: Establish regular
+ review cycles for all security standards
+
+
+
+ β’
+
+ Training: Implement security
+ awareness programs to reduce human risk factors
+
+
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ )}
+
+ {/* DRIFT COMPLIANCE - Only show if drift compliance is enabled and security standards is disabled */}
+ {sectionConfig.driftCompliance &&
+ !sectionConfig.securityStandards &&
+ driftComplianceInfo &&
+ (driftComplianceInfo.currentDeviationsCount > 0 ||
+ driftComplianceInfo.acceptedDeviationsCount > 0 ||
+ driftComplianceInfo.deniedDeviationsCount > 0 ||
+ driftComplianceInfo.customerSpecificDeviationsCount > 0 ||
+ driftComplianceInfo.appliedStandards.length > 0) && (
+ <>
+
+
+
+ Drift Compliance Assessment
+
+ Detailed evaluation of policy drift and compliance deviations
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ Your drift compliance assessment shows how your current security policies compare
+ to your organization's approved standards. This analysis helps identify where
+ configurations have drifted from intended baselines and provides insights into
+ policy compliance across your Microsoft 365 environment.
+
+
+
+ {/* Drift Overview Chart */}
+
+ Drift Compliance Overview
+
+
+ Policy Deviation Distribution
+
+
+ {(() => {
+ const chartData = [
+ driftComplianceInfo.alignedCount,
+ driftComplianceInfo.acceptedDeviationsCount,
+ driftComplianceInfo.customerSpecificDeviationsCount,
+ driftComplianceInfo.currentDeviationsCount,
+ driftComplianceInfo.deniedDeviationsCount,
+ ];
+ const chartLabels = [
+ "Aligned Policies",
+ "Accepted Deviations",
+ "Client Specific Deviations",
+ "Current Deviations",
+ "Denied Deviations",
+ ];
+ const chartColors = ["#10B981", "#3B82F6", "#8B5CF6", "#F59E0B", "#EF4444"];
+
+ const total = chartData.reduce((sum, value) => sum + value, 0);
+ if (total === 0) return null;
+
+ const centerX = 200;
+ const centerY = 100;
+ const outerRadius = 60;
+ const innerRadius = 25; // For donut effect
+
+ let currentAngle = 0;
+
+ return (
+ <>
+ {/* Donut Chart */}
+ {chartData.map((value, index) => {
+ if (value === 0) return null;
+
+ const angle = (value / total) * 360;
+ const startAngle = currentAngle;
+ const endAngle = currentAngle + angle;
+
+ // Outer arc points
+ const outerStartX =
+ centerX + outerRadius * Math.cos((startAngle * Math.PI) / 180);
+ const outerStartY =
+ centerY + outerRadius * Math.sin((startAngle * Math.PI) / 180);
+ const outerEndX =
+ centerX + outerRadius * Math.cos((endAngle * Math.PI) / 180);
+ const outerEndY =
+ centerY + outerRadius * Math.sin((endAngle * Math.PI) / 180);
+
+ // Inner arc points
+ const innerStartX =
+ centerX + innerRadius * Math.cos((startAngle * Math.PI) / 180);
+ const innerStartY =
+ centerY + innerRadius * Math.sin((startAngle * Math.PI) / 180);
+ const innerEndX =
+ centerX + innerRadius * Math.cos((endAngle * Math.PI) / 180);
+ const innerEndY =
+ centerY + innerRadius * Math.sin((endAngle * Math.PI) / 180);
+
+ const largeArcFlag = angle > 180 ? 1 : 0;
+
+ // Create donut path
+ const pathData = [
+ `M ${outerStartX} ${outerStartY}`,
+ `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${outerEndX} ${outerEndY}`,
+ `L ${innerEndX} ${innerEndY}`,
+ `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${innerStartX} ${innerStartY}`,
+ "Z",
+ ].join(" ");
+
+ currentAngle += angle;
+
+ return (
+
+ );
+ })}
+
+ {/* Center text */}
+
+ {total}
+
+
+ Total Policies
+
+
+ {/* Clean Horizontal Legend at Bottom */}
+ {(() => {
+ const visibleItems = chartData
+ .map((value, index) => ({
+ value,
+ index,
+ label: chartLabels[index]
+ .replace(" Deviations", "")
+ .replace(" Policies", ""),
+ color: chartColors[index],
+ }))
+ .filter((item) => item.value > 0);
+
+ return visibleItems.map((item, displayIndex) => {
+ const legendX = 30 + displayIndex * 90;
+ const legendY = 175;
+
+ return (
+
+
+
+ {item.label} ({item.value})
+
+
+ );
+ });
+ })()}
+ >
+ );
+ })()}
+
+
+
+
+
+ {/* Deviation Statistics */}
+
+ Deviation Statistics
+
+
+
+
+ {driftComplianceInfo.acceptedDeviationsCount}
+
+ Accepted Deviations
+
+
+
+ {driftComplianceInfo.customerSpecificDeviationsCount}
+
+ Client Specific
+
+
+
+ {driftComplianceInfo.deniedDeviationsCount}
+
+ Denied Deviations
+
+
+
+ {driftComplianceInfo.currentDeviationsCount}
+
+ Current Deviations
+
+
+
+
+ {/* Chart Legend Explanations */}
+
+ Deviation Types Explained
+
+
+
+ β’
+
+ Aligned: Policies that match
+ the approved template exactly with no deviations
+
+
+
+ β’
+
+ Accepted Deviations: Policy
+ differences that have been reviewed and approved by administrators
+
+
+
+ β’
+
+ Client Specific Deviations:{" "}
+ Policy configurations approved as customer-specific business requirements
+
+
+
+ β’
+
+ Current Deviations: Policy
+ differences that require review and administrative action
+
+
+
+ β’
+
+ Denied Deviations: Policy
+ differences that have been rejected and require remediation
+
+
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+
+ {/* Deviations Detail Page */}
+ {(driftComplianceInfo.currentDeviations.length > 0 ||
+ driftComplianceInfo.acceptedDeviations.length > 0 ||
+ driftComplianceInfo.deniedDeviations.length > 0 ||
+ driftComplianceInfo.customerSpecificDeviations.length > 0) && (
+
+
+
+ Policy Deviations Detail
+
+ Comprehensive list of all policy deviations and their status
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ The following table shows all identified policy deviations, their current
+ status, and executive descriptions of what each deviation means for your
+ organization's security posture and compliance requirements.
+
+
+
+
+ Policy Deviations
+
+
+
+ Policy
+
+ Description
+
+ Status
+
+
+ {/* Current Deviations */}
+ {driftComplianceInfo.currentDeviations.slice(0, 5).map((deviation, index) => {
+ let standardsData = null;
+ try {
+ standardsData = require("../data/standards.json");
+ } catch (error) {}
+
+ const standardDef = standardsData?.find(
+ (std) => std.name === deviation.standardName
+ );
+ const description =
+ standardDef?.executiveText ||
+ standardDef?.helpText ||
+ "Policy deviation detected";
+
+ return (
+
+
+ {deviation.prettyName || "Unknown Policy"}
+
+
+ {description}
+
+
+ Current
+
+
+ );
+ })}
+
+ {/* Accepted Deviations */}
+ {driftComplianceInfo.acceptedDeviations.slice(0, 3).map((deviation, index) => {
+ let standardsData = null;
+ try {
+ standardsData = require("../data/standards.json");
+ } catch (error) {}
+
+ const standardDef = standardsData?.find(
+ (std) => std.name === deviation.standardName
+ );
+ const description =
+ standardDef?.executiveText ||
+ standardDef?.helpText ||
+ "Accepted policy deviation";
+
+ return (
+
+
+ {deviation.prettyName || "Unknown Policy"}
+
+
+ {description}
+
+
+
+ Accepted
+
+
+
+ );
+ })}
+
+ {/* Customer Specific Deviations */}
+ {driftComplianceInfo.customerSpecificDeviations
+ .slice(0, 3)
+ .map((deviation, index) => {
+ let standardsData = null;
+ try {
+ standardsData = require("../data/standards.json");
+ } catch (error) {}
+
+ const standardDef = standardsData?.find(
+ (std) => std.name === deviation.standardName
+ );
+ const description =
+ standardDef?.executiveText ||
+ standardDef?.helpText ||
+ "Customer-specific policy configuration";
+
+ return (
+
+
+ {deviation.prettyName || "Unknown Policy"}
+
+
+ {description}
+
+
+
+ Client Specific
+
+
+
+ );
+ })}
+
+ {/* Denied Deviations */}
+ {driftComplianceInfo.deniedDeviations.slice(0, 2).map((deviation, index) => {
+ let standardsData = null;
+ try {
+ standardsData = require("../data/standards.json");
+ } catch (error) {}
+
+ const standardDef = standardsData?.find(
+ (std) => std.name === deviation.standardName
+ );
+ const description =
+ standardDef?.executiveText ||
+ standardDef?.helpText ||
+ "Denied policy deviation";
+
+ return (
+
+
+ {deviation.prettyName || "Unknown Policy"}
+
+
+ {description}
+
+
+ Denied
+
+
+ );
+ })}
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ )}
+
+ {/* Applied Standards Page */}
+ {driftComplianceInfo.appliedStandards.length > 0 && (
+
+
+
+ Applied Standards
+
+ Security standards currently implemented in your environment
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ These are the security standards that have been applied to your Microsoft 365
+ environment. Each standard represents a specific security control or policy
+ designed to protect your organization's data and systems.
+
+
+
+ {/* Group standards by category */}
+ {(() => {
+ const groupedStandards = driftComplianceInfo.appliedStandards.reduce(
+ (acc, standard) => {
+ const category = standard.category || "General";
+ if (!acc[category]) acc[category] = [];
+ acc[category].push(standard);
+ return acc;
+ },
+ {}
+ );
+
+ return Object.entries(groupedStandards).map(([category, standards]) => (
+
+ {category}
+
+ {standards.map((standard, index) => (
+
+ β’
+
+ {standard.name}:{" "}
+ {standard.executiveDescription}
+
+
+ ))}
+
+
+ ));
+ })()}
+
+
+ Compliance Summary
+
+
+ Overall Compliance Status
+
+ Your organization has {driftComplianceInfo.appliedStandards.length} security
+ standards implemented with {driftComplianceInfo.alignedCount} policies fully
+ aligned,{" "}
+ {driftComplianceInfo.acceptedDeviationsCount +
+ driftComplianceInfo.customerSpecificDeviationsCount}{" "}
+ approved deviations, and {driftComplianceInfo.currentDeviationsCount}{" "}
+ deviations requiring attention.
+
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ )}
+ >
+ )}
+
+ {/* STATISTIC PAGE 2 - CHAPTER SPLITTER - Only show if secure score data is available and enabled */}
+ {sectionConfig.infographics &&
+ sectionConfig.secureScore &&
+ secureScoreData &&
+ secureScoreData?.isSuccess &&
+ secureScoreData?.translatedData && (
+
+
+
+ 95%
+
+ of successful cyber attacks{"\n"}
+ could have been prevented with{"\n"}
+ proactive security measures
+
+
+
+ Your security resilience is{"\n"}
+ our primary mission
+
+
+ )}
+
+ {/* MICROSOFT SECURE SCORE - DEDICATED PAGE - Only show if secure score data is available and enabled */}
+ {sectionConfig.secureScore &&
+ secureScoreData &&
+ secureScoreData?.isSuccess &&
+ secureScoreData?.translatedData && (
+
+
+
+ Microsoft Secure Score
+
+ Comprehensive security posture measurement and benchmarking
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ Microsoft Secure Score measures how well your organization is protected against
+ cyber threats. This score reflects the effectiveness of your current security
+ measures and helps identify areas where additional protection could strengthen your
+ business resilience.
+
+
+
+
+ Score Comparison
+
+
+
+
+ {secureScoreData?.translatedData?.currentScore || "N/A"}
+
+ Current Score
+
+
+
+ {secureScoreData?.translatedData?.maxScore || "N/A"}
+
+ Max Score
+
+
+
+ {secureScoreData?.translatedData?.percentageVsSimilar || "N/A"}%
+
+ vs Similar Orgs
+
+
+
+ {secureScoreData?.translatedData?.percentageVsAllTenants || "N/A"}%
+
+ vs All Orgs
+
+
+
+
+
+ 7-Day Score Trend
+
+
+ Secure Score Progress
+ {secureScoreData?.secureScore?.data?.Results &&
+ secureScoreData.secureScore.data.Results.length > 0 ? (
+
+
+ {/* Chart Background */}
+
+
+ {/* Chart Grid Lines */}
+ {[0, 1, 2, 3, 4].map((i) => (
+
+ ))}
+
+ {/* Chart Data Points and Area */}
+ {(() => {
+ const data = secureScoreData.secureScore.data.Results.slice().reverse();
+ const maxScore = secureScoreData?.translatedData?.maxScore || 100;
+ const minScore = 0; // Always start from 0
+ const scoreRange = maxScore; // Full range from 0 to max
+ const chartWidth = 320;
+ const chartHeight = 140;
+ const pointSpacing = chartWidth / Math.max(data.length - 1, 1);
+
+ // Generate path for area chart
+ let pathData = `M 40 ${
+ 160 - (data[0].currentScore / scoreRange) * chartHeight
+ }`;
+ data.forEach((point, index) => {
+ if (index > 0) {
+ const x = 40 + index * pointSpacing;
+ const y = 160 - (point.currentScore / scoreRange) * chartHeight;
+ pathData += ` L ${x} ${y}`;
+ }
+ });
+ pathData += ` L ${40 + (data.length - 1) * pointSpacing} 160 L 40 160 Z`;
+
+ // Generate line path (without area fill)
+ let lineData = `M 40 ${
+ 160 - (data[0].currentScore / scoreRange) * chartHeight
+ }`;
+ data.forEach((point, index) => {
+ if (index > 0) {
+ const x = 40 + index * pointSpacing;
+ const y = 160 - (point.currentScore / scoreRange) * chartHeight;
+ lineData += ` L ${x} ${y}`;
+ }
+ });
+
+ return (
+ <>
+ {/* Area Fill */}
+
+
+ {/* Line */}
+
+
+ {/* Data Points */}
+ {data.map((point, index) => {
+ const x = 40 + index * pointSpacing;
+ const y = 160 - (point.currentScore / scoreRange) * chartHeight;
+ return ;
+ })}
+
+ {/* X-axis Labels */}
+ {data.map((point, index) => {
+ const x = 40 + index * pointSpacing;
+ const date = new Date(point.createdDateTime);
+ const label = date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+ return (
+
+ {label}
+
+ );
+ })}
+
+ {/* Y-axis Labels */}
+ {[
+ 0,
+ Math.round(maxScore * 0.25),
+ Math.round(maxScore * 0.5),
+ Math.round(maxScore * 0.75),
+ maxScore,
+ ].map((score, index) => (
+
+ {score}
+
+ ))}
+ >
+ );
+ })()}
+
+
+
+ Current: {secureScoreData?.translatedData?.currentScore || "N/A"} /{" "}
+ {secureScoreData?.translatedData?.maxScore || "N/A"}(
+ {secureScoreData?.translatedData?.percentageCurrent || "N/A"}%)
+
+
+ ) : (
+
+ Current Score: {secureScoreData?.translatedData?.currentScore || "N/A"} /{" "}
+ {secureScoreData?.translatedData?.maxScore || "N/A"}
+ {"\n"}
+ Achievement Rate: {secureScoreData?.translatedData?.percentageCurrent || "N/A"}%
+ {"\n"}
+ Historical data not available
+
+ )}
+
+
+
+
+ What Your Score Means
+
+ Your current score of {secureScoreData?.translatedData?.currentScore || "N/A"}{" "}
+ represents {secureScoreData?.translatedData?.percentageCurrent || "N/A"}% of the
+ maximum protection level available. This indicates how well your organization is
+ currently defended against common cyber threats and data breaches.
+
+
+
+
+ Why Scores Change
+
+ β’ Business growth and new employees may temporarily lower scores until security
+ measures are applied{"\n"}β’ Changes in software licenses can affect available
+ security features{"\n"}β’ New security threats require updated protections, which may
+ impact scores{"\n"}β’ Regular security improvements help maintain and increase your
+ protection level
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ )}
+
+ {/* LICENSING PAGE - Only show if license data is available */}
+ {sectionConfig.licenseManagement &&
+ licensingData &&
+ Array.isArray(licensingData) &&
+ licensingData.length > 0 && (
+ <>
+ {/* STATISTIC PAGE 3 - CHAPTER SPLITTER */}
+ {sectionConfig.infographics && (
+
+
+
+ Every
+ 39
+ seconds
+
+ a business falls victim to{"\n"}
+ ransomware attacks
+
+
+
+ Proactive defense beats{"\n"}
+ reactive recovery
+
+
+ )}
+
+
+
+ License Management
+
+ Microsoft 365 license allocation and utilization analysis
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ Smart license management helps control costs while ensuring your team has the
+ tools they need to be productive. This analysis shows how your current licenses
+ are being used and identifies opportunities to optimize spending without
+ compromising business operations.
+
+
+
+
+ License Allocation Summary
+
+
+
+ License Type
+
+ Used
+
+
+ Available
+
+
+ Total
+
+
+
+ {licensingData.map((license, index) => (
+
+
+ {(() => {
+ const licenseValue = license.License || license.license || "N/A";
+ if (typeof licenseValue === "object") {
+ }
+ return licenseValue;
+ })()}
+
+
+ {(() => {
+ const countUsed = license.CountUsed || license.countUsed || "0";
+ if (typeof countUsed === "object") {
+ console.log(
+ "DEBUG: license.CountUsed is an object:",
+ countUsed,
+ "full license:",
+ license
+ );
+ }
+ return countUsed;
+ })()}
+
+
+ {(() => {
+ const countAvailable =
+ license.CountAvailable || license.countAvailable || "0";
+ if (typeof countAvailable === "object") {
+ }
+ return countAvailable;
+ })()}
+
+
+ {(() => {
+ const totalLicenses =
+ license.TotalLicenses || license.totalLicenses || "0";
+ if (typeof totalLicenses === "object") {
+ }
+ return totalLicenses;
+ })()}
+
+
+ ))}
+
+
+
+
+ License Optimization Recommendations
+
+
+
+ β’
+
+ Usage Monitoring: Track how
+ licenses are being used to identify cost-saving opportunities
+
+
+
+ β’
+
+ Cost Control: Review unused
+ licenses to reduce unnecessary spending
+
+
+
+ β’
+
+ Growth Planning: Ensure you
+ have enough licenses for business expansion without overspending
+
+
+
+ β’
+
+ Regular Reviews: Conduct
+ quarterly reviews to maintain cost-effective license allocation
+
+
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ >
+ )}
+
+ {/* DEVICES PAGE - Only show if device data is available */}
+ {sectionConfig.deviceManagement &&
+ deviceData &&
+ Array.isArray(deviceData) &&
+ deviceData.length > 0 && (
+ <>
+ {/* STATISTIC PAGE 4 - CHAPTER SPLITTER */}
+ {sectionConfig.infographics && (
+
+
+
+ $4.45M
+
+ average cost of a{"\n"}
+ data breach in 2024
+
+
+
+ Investment in security
+ {"\n"}
+ saves millions in recovery
+
+
+ )}
+
+
+
+ Device Management
+
+ Device compliance status and management overview
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ Managing employee devices is essential for protecting your business data and
+ maintaining productivity. This analysis shows which devices meet your security
+ standards and identifies any that may need attention to prevent data breaches or
+ operational disruptions.
+
+
+
+
+ Device Compliance Overview
+
+
+
+ {deviceData.length}
+ Total Devices
+
+
+
+ {
+ deviceData.filter(
+ (device) =>
+ device.complianceState === "compliant" ||
+ device.ComplianceState === "compliant"
+ ).length
+ }
+
+ Compliant
+
+
+
+ {
+ deviceData.filter(
+ (device) =>
+ device.complianceState !== "compliant" &&
+ device.ComplianceState !== "compliant"
+ ).length
+ }
+
+ Non-Compliant
+
+
+
+ {Math.round(
+ (deviceData.filter(
+ (device) =>
+ device.complianceState === "Compliant" ||
+ device.ComplianceState === "Compliant"
+ ).length /
+ deviceData.length) *
+ 100
+ )}
+ %
+
+ Compliance Rate
+
+
+
+
+
+ Device Management Summary
+
+
+
+ Device Name
+ OS
+ Compliance
+ Last Sync
+
+
+ {deviceData.slice(0, 8).map((device, index) => {
+ const lastSync = device.lastSyncDateTime
+ ? new Date(device.lastSyncDateTime).toLocaleDateString()
+ : "N/A";
+ return (
+
+
+ {(() => {
+ const deviceName = device.deviceName || "N/A";
+ if (typeof deviceName === "object") {
+ }
+ return deviceName;
+ })()}
+
+
+ {(() => {
+ const operatingSystem = device.operatingSystem || "N/A";
+ if (typeof operatingSystem === "object") {
+ }
+ return operatingSystem;
+ })()}
+
+
+
+ {(() => {
+ const complianceState = device.complianceState || "Unknown";
+ if (typeof complianceState === "object") {
+ }
+ return complianceState;
+ })()}
+
+
+ {lastSync}
+
+ );
+ })}
+
+
+
+
+ Device Insights
+
+
+
+
+ {deviceData.filter((device) => device.operatingSystem === "Windows").length}
+
+ Windows Devices
+
+
+
+ {deviceData.filter((device) => device.operatingSystem === "iOS").length}
+
+ iOS Devices
+
+
+
+ {deviceData.filter((device) => device.operatingSystem === "Android").length}
+
+ Android Devices
+
+
+
+ {deviceData.filter((device) => device.isEncrypted === true).length}
+
+ Encrypted
+
+
+
+
+
+ Device Management Recommendations
+
+ Keep devices updated and secure to protect business data. Regularly check that all
+ employee devices meet security standards and address any issues promptly. Consider
+ automated policies to maintain consistent security across all devices and conduct
+ regular reviews to identify potential risks.
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ >
+ )}
+
+ {/* CONDITIONAL ACCESS POLICIES PAGE - Only show if data is available */}
+ {sectionConfig.conditionalAccess &&
+ conditionalAccessData &&
+ Array.isArray(conditionalAccessData) &&
+ conditionalAccessData.length > 0 && (
+ <>
+ {/* STATISTIC PAGE 5 - CHAPTER SPLITTER */}
+ {sectionConfig.infographics && (
+
+
+
+ 277
+ days
+
+ average time to identify and{"\n"}
+ contain a data breach
+
+
+
+ Early detection minimizes{"\n"}
+ business impact
+
+
+ )}
+
+
+
+ Conditional Access Policies
+
+ Identity and access management security controls
+
+
+ {brandingSettings?.logo && (
+
+ )}
+
+
+
+
+ Access control policies help protect your business by ensuring only the right
+ people can access sensitive information under appropriate circumstances. These
+ smart security measures automatically evaluate each access request and apply
+ additional verification when needed, balancing security with employee
+ productivity.
+
+
+
+
+ How Access Controls Protect Your Business
+
+ These policies work like intelligent security guards, making decisions based on
+ who is trying to access what, from where, and when. For example, accessing email
+ from the office might be seamless, but accessing it from an unusual location might
+ require additional verification. This approach protects your data while minimizing
+ disruption to daily work.
+
+
+
+
+ Current Policy Configuration
+
+
+
+ Policy Name
+ State
+ Applications
+ Controls
+
+
+ {conditionalAccessData.slice(0, 8).map((policy, index) => {
+ const getStateStyle = (state) => {
+ switch (state) {
+ case "enabled":
+ return styles.statusCompliant;
+ case "enabledForReportingButNotEnforced":
+ return styles.statusPartial;
+ case "disabled":
+ return styles.statusReview;
+ default:
+ return styles.statusText;
+ }
+ };
+
+ const getStateDisplay = (state) => {
+ switch (state) {
+ case "enabled":
+ return "Enabled";
+ case "enabledForReportingButNotEnforced":
+ return "Report Only";
+ case "disabled":
+ return "Disabled";
+ default:
+ return state || "Unknown";
+ }
+ };
+
+ const getControlsText = (policy) => {
+ const controls = [];
+ if (policy.builtInControls) {
+ if (policy.builtInControls.includes("mfa")) controls.push("MFA");
+ if (policy.builtInControls.includes("block")) controls.push("Block");
+ if (policy.builtInControls.includes("compliantDevice"))
+ controls.push("Compliant Device");
+ }
+ return controls.length > 0 ? controls.join(", ") : "Custom";
+ };
+
+ return (
+
+
+ {(() => {
+ const displayName = policy.displayName || "N/A";
+ if (typeof displayName === "object") {
+ }
+ return displayName;
+ })()}
+
+
+
+ {getStateDisplay(policy.state)}
+
+
+
+ {(() => {
+ const includeApplications = policy.includeApplications || "All";
+ if (typeof includeApplications === "object") {
+ }
+ return includeApplications;
+ })()}
+
+
+ {getControlsText(policy)}
+
+
+ );
+ })}
+
+
+
+
+ Policy Overview
+
+
+
+ {conditionalAccessData.length}
+ Total Policies
+
+
+
+ {conditionalAccessData.filter((policy) => policy.state === "enabled").length}
+
+ Enabled
+
+
+
+ {
+ conditionalAccessData.filter(
+ (policy) => policy.state === "enabledForReportingButNotEnforced"
+ ).length
+ }
+
+ Report Only
+
+
+
+ {
+ conditionalAccessData.filter(
+ (policy) =>
+ policy.builtInControls && policy.builtInControls.includes("mfa")
+ ).length
+ }
+
+ MFA Policies
+
+
+
+
+
+ Policy Analysis
+
+
+
+ β’
+
+ Policy Coverage:{" "}
+ {conditionalAccessData.length} conditional access policies configured
+
+
+
+ β’
+
+ Enforcement Status:{" "}
+ {conditionalAccessData.filter((policy) => policy.state === "enabled").length}{" "}
+ policies actively enforced
+
+
+
+ β’
+
+ Testing Phase:{" "}
+ {
+ conditionalAccessData.filter(
+ (policy) => policy.state === "enabledForReportingButNotEnforced"
+ ).length
+ }{" "}
+ policies in report-only mode
+
+
+
+ β’
+
+ Security Controls:{" "}
+ Multi-factor authentication and access blocking implemented
+
+
+
+
+
+
+ Access Control Recommendations
+
+ {conditionalAccessData.filter(
+ (policy) => policy.state === "enabledForReportingButNotEnforced"
+ ).length > 0
+ ? `Consider activating ${
+ conditionalAccessData.filter(
+ (policy) => policy.state === "enabledForReportingButNotEnforced"
+ ).length
+ } policies currently in testing mode after ensuring they don't disrupt business operations. `
+ : "Your access controls are properly configured. "}
+ Regularly review how these policies affect employee productivity and adjust as
+ needed. Consider additional location-based protections for enhanced security
+ without impacting daily operations.
+
+
+
+
+ `Page ${pageNumber} of ${totalPages}`}
+ />
+
+
+ >
+ )}
+
+ );
+};
+
+export const ExecutiveReportButton = (props) => {
+ const { tenantName, tenantId, userStats, standardsData, organizationData, ...other } = props;
+ const settings = useSettings();
+ const brandingSettings = settings.customBranding;
+
+ // Preview state
+ const [previewOpen, setPreviewOpen] = useState(false);
+ const [sectionConfig, setSectionConfig] = useState({
+ executiveSummary: true,
+ securityStandards: true,
+ driftCompliance: false,
+ secureScore: true,
+ licenseManagement: true,
+ deviceManagement: true,
+ conditionalAccess: true,
+ infographics: true,
+ });
+
+ // Only fetch additional data when preview dialog is opened
+ const secureScore = useSecureScore({ waiting: previewOpen });
+
+ // Get real license data - only when preview is open
+ const licenseData = ApiGetCall({
+ url: "/api/ListLicenses",
+ data: {
+ tenantFilter: settings.currentTenant,
+ },
+ queryKey: `licenses-report-${settings.currentTenant}`,
+ waiting: previewOpen,
+ });
+
+ // Get real device data - only when preview is open
+ const deviceData = ApiGetCall({
+ url: "/api/ListDevices",
+ data: {
+ tenantFilter: settings.currentTenant,
+ },
+ queryKey: `devices-report-${settings.currentTenant}`,
+ waiting: previewOpen,
+ });
+
+ // Get real conditional access policy data - only when preview is open
+ const conditionalAccessData = ApiGetCall({
+ url: "/api/ListConditionalAccessPolicies",
+ data: {
+ tenantFilter: settings.currentTenant,
+ },
+ queryKey: `ca-policies-report-${settings.currentTenant}`,
+ waiting: previewOpen,
+ });
+
+ // Get real standards data - only when preview is open
+ const standardsCompareData = ApiGetCall({
+ url: "/api/ListStandardsCompare",
+ data: {
+ tenantFilter: settings.currentTenant,
+ },
+ queryKey: `standards-compare-report-${settings.currentTenant}`,
+ waiting: previewOpen,
+ });
+
+ // Get drift compliance data - only when preview is open
+ const driftComplianceData = ApiGetCall({
+ url: "/api/listTenantDrift",
+ data: {
+ TenantFilter: settings.currentTenant,
+ },
+ queryKey: `drift-compliance-report-${settings.currentTenant}`,
+ waiting: previewOpen,
+ });
+
+ // Check if all data is loaded (either successful or failed) - only relevant when preview is open
+ const isDataLoading =
+ previewOpen &&
+ (secureScore.isFetching ||
+ licenseData.isFetching ||
+ deviceData.isFetching ||
+ conditionalAccessData.isFetching ||
+ standardsCompareData.isFetching ||
+ driftComplianceData.isFetching);
+
+ const hasAllDataFinished =
+ !previewOpen ||
+ ((secureScore.isSuccess || secureScore.isError) &&
+ (licenseData.isSuccess || licenseData.isError) &&
+ (deviceData.isSuccess || deviceData.isError) &&
+ (conditionalAccessData.isSuccess || conditionalAccessData.isError) &&
+ (standardsCompareData.isSuccess || standardsCompareData.isError) &&
+ (driftComplianceData.isSuccess || driftComplianceData.isError));
+
+ // Button is always available now since we don't need to wait for data
+ const shouldShowButton = true;
+
+ const fileName = `Executive_Report_${tenantName?.replace(/[^a-zA-Z0-9]/g, "_") || "Tenant"}_${
+ new Date().toISOString().split("T")[0]
+ }.pdf`;
+
+ // Memoize the document to prevent unnecessary re-renders - only when dialog is open
+ const reportDocument = useMemo(() => {
+ // Don't create document if dialog is closed
+ if (!previewOpen) {
+ return null;
+ }
+
+ // Only create document if preview is open and data is ready
+ if (!hasAllDataFinished) {
+ return (
+
+
+
+ Loading report data...
+
+
+
+ );
+ }
+
+ try {
+ return (
+
+ );
+ } catch (error) {
+ console.error("Error creating ExecutiveReportDocument:", error);
+ return (
+
+
+
+ Error creating document: {error.message}
+
+
+
+ );
+ }
+ }, [
+ previewOpen, // Most important - prevents creation when dialog is closed
+ hasAllDataFinished,
+ tenantName,
+ tenantId,
+ userStats,
+ standardsData,
+ organizationData,
+ brandingSettings,
+ secureScore?.isSuccess,
+ licenseData?.isSuccess,
+ deviceData?.isSuccess,
+ conditionalAccessData?.isSuccess,
+ standardsCompareData?.isSuccess,
+ driftComplianceData?.isSuccess,
+ JSON.stringify(sectionConfig), // Stringify to prevent reference issues
+ ]);
+
+ // Handle section toggle with mutual exclusion logic
+ const handleSectionToggle = (sectionKey) => {
+ setSectionConfig((prev) => {
+ // Count currently enabled sections
+ const enabledSections = Object.values(prev).filter(Boolean).length;
+
+ // If trying to disable the last remaining section, prevent it
+ if (prev[sectionKey] && enabledSections === 1) {
+ return prev; // Don't change state
+ }
+
+ // Mutual exclusion logic for Security Standards and Drift Compliance
+ if (sectionKey === "securityStandards" && !prev[sectionKey]) {
+ // Enabling Security Standards, disable Drift Compliance
+ return {
+ ...prev,
+ securityStandards: true,
+ driftCompliance: false,
+ };
+ }
+
+ if (sectionKey === "driftCompliance" && !prev[sectionKey]) {
+ // Enabling Drift Compliance, disable Security Standards
+ return {
+ ...prev,
+ driftCompliance: true,
+ securityStandards: false,
+ };
+ }
+
+ return {
+ ...prev,
+ [sectionKey]: !prev[sectionKey],
+ };
+ });
+ };
+
+ // Close handler with cleanup
+ const handleClose = () => {
+ setPreviewOpen(false);
+ };
+
+ // Section configuration options
+ const sectionOptions = [
+ {
+ key: "executiveSummary",
+ label: "Executive Summary",
+ description: "High-level overview and statistics",
+ },
+ {
+ key: "securityStandards",
+ label: "Security Standards",
+ description: "Compliance assessment and standards evaluation",
+ },
+ {
+ key: "driftCompliance",
+ label: "Drift Compliance",
+ description: "Policy drift analysis and deviation management",
+ },
+ {
+ key: "secureScore",
+ label: "Microsoft Secure Score",
+ description: "Security posture measurement and trends",
+ },
+ {
+ key: "licenseManagement",
+ label: "License Management",
+ description: "License allocation and optimization",
+ },
+ {
+ key: "deviceManagement",
+ label: "Device Management",
+ description: "Device compliance and insights",
+ },
+ {
+ key: "conditionalAccess",
+ label: "Conditional Access",
+ description: "Access control policies and analysis",
+ },
+ {
+ key: "infographics",
+ label: "Infographic Pages",
+ description: "Statistical pages with visual elements between sections",
+ },
+ ];
+
+ return (
+ <>
+ {/* Main Executive Summary Button - Always available */}
+
+ }
+ onClick={() => setPreviewOpen(true)}
+ sx={{
+ fontWeight: "bold",
+ textTransform: "none",
+ borderRadius: 2,
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
+ transition: "all 0.2s ease-in-out",
+ }}
+ {...other}
+ >
+ Executive Summary
+
+
+
+ {/* Combined Preview and Configuration Dialog */}
+
+
+
+ Executive Report - {tenantName}
+
+
+
+
+
+
+
+ {/* Left Panel - Section Configuration */}
+
+
+
+
+ Report Sections
+
+
+ Configure which sections to include in your executive report. Changes are reflected
+ in real-time.
+
+
+
+ {sectionOptions.map((option) => (
+
+
+ {
+ event.stopPropagation();
+ handleSectionToggle(option.key);
+ }}
+ color="primary"
+ size="small"
+ disabled={
+ // Disable if this is the last enabled section
+ sectionConfig[option.key] &&
+ Object.values(sectionConfig).filter(Boolean).length === 1
+ }
+ />
+ }
+ label={
+ handleSectionToggle(option.key)}>
+
+ {option.label}
+
+
+ {option.description}
+
+
+ }
+ sx={{ margin: 0, width: "100%" }}
+ />
+
+
+ ))}
+
+
+
+
+ π‘ Pro Tip
+
+
+ Enable only the sections relevant to your audience to create focused, impactful
+ reports. At least one section must be enabled.
+
+
+
+
+
+ {/* Right Panel - PDF Preview */}
+
+ {isDataLoading ? (
+
+ Loading Report Data...
+
+ Fetching additional data for comprehensive report generation
+
+
+ ) : reportDocument ? (
+
+ {reportDocument}
+
+ ) : (
+
+
+ Report preview will appear here
+
+
+ )}
+
+
+
+
+
+
+ Sections enabled: {Object.values(sectionConfig).filter(Boolean).length} of{" "}
+ {sectionOptions.length}
+
+
+
+ }
+ disabled={isDataLoading}
+ sx={{ minWidth: 140 }}
+ onClick={() => {
+ // Create document dynamically when download is clicked
+ const downloadDocument = (
+
+ );
+
+ // Use react-pdf's pdf() function to generate and download
+ import("@react-pdf/renderer").then(({ pdf }) => {
+ pdf(downloadDocument)
+ .toBlob()
+ .then((blob) => {
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ })
+ .catch((error) => {
+ console.error("Error generating PDF:", error);
+ });
+ });
+ }}
+ >
+ {isDataLoading ? "Loading..." : "Download PDF"}
+
+
+
+ Close
+
+
+
+ >
+ );
+};
diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js
index 011886bc4499..5b067cf4e7c3 100644
--- a/src/components/PrivateRoute.js
+++ b/src/components/PrivateRoute.js
@@ -1,39 +1,82 @@
import { ApiGetCall } from "../api/ApiCall.jsx";
import UnauthenticatedPage from "../pages/unauthenticated.js";
+import LoadingPage from "../pages/loading.js";
+import ApiOfflinePage from "../pages/api-offline.js";
export const PrivateRoute = ({ children, routeType }) => {
- const {
- data: profile,
- error,
- isLoading,
- } = ApiGetCall({
+ const session = ApiGetCall({
url: "/.auth/me",
- queryKey: "authmecipp",
+ queryKey: "authmeswa",
refetchOnWindowFocus: true,
staleTime: 120000, // 2 minutes
});
- if (isLoading) {
- return "Loading...";
+ const apiRoles = ApiGetCall({
+ url: "/api/me",
+ queryKey: "authmecipp",
+ retry: 2, // Reduced retry count to show offline message sooner
+ waiting: !session.isSuccess || session.data?.clientPrincipal === null,
+ });
+
+ // Check if the session is still loading before determining authentication status
+ if (
+ session.isLoading ||
+ apiRoles.isLoading ||
+ (apiRoles.isFetching && (apiRoles.data === null || apiRoles.data === undefined))
+ ) {
+ return ;
+ }
+
+ // Check if the API is offline (404 error from /api/me endpoint)
+ // Or other network errors that would indicate API is unavailable
+ if (
+ apiRoles?.error?.response?.status === 404 || // API endpoint not found
+ apiRoles?.error?.response?.status === 502 || // Bad Gateway
+ apiRoles?.error?.response?.status === 503 || // Service Unavailable
+ (apiRoles?.isSuccess && !apiRoles?.data) // No client principal data, indicating API might be offline
+ ) {
+ return ;
+ }
+
+ // if not logged into swa
+ if (null === session?.data?.clientPrincipal || session?.data === undefined) {
+ return ;
}
let roles = null;
- if (null !== profile?.clientPrincipal) {
- roles = profile?.clientPrincipal.userRoles;
- } else if (null === profile?.clientPrincipal) {
+
+ if (
+ session?.isSuccess &&
+ apiRoles?.isSuccess &&
+ undefined !== apiRoles?.data?.clientPrincipal &&
+ session?.data?.clientPrincipal?.userDetails &&
+ apiRoles?.data?.clientPrincipal?.userDetails &&
+ session?.data?.clientPrincipal?.userDetails !== apiRoles?.data?.clientPrincipal?.userDetails
+ ) {
+ // refetch the profile if the user details are different
+ apiRoles.refetch();
+ }
+
+ if (null !== apiRoles?.data?.clientPrincipal && undefined !== apiRoles?.data) {
+ roles = apiRoles?.data?.clientPrincipal?.userRoles ?? [];
+ } else if (null === apiRoles?.data?.clientPrincipal || undefined === apiRoles?.data) {
return ;
}
if (null === roles) {
return ;
} else {
const blockedRoles = ["anonymous", "authenticated"];
- const userRoles = roles.filter((role) => !blockedRoles.includes(role));
- const isAuthenticated = userRoles.length > 0 && !error;
- const isAdmin = roles.includes("admin");
- if (routeType === "admin") {
- return !isAdmin ? : children;
- } else {
- return !isAuthenticated ? : children;
+ const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? [];
+ const isAuthenticated = userRoles.length > 0 && !apiRoles?.error;
+ const isAdmin = roles?.includes("admin") || roles?.includes("superadmin");
+ if (routeType === "admin" && !isAdmin) {
+ return ;
}
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return children;
}
};
diff --git a/src/components/ReleaseNotesDialog.js b/src/components/ReleaseNotesDialog.js
new file mode 100644
index 000000000000..6fc9274dbca3
--- /dev/null
+++ b/src/components/ReleaseNotesDialog.js
@@ -0,0 +1,474 @@
+ο»Ώimport {
+ Component,
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import {
+ Box,
+ Button,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Link,
+ Stack,
+ Typography,
+} from "@mui/material";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkParse from "remark-parse";
+import rehypeRaw from "rehype-raw";
+import { unified } from "unified";
+import packageInfo from "../../public/version.json";
+import { ApiGetCall } from "../api/ApiCall";
+import { GitHub } from "@mui/icons-material";
+import { CippAutoComplete } from "./CippComponents/CippAutocomplete";
+
+const RELEASE_COOKIE_KEY = "cipp_release_notice";
+const RELEASE_OWNER = "KelvinTegelaar";
+const RELEASE_REPO = "CIPP";
+
+const secureFlag = () => {
+ if (typeof window === "undefined") {
+ return "";
+ }
+
+ return window.location.protocol === "https:" ? " Secure" : "";
+};
+
+const getCookie = (name) => {
+ if (typeof document === "undefined") {
+ return null;
+ }
+
+ const cookiePrefix = `${name}=`;
+ const cookies = document.cookie.split("; ");
+
+ for (const cookie of cookies) {
+ if (cookie.startsWith(cookiePrefix)) {
+ return decodeURIComponent(cookie.slice(cookiePrefix.length));
+ }
+ }
+
+ return null;
+};
+
+const setCookie = (name, value, days = 365) => {
+ if (typeof document === "undefined") {
+ return;
+ }
+
+ const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
+ document.cookie = `${name}=${encodeURIComponent(
+ value
+ )}; expires=${expires}; path=/; SameSite=Lax;${secureFlag()}`;
+};
+
+const buildReleaseMetadata = (version) => {
+ const [major = "0", minor = "0", patch = "0"] = String(version).split(".");
+ const currentTag = `v${major}.${minor}.${patch}`;
+ const baseTag = `v${major}.${minor}.0`;
+ const tagToUse = patch === "0" ? currentTag : baseTag;
+
+ return {
+ currentTag,
+ releaseTag: tagToUse,
+ releaseUrl: `https://github.com/${RELEASE_OWNER}/${RELEASE_REPO}/releases/tag/${tagToUse}`,
+ };
+};
+
+const formatReleaseBody = (body) => {
+ if (!body) {
+ return "";
+ }
+
+ return body.replace(/(^|[^\w/])@([a-zA-Z0-9-]+)/g, (match, prefix, username) => {
+ return `${prefix}[@${username}](https://github.com/${username})`;
+ });
+};
+
+class MarkdownErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error) {
+ if (process.env.NODE_ENV !== "production") {
+ // eslint-disable-next-line no-console
+ console.error("Failed to render release notes", error);
+ }
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback(this.state.error);
+ }
+
+ return this.props.children;
+ }
+}
+
+export const ReleaseNotesDialog = forwardRef((_props, ref) => {
+ const releaseMeta = useMemo(() => buildReleaseMetadata(packageInfo.version), []);
+ const [isEligible, setIsEligible] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [manualOpenRequested, setManualOpenRequested] = useState(false);
+ const [selectedReleaseTag, setSelectedReleaseTag] = useState(releaseMeta.releaseTag);
+ const hasOpenedRef = useRef(false);
+
+ useEffect(() => {
+ hasOpenedRef.current = false;
+ }, [releaseMeta.releaseTag]);
+
+ useEffect(() => {
+ setSelectedReleaseTag(releaseMeta.releaseTag);
+ }, [releaseMeta.releaseTag]);
+
+ useEffect(() => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ const storedValue = getCookie(RELEASE_COOKIE_KEY);
+
+ if (storedValue !== releaseMeta.releaseTag) {
+ setIsEligible(true);
+ }
+ }, [releaseMeta.releaseTag]);
+
+ const shouldFetchReleaseList = isEligible || manualOpenRequested || open;
+
+ const releaseListQuery = ApiGetCall({
+ url: "/api/ListGitHubReleaseNotes",
+ queryKey: "list-github-release-options",
+ data: {
+ Owner: RELEASE_OWNER,
+ Repository: RELEASE_REPO,
+ },
+ waiting: shouldFetchReleaseList,
+ staleTime: 300000,
+ });
+
+ const isReleaseListLoading = releaseListQuery.isLoading || releaseListQuery.isFetching;
+
+ const releaseCatalog = useMemo(() => {
+ return Array.isArray(releaseListQuery.data) ? releaseListQuery.data : [];
+ }, [releaseListQuery.data]);
+
+ useEffect(() => {
+ if (!releaseCatalog.length) {
+ return;
+ }
+
+ if (!selectedReleaseTag) {
+ setSelectedReleaseTag(releaseCatalog[0].releaseTag);
+ return;
+ }
+
+ const hasSelected = releaseCatalog.some((release) => release.releaseTag === selectedReleaseTag);
+
+ if (!hasSelected) {
+ const fallbackRelease =
+ releaseCatalog.find((release) => release.releaseTag === releaseMeta.releaseTag) ||
+ releaseCatalog[0];
+ if (fallbackRelease) {
+ setSelectedReleaseTag(fallbackRelease.releaseTag);
+ }
+ }
+ }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseTag]);
+
+ const releaseOptions = useMemo(() => {
+ const mapped = releaseCatalog.map((release) => {
+ const tag = release.releaseTag ?? release.tagName;
+ const label = release.name ? `${release.name} (${tag})` : tag;
+ return {
+ label,
+ value: tag,
+ addedFields: {
+ htmlUrl: release.htmlUrl,
+ publishedAt: release.publishedAt,
+ },
+ };
+ });
+
+ if (selectedReleaseTag && !mapped.some((option) => option.value === selectedReleaseTag)) {
+ mapped.push({
+ label: selectedReleaseTag,
+ value: selectedReleaseTag,
+ addedFields: {
+ htmlUrl: releaseMeta.releaseUrl,
+ publishedAt: null,
+ },
+ });
+ }
+
+ return mapped;
+ }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseUrl]);
+
+ const selectedReleaseValue = useMemo(() => {
+ if (!selectedReleaseTag) {
+ return null;
+ }
+
+ return (
+ releaseOptions.find((option) => option.value === selectedReleaseTag) || {
+ label: selectedReleaseTag,
+ value: selectedReleaseTag,
+ }
+ );
+ }, [releaseOptions, selectedReleaseTag]);
+
+ const handleReleaseChange = useCallback(
+ (newValue) => {
+ const nextValue = Array.isArray(newValue) ? newValue[0] : newValue;
+ if (nextValue?.value && nextValue.value !== selectedReleaseTag) {
+ setSelectedReleaseTag(nextValue.value);
+ }
+ },
+ [selectedReleaseTag]
+ );
+
+ useImperativeHandle(ref, () => ({
+ open: () => {
+ setManualOpenRequested(true);
+ setOpen(true);
+ },
+ }));
+
+ const selectedReleaseData = useMemo(() => {
+ if (!selectedReleaseTag) {
+ return null;
+ }
+
+ return (
+ releaseCatalog.find((release) => release.releaseTag === selectedReleaseTag) ||
+ releaseCatalog.find((release) => release.releaseTag === releaseMeta.releaseTag) ||
+ null
+ );
+ }, [releaseCatalog, selectedReleaseTag, releaseMeta.releaseTag]);
+
+ const handleDismissUntilNextRelease = () => {
+ const newestRelease = releaseCatalog[0];
+ const tagToStore =
+ newestRelease?.releaseTag ?? newestRelease?.tagName ?? releaseMeta.releaseTag;
+ setCookie(RELEASE_COOKIE_KEY, tagToStore);
+ setOpen(false);
+ setIsExpanded(false);
+ setManualOpenRequested(false);
+ setIsEligible(false);
+ };
+
+ const handleRemindLater = () => {
+ setOpen(false);
+ setIsExpanded(false);
+ setManualOpenRequested(false);
+ };
+
+ const toggleExpanded = () => {
+ setIsExpanded((prev) => !prev);
+ };
+
+ const requestedVersionLabel =
+ selectedReleaseData?.releaseTag ?? selectedReleaseTag ?? releaseMeta.currentTag;
+ const releaseName =
+ selectedReleaseData?.name || selectedReleaseValue?.label || `CIPP ${releaseMeta.currentTag}`;
+ const releaseHeading = releaseName || requestedVersionLabel;
+ const releaseBody = typeof selectedReleaseData?.body === "string" ? selectedReleaseData.body : "";
+ const releaseUrl =
+ selectedReleaseData?.htmlUrl ??
+ selectedReleaseValue?.addedFields?.htmlUrl ??
+ releaseMeta.releaseUrl;
+ const formattedReleaseBody = useMemo(() => formatReleaseBody(releaseBody), [releaseBody]);
+ const gfmSupport = useMemo(() => {
+ if (!formattedReleaseBody) {
+ return { plugins: [remarkGfm], error: null };
+ }
+
+ try {
+ unified().use(remarkParse).use(remarkGfm).parse(formattedReleaseBody);
+ return { plugins: [remarkGfm], error: null };
+ } catch (err) {
+ return { plugins: [], error: err };
+ }
+ }, [formattedReleaseBody]);
+
+ useEffect(() => {
+ if (!isEligible || hasOpenedRef.current) {
+ return;
+ }
+
+ if (releaseCatalog.length || releaseListQuery.error) {
+ setOpen(true);
+ hasOpenedRef.current = true;
+ }
+ }, [isEligible, releaseCatalog.length, releaseListQuery.error]);
+
+ return (
+
+
+
+
+ {`Release notes for ${releaseHeading}`}
+
+
+
+ {isExpanded ? "Shrink" : "Expand"}
+
+
+
+
+
+ {releaseListQuery.error ? (
+
+ We couldn't load additional releases right now. The latest release notes are shown
+ below.
+ {releaseListQuery.error?.message ? ` (${releaseListQuery.error.message})` : ""}
+
+ ) : null}
+ {gfmSupport.error ? (
+
+ Displaying these release notes without GitHub-flavoured markdown enhancements due to a
+ parsing issue. Formatting may look different.
+
+ ) : null}
+ {isReleaseListLoading && !selectedReleaseData ? (
+
+
+
+ ) : releaseListQuery.error ? (
+
+ We couldn't load the release notes right now. You can view them on GitHub instead.
+ {releaseListQuery.error?.message ? ` (${releaseListQuery.error.message})` : ""}
+
+ ) : (
+
+ (
+
+
+ We couldn't format these release notes
+ {error?.message ? ` (${error.message})` : ""}. A plain-text version is shown
+ below.
+
+
+ {releaseBody}
+
+
+ )}
+ >
+ (
+
+ ),
+ img: ({ node, ...props }) => (
+
+ ),
+ }}
+ rehypePlugins={[rehypeRaw]}
+ remarkPlugins={gfmSupport.plugins}
+ >
+ {formattedReleaseBody}
+
+
+
+ )}
+
+
+
+ }
+ >
+ View release notes on GitHub
+
+
+
+ Remind me next time
+
+
+ Don't show until next release
+
+
+
+
+ );
+});
+
+ReleaseNotesDialog.displayName = "ReleaseNotesDialog";
diff --git a/src/components/bulk-actions-menu.js b/src/components/bulk-actions-menu.js
index fd15898e28a3..dc1a0c167c1a 100644
--- a/src/components/bulk-actions-menu.js
+++ b/src/components/bulk-actions-menu.js
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon";
import { Button, Link, ListItemText, Menu, MenuItem, SvgIcon } from "@mui/material";
import { usePopover } from "../hooks/use-popover";
-import { FilePresent, Laptop, Mail, Share, Shield, ShieldMoon } from "@mui/icons-material";
+import { FilePresent, Laptop, Mail, Share, Shield, ShieldMoon, PrecisionManufacturing, BarChart } from "@mui/icons-material";
import { GlobeAltIcon, UsersIcon, ServerIcon } from "@heroicons/react/24/outline";
function getIconByName(iconName) {
@@ -25,6 +25,10 @@ function getIconByName(iconName) {
return ;
case "ShieldMoon":
return ;
+ case "PrecisionManufacturing":
+ return ;
+ case "BarChart":
+ return ;
default:
return null;
}
diff --git a/src/components/linearProgressWithLabel.jsx b/src/components/linearProgressWithLabel.jsx
index f01031da45ca..55b4db2967bd 100644
--- a/src/components/linearProgressWithLabel.jsx
+++ b/src/components/linearProgressWithLabel.jsx
@@ -1,12 +1,65 @@
-import { Box, LinearProgress, Typography } from "@mui/material";
+import { Box, LinearProgress } from "@mui/material";
export const LinearProgressWithLabel = (props) => {
+ const { value, colourLevels, addedLabel, ...otherProps } = props;
+
+ // Function to determine color based on value and colourLevels
+ const getProgressColor = (value, colourLevels) => {
+ if (!colourLevels) {
+ return undefined; // Use default MUI color
+ }
+
+ // Check if flipped mode is enabled
+ const isFlipped = colourLevels === 'flipped' || colourLevels.flipped === true;
+
+ if (isFlipped) {
+ // Flipped color order: green -> yellow -> orange -> red
+ if (value >= 0 && value < 25) {
+ return "#4caf50"; // Green for low values when flipped
+ } else if (value >= 25 && value < 50) {
+ return "#ffeb3b"; // Yellow
+ } else if (value >= 50 && value < 75) {
+ return "#ff9800"; // Orange
+ } else if (value >= 75 && value <= 100) {
+ return "#f44336"; // Red for high values when flipped
+ }
+ } else {
+ // Normal color order: red -> orange -> yellow -> green
+ if (value >= 0 && value < 25) {
+ return colourLevels.level0to25 || "#f44336"; // Default red
+ } else if (value >= 25 && value < 50) {
+ return colourLevels.level25to50 || "#ff9800"; // Default orange
+ } else if (value >= 50 && value < 75) {
+ return colourLevels.level50to75 || "#ffeb3b"; // Default yellow
+ } else if (value >= 75 && value <= 100) {
+ return colourLevels.level75to100 || "#4caf50"; // Default green
+ }
+ }
+
+ return undefined; // Fallback to default
+ };
+
+ const progressColor = getProgressColor(value, colourLevels);
+
return (
-
+
- {`${Math.round(props.value)}% ${props?.addedLabel ?? ""}`}
+ {`${Math.round(value)}% ${addedLabel ?? ""}`}
);
};
diff --git a/src/components/pdfExportButton.js b/src/components/pdfExportButton.js
index 3d011fd1ebf2..c95c27911bc4 100644
--- a/src/components/pdfExportButton.js
+++ b/src/components/pdfExportButton.js
@@ -3,10 +3,12 @@ import { PictureAsPdf } from "@mui/icons-material";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
import { getCippFormatting } from "../utils/get-cipp-formatting";
+import { useSettings } from "../hooks/use-settings";
export const PDFExportButton = (props) => {
const { rows, columns, reportName, columnVisibility, ...other } = props;
-
+ const brandingSettings = useSettings().customBranding;
+ //we need to use jspdf here because the react-pdf library gets killed with our amount of data.
const handleExportRows = (rows) => {
const unit = "pt";
const size = "A3"; // Use A1, A2, A3 or A4
@@ -27,12 +29,98 @@ export const PDFExportButton = (props) => {
return formattedRow;
});
+ // Add custom branding logo if available
+ let logoHeight = 0;
+ if (brandingSettings?.logo) {
+ try {
+ const logoSize = 60; // Fixed logo height
+ const logoX = 40; // Left margin
+ const logoY = 30; // Top margin
+
+ // Add the base64 image to the PDF
+ doc.addImage(brandingSettings.logo, "PNG", logoX, logoY, logoSize, logoSize);
+ logoHeight = logoSize + 20; // Logo height plus some spacing
+ } catch (error) {
+ console.warn("Failed to add logo to PDF:", error);
+ }
+ }
+
+ // Calculate column widths based on content and available space
+ const pageWidth = doc.internal.pageSize.getWidth();
+ const margin = 40; // Consistent margins from edges
+ const availableWidth = pageWidth - 2 * margin;
+ const columnCount = exportColumns.length;
+
+ // Calculate dynamic column widths based on content length
+ const columnWidths = exportColumns.map((col) => {
+ const headerLength = col.header.length;
+ const maxContentLength = Math.max(
+ ...formattedData.map((row) => String(row[col.dataKey] || "").length)
+ );
+ const estimatedWidth = Math.max(headerLength, maxContentLength) * 6; // 6 points per character
+ return Math.min(estimatedWidth, (availableWidth / columnCount) * 1.5); // Cap at 1.5x average
+ });
+
+ // Normalize widths to fit available space
+ const totalEstimatedWidth = columnWidths.reduce((sum, width) => sum + width, 0);
+ const normalizedWidths = columnWidths.map(
+ (width) => (width / totalEstimatedWidth) * availableWidth
+ );
+
+ // Convert hex color to RGB if custom branding color is provided
+ const getHeaderColor = () => {
+ if (brandingSettings?.colour) {
+ const hex = brandingSettings.colour.replace("#", "");
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+ return [r, g, b];
+ }
+ return [247, 127, 0]; // Default orange color
+ };
+
let content = {
- startY: 100,
- columns: exportColumns,
- body: formattedData,
+ startY: 100 + logoHeight, // Adjust table start position based on logo
+ head: [exportColumns.map((col) => col.header)],
+ body: formattedData.map((row) => exportColumns.map((col) => String(row[col.dataKey] || ""))),
theme: "striped",
- headStyles: { fillColor: [247, 127, 0] },
+ headStyles: {
+ fillColor: getHeaderColor(),
+ textColor: [255, 255, 255],
+ fontStyle: "bold",
+ halign: "center",
+ valign: "middle",
+ fontSize: 10,
+ cellPadding: 8,
+ },
+ bodyStyles: {
+ fontSize: 9,
+ cellPadding: 6,
+ valign: "top",
+ overflow: "linebreak",
+ cellWidth: "wrap",
+ },
+ columnStyles: exportColumns.reduce((styles, col, index) => {
+ styles[index] = {
+ cellWidth: normalizedWidths[index],
+ halign: "left",
+ valign: "top",
+ };
+ return styles;
+ }, {}),
+ margin: {
+ top: margin,
+ right: margin,
+ bottom: margin,
+ left: margin,
+ },
+ tableWidth: "auto",
+ styles: {
+ overflow: "linebreak",
+ cellWidth: "wrap",
+ fontSize: 9,
+ cellPadding: 6,
+ },
};
autoTable(doc, content);
diff --git a/src/components/property-list-item.js b/src/components/property-list-item.js
index aa61fa0b5d23..4249e975cef0 100644
--- a/src/components/property-list-item.js
+++ b/src/components/property-list-item.js
@@ -1,16 +1,6 @@
-import {
- Box,
- Button,
- IconButton,
- ListItem,
- ListItemText,
- SvgIcon,
- Tooltip,
- Typography,
-} from "@mui/material";
+import { Box, Button, ListItem, ListItemText, Typography } from "@mui/material";
import { useState } from "react";
-import CopyToClipboard from "react-copy-to-clipboard";
-import { CopyAll } from "@mui/icons-material";
+import { CippCopyToClipBoard } from "./CippComponents/CippCopyToClipboard";
export const PropertyListItem = (props) => {
const {
@@ -57,17 +47,7 @@ export const PropertyListItem = (props) => {
)}
>
)}
- {copyItems && (
-
-
-
-
-
-
-
-
-
- )}
+ {copyItems && }
)}
diff --git a/src/components/query-field.js b/src/components/query-field.js
index f1078830ee60..dfeb6f37e1a4 100644
--- a/src/components/query-field.js
+++ b/src/components/query-field.js
@@ -29,7 +29,6 @@ export const QueryField = (props) => {
inputRef.current.focus();
}
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
[disabled]);
const handleChange = useCallback((event) => {
diff --git a/src/components/resource-loading.js b/src/components/resource-loading.js
index 743a53baea03..d80a6b09452a 100644
--- a/src/components/resource-loading.js
+++ b/src/components/resource-loading.js
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
-import { CircularProgress, SvgIcon, Typography } from "@mui/material";
+import { CircularProgress, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
const ResourceLoadingRoot = styled("div")(({ theme }) => ({
diff --git a/src/components/toaster.js b/src/components/toaster.js
index aa26b5c462c6..933adf625de1 100644
--- a/src/components/toaster.js
+++ b/src/components/toaster.js
@@ -1,5 +1,5 @@
import { CloseSharp } from "@mui/icons-material";
-import { Alert, Button, IconButton, Snackbar } from "@mui/material";
+import { Alert, IconButton, Snackbar } from "@mui/material";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { closeToast } from "../store/toasts";
diff --git a/src/contexts/release-notes-context.js b/src/contexts/release-notes-context.js
new file mode 100644
index 000000000000..54f29623522e
--- /dev/null
+++ b/src/contexts/release-notes-context.js
@@ -0,0 +1,30 @@
+ο»Ώimport { createContext, useCallback, useContext, useMemo, useRef } from "react";
+import PropTypes from "prop-types";
+import { ReleaseNotesDialog } from "../components/ReleaseNotesDialog";
+
+const ReleaseNotesContext = createContext({
+ openReleaseNotes: () => {},
+});
+
+export const ReleaseNotesProvider = ({ children }) => {
+ const dialogRef = useRef(null);
+
+ const openReleaseNotes = useCallback(() => {
+ dialogRef.current?.open();
+ }, []);
+
+ const value = useMemo(() => ({ openReleaseNotes }), [openReleaseNotes]);
+
+ return (
+
+ {children}
+
+
+ );
+};
+
+ReleaseNotesProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export const useReleaseNotes = () => useContext(ReleaseNotesContext);
diff --git a/src/contexts/settings-context.js b/src/contexts/settings-context.js
index a265c9fd09aa..35c87c90d658 100644
--- a/src/contexts/settings-context.js
+++ b/src/contexts/settings-context.js
@@ -74,6 +74,12 @@ const initialSettings = {
pinNav: true,
currentTenant: null,
showDevtools: false,
+ customBranding: {
+ colour: "#F77F00",
+ logo: null,
+ },
+ persistFilters: false,
+ lastUsedFilters: {},
};
const initialState = {
@@ -86,6 +92,7 @@ export const SettingsContext = createContext({
handleReset: () => {},
handleUpdate: () => {},
isCustom: false,
+ setLastUsedFilter: () => {},
});
export const SettingsProvider = (props) => {
@@ -146,6 +153,19 @@ export const SettingsProvider = (props) => {
handleReset,
handleUpdate,
isCustom,
+ setLastUsedFilter: (page, filter) => {
+ setState((prevState) => {
+ const updated = {
+ ...prevState,
+ lastUsedFilters: {
+ ...prevState.lastUsedFilters,
+ [page]: filter,
+ },
+ };
+ storeSettings(updated);
+ return updated;
+ });
+ },
}}
>
{children}
diff --git a/src/data/AuditLogSchema.json b/src/data/AuditLogSchema.json
index 4ed181f46654..93122adbc4cf 100644
--- a/src/data/AuditLogSchema.json
+++ b/src/data/AuditLogSchema.json
@@ -16,7 +16,10 @@
"CIPPGeoLocation": "List:countryList",
"CIPPBadRepIP": "String",
"CIPPHostedIP": "String",
- "CIPPIPDetected": "String"
+ "CIPPIPDetected": "String",
+ "CIPPUserId": "String",
+ "CIPPUserKey": "String",
+ "CIPPUsername": "String"
},
"Audit.Exchange": {
"Id": "Combination GUID",
@@ -71,40 +74,126 @@
"LogonError": "String"
},
"List:Operation": [
- { "value": "UserLoggedIn", "label": "A user logged in" },
- { "value": "mailitemsaccessed", "label": "accessed mailbox items" },
- { "value": "add delegation entry.", "label": "added delegation entry" },
- { "value": "add domain to company.", "label": "added domain to company" },
- { "value": "add group.", "label": "added group" },
- { "value": "add member to group.", "label": "added member to group" },
- { "value": "add-mailboxpermission", "label": "added delegate mailbox permissions" },
- { "value": "add member to role.", "label": "added member to role" },
- { "value": "add partner to company.", "label": "added a partner to the directory" },
- { "value": "add service principal.", "label": "added service principal" },
+ {
+ "value": "UserLoggedIn",
+ "label": "A user logged in"
+ },
+ {
+ "value": "mailitemsaccessed",
+ "label": "accessed mailbox items"
+ },
+ {
+ "value": "add delegation entry.",
+ "label": "added delegation entry"
+ },
+ {
+ "value": "add domain to company.",
+ "label": "added domain to company"
+ },
+ {
+ "value": "add group.",
+ "label": "added group"
+ },
+ {
+ "value": "add member to group.",
+ "label": "added member to group"
+ },
+ {
+ "value": "add-mailboxpermission",
+ "label": "added delegate mailbox permissions"
+ },
+ {
+ "value": "add member to role.",
+ "label": "added member to role"
+ },
+ {
+ "value": "add partner to company.",
+ "label": "added a partner to the directory"
+ },
+ {
+ "value": "add service principal.",
+ "label": "added service principal"
+ },
{
"value": "add service principal credentials.",
"label": "added credentials to a service principal"
},
- { "value": "add user.", "label": "added user" },
- { "value": "addfolderpermissions", "label": "added permissions to folder" },
- { "value": "applyrecordlabel", "label": "labeled message as a record" },
- { "value": "change user license.", "label": "changed user license" },
- { "value": "change user password.", "label": "changed user password" },
- { "value": "copy", "label": "copied messages to another folder" },
- { "value": "create", "label": "created mailbox item" },
- { "value": "delete group.", "label": "deleted group" },
- { "value": "delete user.", "label": "deleted user" },
- { "value": "harddelete", "label": "purged messages from the mailbox" },
- { "value": "mailboxlogin", "label": "user signed in to mailbox" },
- { "value": "move", "label": "moved messages to another folder" },
- { "value": "movetodeleteditems", "label": "moved messages to deleted items folder" },
- { "value": "new-inboxrule", "label": "created new inbox rule in outlook web app" },
- { "value": "remove delegation entry.", "label": "removed delegation entry" },
- { "value": "remove domain from company.", "label": "removed domain from company" },
- { "value": "remove member from group.", "label": "removed member from group" },
- { "value": "remove member from a role.", "label": "remove member from a role" },
- { "value": "Disable Strong Authentication.", "label": "Disable Strong Authentication." },
-
+ {
+ "value": "add user.",
+ "label": "added user"
+ },
+ {
+ "value": "addfolderpermissions",
+ "label": "added permissions to folder"
+ },
+ {
+ "value": "applyrecordlabel",
+ "label": "labeled message as a record"
+ },
+ {
+ "value": "change user license.",
+ "label": "changed user license"
+ },
+ {
+ "value": "change user password.",
+ "label": "changed user password"
+ },
+ {
+ "value": "copy",
+ "label": "copied messages to another folder"
+ },
+ {
+ "value": "create",
+ "label": "created mailbox item"
+ },
+ {
+ "value": "delete group.",
+ "label": "deleted group"
+ },
+ {
+ "value": "delete user.",
+ "label": "deleted user"
+ },
+ {
+ "value": "harddelete",
+ "label": "purged messages from the mailbox"
+ },
+ {
+ "value": "mailboxlogin",
+ "label": "user signed in to mailbox"
+ },
+ {
+ "value": "move",
+ "label": "moved messages to another folder"
+ },
+ {
+ "value": "movetodeleteditems",
+ "label": "moved messages to deleted items folder"
+ },
+ {
+ "value": "new-inboxrule",
+ "label": "created new inbox rule in outlook web app"
+ },
+ {
+ "value": "remove delegation entry.",
+ "label": "removed delegation entry"
+ },
+ {
+ "value": "remove domain from company.",
+ "label": "removed domain from company"
+ },
+ {
+ "value": "remove member from group.",
+ "label": "removed member from group"
+ },
+ {
+ "value": "remove member from a role.",
+ "label": "remove member from a role"
+ },
+ {
+ "value": "Disable Strong Authentication.",
+ "label": "Disable Strong Authentication."
+ },
{
"value": "remove service principal.",
"label": "removed a service principal from the directory"
@@ -113,19 +202,58 @@
"value": "remove service principal credentials.",
"label": "removed credentials from a service principal"
},
- { "value": "remove-mailboxpermission", "label": "removed delegate mailbox permissions" },
- { "value": "remove member from role.", "label": "removed a user from a directory role" },
- { "value": "remove partner from company.", "label": "removed a partner from the directory" },
- { "value": "removefolderpermissions", "label": "removed permissions from folder" },
- { "value": "reset user password.", "label": "reset user password" },
- { "value": "send", "label": "sent message" },
- { "value": "sendas", "label": "sent message using send as permissions" },
- { "value": "sendonbehalf", "label": "sent message using send on behalf permissions" },
- { "value": "set company contact information.", "label": "set company contact information" },
- { "value": "set company information.", "label": "set company information" },
- { "value": "set delegation entry.", "label": "set delegation entry" },
- { "value": "set dirsyncenabled flag.", "label": "turned on azure ad sync" },
- { "value": "set domain authentication.", "label": "set domain authentication" },
+ {
+ "value": "remove-mailboxpermission",
+ "label": "removed delegate mailbox permissions"
+ },
+ {
+ "value": "remove member from role.",
+ "label": "removed a user from a directory role"
+ },
+ {
+ "value": "remove partner from company.",
+ "label": "removed a partner from the directory"
+ },
+ {
+ "value": "removefolderpermissions",
+ "label": "removed permissions from folder"
+ },
+ {
+ "value": "reset user password.",
+ "label": "reset user password"
+ },
+ {
+ "value": "send",
+ "label": "sent message"
+ },
+ {
+ "value": "sendas",
+ "label": "sent message using send as permissions"
+ },
+ {
+ "value": "sendonbehalf",
+ "label": "sent message using send on behalf permissions"
+ },
+ {
+ "value": "set company contact information.",
+ "label": "set company contact information"
+ },
+ {
+ "value": "set company information.",
+ "label": "set company information"
+ },
+ {
+ "value": "set delegation entry.",
+ "label": "set delegation entry"
+ },
+ {
+ "value": "set dirsyncenabled flag.",
+ "label": "turned on azure ad sync"
+ },
+ {
+ "value": "set domain authentication.",
+ "label": "set domain authentication"
+ },
{
"value": "set federation settings on domain.",
"label": "updated the federation settings for a domain"
@@ -134,29 +262,69 @@
"value": "set force change user password.",
"label": "set property that forces user to change password"
},
- { "value": "set-inboxrule", "label": "modified inbox rule from outlook web app" },
- { "value": "set license properties.", "label": "set license properties" },
- { "value": "set password policy.", "label": "set password policy" },
- { "value": "softdelete", "label": "deleted messages from deleted items folder" },
- { "value": "update", "label": "updated message" },
- { "value": "update user.", "label": "updated user" },
- { "value": "update group.", "label": "updated group" },
- { "value": "update domain.", "label": "updated domain" },
+ {
+ "value": "set-inboxrule",
+ "label": "modified inbox rule from outlook web app"
+ },
+ {
+ "value": "set license properties.",
+ "label": "set license properties"
+ },
+ {
+ "value": "set password policy.",
+ "label": "set password policy"
+ },
+ {
+ "value": "softdelete",
+ "label": "deleted messages from deleted items folder"
+ },
+ {
+ "value": "update",
+ "label": "updated message"
+ },
+ {
+ "value": "update user.",
+ "label": "updated user"
+ },
+ {
+ "value": "update group.",
+ "label": "updated group"
+ },
+ {
+ "value": "update domain.",
+ "label": "updated domain"
+ },
{
"value": "updatecalendardelegation",
"label": "added or removed user with delegate access to calendar folder"
},
- { "value": "updatefolderpermissions", "label": "modified folder permission" },
- { "value": "updateinboxrules", "label": "updated inbox rules from outlook client" },
- { "value": "verify domain.", "label": "verified domain" },
- { "value": "verify email verified domain.", "label": "verified email verified domain" },
+ {
+ "value": "updatefolderpermissions",
+ "label": "modified folder permission"
+ },
+ {
+ "value": "updateinboxrules",
+ "label": "updated inbox rules from outlook client"
+ },
+ {
+ "value": "verify domain.",
+ "label": "verified domain"
+ },
+ {
+ "value": "verify email verified domain.",
+ "label": "verified email verified domain"
+ },
{
"value": "Update StsRefreshTokenValidFrom Timestamp.",
"label": "Update StsRefreshTokenValidFrom Timestamp."
}
],
"List:LogonType": [
- { "value": 0, "Membername": "Owner", "label": "The mailbox owner." },
+ {
+ "value": 0,
+ "Membername": "Owner",
+ "label": "The mailbox owner."
+ },
{
"value": 1,
"Membername": "Admin",
@@ -177,19 +345,63 @@
"Membername": "SystemService",
"label": "A service account in the Microsoft datacenter"
},
- { "value": 5, "Membername": "BestAccess", "label": "Reserved for internal use." },
- { "value": 6, "Membername": "DelegatedAdmin", "label": "A delegated administrator." }
+ {
+ "value": 5,
+ "Membername": "BestAccess",
+ "label": "Reserved for internal use."
+ },
+ {
+ "value": 6,
+ "Membername": "DelegatedAdmin",
+ "label": "A delegated administrator."
+ }
],
"List:UserType": [
- { "value": 0, "Membername": "Regular", "label": "A regular user." },
- { "value": 1, "Membername": "Reserved", "label": "A reserved user." },
- { "value": 2, "Membername": "Admin", "label": "An administrator." },
- { "value": 3, "Membername": "DcAdmin", "label": "A Microsoft datacenter operator." },
- { "value": 4, "Membername": "System", "label": "A system account." },
- { "value": 5, "Membername": "Application", "label": "An application." },
- { "value": 6, "Membername": "ServicePrincipal", "label": "A service principal." },
- { "value": 7, "Membername": "CustomPolicy", "label": "A custom policy." },
- { "value": 8, "Membername": "SystemPolicy", "label": "A system policy." }
+ {
+ "value": 0,
+ "Membername": "Regular",
+ "label": "A regular user."
+ },
+ {
+ "value": 1,
+ "Membername": "Reserved",
+ "label": "A reserved user."
+ },
+ {
+ "value": 2,
+ "Membername": "Admin",
+ "label": "An administrator."
+ },
+ {
+ "value": 3,
+ "Membername": "DcAdmin",
+ "label": "A Microsoft datacenter operator."
+ },
+ {
+ "value": 4,
+ "Membername": "System",
+ "label": "A system account."
+ },
+ {
+ "value": 5,
+ "Membername": "Application",
+ "label": "An application."
+ },
+ {
+ "value": 6,
+ "Membername": "ServicePrincipal",
+ "label": "A service principal."
+ },
+ {
+ "value": 7,
+ "Membername": "CustomPolicy",
+ "label": "A custom policy."
+ },
+ {
+ "value": 8,
+ "Membername": "SystemPolicy",
+ "label": "A system policy."
+ }
],
"List:AuditLogRecordType": [
{
@@ -207,13 +419,21 @@
"Membername": "ExchangeItemGroup",
"label": "Events from an Exchange mailbox audit log for actions that can be performed on multiple items, such as moving or deleted one or more email messages."
},
- { "value": 4, "Membername": "SharePoint", "label": "SharePoint events." },
+ {
+ "value": 4,
+ "Membername": "SharePoint",
+ "label": "SharePoint events."
+ },
{
"value": 6,
"Membername": "SharePointFileOperation",
"label": "SharePoint file operation events."
},
- { "value": 7, "Membername": "OneDrive", "label": "OneDrive for Business events." },
+ {
+ "value": 7,
+ "Membername": "OneDrive",
+ "label": "OneDrive for Business events."
+ },
{
"value": 8,
"Membername": "AzureActiveDirectory",
@@ -269,9 +489,21 @@
"Membername": "ExchangeAggregatedOperation",
"label": "Aggregated Exchange mailbox auditing events."
},
- { "value": 20, "Membername": "PowerBIAudit", "label": "Power BI events." },
- { "value": 21, "Membername": "CRM", "label": "Dynamics 365 events." },
- { "value": 22, "Membername": "Yammer", "label": "Yammer events." },
+ {
+ "value": 20,
+ "Membername": "PowerBIAudit",
+ "label": "Power BI events."
+ },
+ {
+ "value": 21,
+ "Membername": "CRM",
+ "label": "Dynamics 365 events."
+ },
+ {
+ "value": 22,
+ "Membername": "Yammer",
+ "label": "Yammer events."
+ },
{
"value": 23,
"Membername": "SkypeForBusinessCmdlets",
@@ -282,7 +514,11 @@
"Membername": "Discovery",
"label": "Events for eDiscovery activities performed by running content searches and managing eDiscovery cases in the Security & Compliance Center."
},
- { "value": 25, "Membername": "MicrosoftTeams", "label": "Events from Microsoft Teams." },
+ {
+ "value": 25,
+ "Membername": "MicrosoftTeams",
+ "label": "Events from Microsoft Teams."
+ },
{
"value": 28,
"Membername": "ThreatIntelligence",
@@ -298,8 +534,16 @@
"Membername": "MicrosoftFlow",
"label": "Microsoft Power Automate (formerly called Microsoft Flow) events."
},
- { "value": 31, "Membername": "AeD", "label": "Advanced eDiscovery events." },
- { "value": 32, "Membername": "MicrosoftStream", "label": "Microsoft Stream events." },
+ {
+ "value": 31,
+ "Membername": "AeD",
+ "label": "Advanced eDiscovery events."
+ },
+ {
+ "value": 32,
+ "Membername": "MicrosoftStream",
+ "label": "Microsoft Stream events."
+ },
{
"value": 33,
"Membername": "ComplianceDLPSharePointClassification",
@@ -310,7 +554,11 @@
"Membername": "ThreatFinder",
"label": "Campaign-related events from Microsoft Defender for Office 365."
},
- { "value": 35, "Membername": "Project", "label": "Microsoft Project events." },
+ {
+ "value": 35,
+ "Membername": "Project",
+ "label": "Microsoft Project events."
+ },
{
"value": 36,
"Membername": "SharePointListOperation",
@@ -326,7 +574,11 @@
"Membername": "DataGovernance",
"label": "Events related to retention policies and retention labels in the Security & Compliance Center"
},
- { "value": 39, "Membername": "Kaizala", "label": "Kaizala events." },
+ {
+ "value": 39,
+ "Membername": "Kaizala",
+ "label": "Kaizala events."
+ },
{
"value": 40,
"Membername": "SecurityComplianceAlerts",
@@ -352,7 +604,11 @@
"Membername": "WorkplaceAnalytics",
"label": "Workplace Analytics events."
},
- { "value": 45, "Membername": "PowerAppsApp", "label": "Power Apps events." },
+ {
+ "value": 45,
+ "Membername": "PowerAppsApp",
+ "label": "Power Apps events."
+ },
{
"value": 46,
"Membername": "PowerAppsPlan",
@@ -408,13 +664,21 @@
"Membername": "SharePointFieldOperation",
"label": "SharePoint list field events."
},
- { "value": 57, "Membername": "MicrosoftTeamsAdmin", "label": "Teams admin events." },
+ {
+ "value": 57,
+ "Membername": "MicrosoftTeamsAdmin",
+ "label": "Teams admin events."
+ },
{
"value": 58,
"Membername": "HRSignal",
"label": "Events related to HR data signals that support the Insider risk management solution."
},
- { "value": 59, "Membername": "MicrosoftTeamsDevice", "label": "Teams device events." },
+ {
+ "value": 59,
+ "Membername": "MicrosoftTeamsDevice",
+ "label": "Teams device events."
+ },
{
"value": 60,
"Membername": "MicrosoftTeamsAnalytics",
@@ -430,15 +694,31 @@
"Membername": "Campaign",
"label": "Email campaign events from Microsoft Defender for Office 365."
},
- { "value": 63, "Membername": "DLPEndpoint", "label": "Endpoint DLP events." },
+ {
+ "value": 63,
+ "Membername": "DLPEndpoint",
+ "label": "Endpoint DLP events."
+ },
{
"value": 64,
"Membername": "AirInvestigation",
"label": "Automated incident response (AIR) events."
},
- { "value": 65, "Membername": "Quarantine", "label": "Quarantine events." },
- { "value": 66, "Membername": "MicrosoftForms", "label": "Microsoft Forms events." },
- { "value": 67, "Membername": "ApplicationAudit", "label": "Application audit events." },
+ {
+ "value": 65,
+ "Membername": "Quarantine",
+ "label": "Quarantine events."
+ },
+ {
+ "value": 66,
+ "Membername": "MicrosoftForms",
+ "label": "Microsoft Forms events."
+ },
+ {
+ "value": 67,
+ "Membername": "ApplicationAudit",
+ "label": "Application audit events."
+ },
{
"value": 68,
"Membername": "ComplianceSupervisionExchange",
@@ -464,13 +744,21 @@
"Membername": "MipAutoLabelSharePointPolicyLocation",
"label": "Auto-labeling policy events in SharePoint."
},
- { "value": 73, "Membername": "MicrosoftTeamsShifts", "label": "Teams Shifts events." },
+ {
+ "value": 73,
+ "Membername": "MicrosoftTeamsShifts",
+ "label": "Teams Shifts events."
+ },
{
"value": 75,
"Membername": "MipAutoLabelExchangeItem",
"label": "Auto-labeling events in Exchange."
},
- { "value": 76, "Membername": "CortanaBriefing", "label": "Briefing email events." },
+ {
+ "value": 76,
+ "Membername": "CortanaBriefing",
+ "label": "Briefing email events."
+ },
{
"value": 78,
"Membername": "WDATPAlerts",
@@ -526,15 +814,31 @@
"Membername": "PhysicalBadgingSignal",
"label": "Events related to physical badging signals that support the Insider risk management solution."
},
- { "value": 93, "Membername": "AipDiscover", "label": "AIP scanner events" },
+ {
+ "value": 93,
+ "Membername": "AipDiscover",
+ "label": "AIP scanner events"
+ },
{
"value": 94,
"Membername": "AipSensitivityLabelAction",
"label": "AIP sensitivity label events"
},
- { "value": 95, "Membername": "AipProtectionAction", "label": "AIP protection events" },
- { "value": 96, "Membername": "AipFileDeleted", "label": "AIP file deletion events" },
- { "value": 97, "Membername": "AipHeartBeat", "label": "AIP heartbeat events" },
+ {
+ "value": 95,
+ "Membername": "AipProtectionAction",
+ "label": "AIP protection events"
+ },
+ {
+ "value": 96,
+ "Membername": "AipFileDeleted",
+ "label": "AIP file deletion events"
+ },
+ {
+ "value": 97,
+ "Membername": "AipHeartBeat",
+ "label": "AIP heartbeat events"
+ },
{
"value": 98,
"Membername": "MCASAlerts",
@@ -560,8 +864,16 @@
"Membername": "SharePointSearch",
"label": "Events related to searching an organization's SharePoint home site."
},
- { "value": 103, "Membername": "PrivacyInsights", "label": "Privacy insight events." },
- { "value": 105, "Membername": "MyAnalyticsSettings", "label": "MyAnalytics events." },
+ {
+ "value": 103,
+ "Membername": "PrivacyInsights",
+ "label": "Privacy insight events."
+ },
+ {
+ "value": 105,
+ "Membername": "MyAnalyticsSettings",
+ "label": "MyAnalytics events."
+ },
{
"value": 106,
"Membername": "SecurityComplianceUserChange",
@@ -617,13 +929,21 @@
"Membername": "PowerPagesSite",
"label": "Activities related to Power Pages site."
},
- { "value": 188, "Membername": "PlannerPlan", "label": "Microsoft Planner plan events." },
+ {
+ "value": 188,
+ "Membername": "PlannerPlan",
+ "label": "Microsoft Planner plan events."
+ },
{
"value": 189,
"Membername": "PlannerCopyPlan",
"label": "Microsoft Planner copy plan events."
},
- { "value": 190, "Membername": "PlannerTask", "label": "Microsoft Planner task events." },
+ {
+ "value": 190,
+ "Membername": "PlannerTask",
+ "label": "Microsoft Planner task events."
+ },
{
"value": 191,
"Membername": "PlannerRoster",
@@ -674,7 +994,11 @@
"Membername": "ProjectForThewebRoadmapSettings",
"label": "Microsoft Project for the web roadmap tenant settings events."
},
- { "value": 216, "Membername": "Viva Goals", "label": "Viva Goals events." },
+ {
+ "value": 216,
+ "Membername": "Viva Goals",
+ "label": "Viva Goals events."
+ },
{
"value": 217,
"Membername": "MicrosoftGraphDataConnectConsent",
@@ -685,7 +1009,11 @@
"Membername": "AttackSimAdmin",
"label": "Events related to admin activities in Attack Simulation & Training in Microsoft Defender for Office 365."
},
- { "value": 230, "Membername": "TeamsUpStrings", "label": "Teams UpStrings App Events." },
+ {
+ "value": 230,
+ "Membername": "TeamsUpStrings",
+ "label": "Teams UpStrings App Events."
+ },
{
"value": 231,
"Membername": "PlannerRosterSensitivityLabel",
@@ -718,257 +1046,1013 @@
}
],
"List:countryList": [
- { "value": "AF", "label": "Afghanistan" },
- { "value": "AX", "label": "\u00c5land Islands" },
- { "value": "AL", "label": "Albania" },
- { "value": "DZ", "label": "Algeria" },
- { "value": "AS", "label": "American Samoa" },
- { "value": "AD", "label": "Andorra" },
- { "value": "AO", "label": "Angola" },
- { "value": "AI", "label": "Anguilla" },
- { "value": "AQ", "label": "Antarctica" },
- { "value": "AG", "label": "Antigua and Barbuda" },
- { "value": "AR", "label": "Argentina" },
- { "value": "AM", "label": "Armenia" },
- { "value": "AW", "label": "Aruba" },
- { "value": "AC", "label": "Ascension Island" },
- { "value": "AU", "label": "Australia" },
- { "value": "AT", "label": "Austria" },
- { "value": "AZ", "label": "Azerbaijan" },
- { "value": "BS", "label": "Bahamas" },
- { "value": "BH", "label": "Bahrain" },
- { "value": "BD", "label": "Bangladesh" },
- { "value": "BB", "label": "Barbados" },
- { "value": "BY", "label": "Belarus" },
- { "value": "BE", "label": "Belgium" },
- { "value": "BZ", "label": "Belize" },
- { "value": "BJ", "label": "Benin" },
- { "value": "BM", "label": "Bermuda" },
- { "value": "BT", "label": "Bhutan" },
- { "value": "BO", "label": "Bolivia, Plurinational State of" },
- { "value": "BQ", "label": "Bonaire, Sint Eustatius and Saba" },
- { "value": "BA", "label": "Bosnia and Herzegovina" },
- { "value": "BW", "label": "Botswana" },
- { "value": "BV", "label": "Bouvet Island" },
- { "value": "BR", "label": "Brazil" },
- { "value": "IO", "label": "British Indian Ocean Territory" },
- { "value": "BN", "label": "Brunei Darussalam" },
- { "value": "BG", "label": "Bulgaria" },
- { "value": "BF", "label": "Burkina Faso" },
- { "value": "BI", "label": "Burundi" },
- { "value": "KH", "label": "Cambodia" },
- { "value": "CM", "label": "Cameroon" },
- { "value": "CA", "label": "Canada" },
- { "value": "CV", "label": "Cape Verde" },
- { "value": "KY", "label": "Cayman Islands" },
- { "value": "CF", "label": "Central African Republic" },
- { "value": "TD", "label": "Chad" },
- { "value": "CL", "label": "Chile" },
- { "value": "CN", "label": "China" },
- { "value": "CX", "label": "Christmas Island" },
- { "value": "CC", "label": "Cocos (Keeling) Islands" },
- { "value": "CO", "label": "Colombia" },
- { "value": "KM", "label": "Comoros" },
- { "value": "CG", "label": "Congo" },
- { "value": "CD", "label": "Congo, the Democratic Republic of the" },
- { "value": "CK", "label": "Cook Islands" },
- { "value": "CR", "label": "Costa Rica" },
- { "value": "CI", "label": "C\u00f4te d'Ivoire" },
- { "value": "HR", "label": "Croatia" },
- { "value": "CU", "label": "Cuba" },
- { "value": "CW", "label": "Cura\u00e7ao" },
- { "value": "CY", "label": "Cyprus" },
- { "value": "CZ", "label": "Czech Republic" },
- { "value": "DK", "label": "Denmark" },
- { "value": "DG", "label": "Diego Garcia" },
- { "value": "DJ", "label": "Djibouti" },
- { "value": "DM", "label": "Dominica" },
- { "value": "DO", "label": "Dominican Republic" },
- { "value": "EC", "label": "Ecuador" },
- { "value": "EG", "label": "Egypt" },
- { "value": "SV", "label": "El Salvador" },
- { "value": "GQ", "label": "Equatorial Guinea" },
- { "value": "ER", "label": "Eritrea" },
- { "value": "EE", "label": "Estonia" },
- { "value": "ET", "label": "Ethiopia" },
- { "value": "FK", "label": "Falkland Islands (Malvinas)" },
- { "value": "FO", "label": "Faroe Islands" },
- { "value": "FJ", "label": "Fiji" },
- { "value": "FI", "label": "Finland" },
- { "value": "FR", "label": "France" },
- { "value": "GF", "label": "French Guiana" },
- { "value": "PF", "label": "French Polynesia" },
- { "value": "TF", "label": "French Southern Territories" },
- { "value": "GA", "label": "Gabon" },
- { "value": "GM", "label": "Gambia" },
- { "value": "GE", "label": "Georgia" },
- { "value": "DE", "label": "Germany" },
- { "value": "GH", "label": "Ghana" },
- { "value": "GI", "label": "Gibraltar" },
- { "value": "GR", "label": "Greece" },
- { "value": "GL", "label": "Greenland" },
- { "value": "GD", "label": "Grenada" },
- { "value": "GP", "label": "Guadeloupe" },
- { "value": "GU", "label": "Guam" },
- { "value": "GT", "label": "Guatemala" },
- { "value": "GG", "label": "Guernsey" },
- { "value": "GN", "label": "Guinea" },
- { "value": "GW", "label": "Guinea-Bissau" },
- { "value": "GY", "label": "Guyana" },
- { "value": "HT", "label": "Haiti" },
- { "value": "HM", "label": "Heard Island and McDonald Islands" },
- { "value": "VA", "label": "Holy See (Vatican City State)" },
- { "value": "HN", "label": "Honduras" },
- { "value": "HK", "label": "Hong Kong" },
- { "value": "HU", "label": "Hungary" },
- { "value": "IS", "label": "Iceland" },
- { "value": "IN", "label": "India" },
- { "value": "ID", "label": "Indonesia" },
- { "value": "IR", "label": "Iran, Islamic Republic of" },
- { "value": "IQ", "label": "Iraq" },
- { "value": "IE", "label": "Ireland" },
- { "value": "IM", "label": "Isle of Man" },
- { "value": "IL", "label": "Israel" },
- { "value": "IT", "label": "Italy" },
- { "value": "JM", "label": "Jamaica" },
- { "value": "JP", "label": "Japan" },
- { "value": "JE", "label": "Jersey" },
- { "value": "JO", "label": "Jordan" },
- { "value": "KZ", "label": "Kazakhstan" },
- { "value": "KE", "label": "Kenya" },
- { "value": "KI", "label": "Kiribati" },
- { "value": "KP", "label": "Korea, Democratic People's Republic of" },
- { "value": "KR", "label": "Korea, Republic of" },
- { "value": "XK", "label": "Kosovo" },
- { "value": "KW", "label": "Kuwait" },
- { "value": "KG", "label": "Kyrgyzstan" },
- { "value": "LA", "label": "Lao People's Democratic Republic" },
- { "value": "LV", "label": "Latvia" },
- { "value": "LB", "label": "Lebanon" },
- { "value": "LS", "label": "Lesotho" },
- { "value": "LR", "label": "Liberia" },
- { "value": "LY", "label": "Libya" },
- { "value": "LI", "label": "Liechtenstein" },
- { "value": "LT", "label": "Lithuania" },
- { "value": "LU", "label": "Luxembourg" },
- { "value": "MO", "label": "Macao" },
- { "value": "MK", "label": "Macedonia, the Former Yugoslav Republic of" },
- { "value": "MG", "label": "Madagascar" },
- { "value": "MW", "label": "Malawi" },
- { "value": "MY", "label": "Malaysia" },
- { "value": "MV", "label": "Maldives" },
- { "value": "ML", "label": "Mali" },
- { "value": "MT", "label": "Malta" },
- { "value": "MH", "label": "Marshall Islands" },
- { "value": "MQ", "label": "Martinique" },
- { "value": "MR", "label": "Mauritania" },
- { "value": "MU", "label": "Mauritius" },
- { "value": "YT", "label": "Mayotte" },
- { "value": "MX", "label": "Mexico" },
- { "value": "FM", "label": "Micronesia, Federated States of" },
- { "value": "MD", "label": "Moldova, Republic of" },
- { "value": "MC", "label": "Monaco" },
- { "value": "MN", "label": "Mongolia" },
- { "value": "ME", "label": "Montenegro" },
- { "value": "MS", "label": "Montserrat" },
- { "value": "MA", "label": "Morocco" },
- { "value": "MZ", "label": "Mozambique" },
- { "value": "MM", "label": "Myanmar" },
- { "value": "NA", "label": "Namibia" },
- { "value": "NR", "label": "Nauru" },
- { "value": "NP", "label": "Nepal" },
- { "value": "NL", "label": "Netherlands" },
- { "value": "NC", "label": "New Caledonia" },
- { "value": "NZ", "label": "New Zealand" },
- { "value": "NI", "label": "Nicaragua" },
- { "value": "NE", "label": "Niger" },
- { "value": "NG", "label": "Nigeria" },
- { "value": "NU", "label": "Niue" },
- { "value": "NF", "label": "Norfolk Island" },
- { "value": "MP", "label": "Northern Mariana Islands" },
- { "value": "NO", "label": "Norway" },
- { "value": "OM", "label": "Oman" },
- { "value": "PK", "label": "Pakistan" },
- { "value": "PW", "label": "Palau" },
- { "value": "PS", "label": "Palestine, State of" },
- { "value": "PA", "label": "Panama" },
- { "value": "PG", "label": "Papua New Guinea" },
- { "value": "PY", "label": "Paraguay" },
- { "value": "PE", "label": "Peru" },
- { "value": "PH", "label": "Philippines" },
- { "value": "PN", "label": "Pitcairn" },
- { "value": "PL", "label": "Poland" },
- { "value": "PT", "label": "Portugal" },
- { "value": "PR", "label": "Puerto Rico" },
- { "value": "QA", "label": "Qatar" },
- { "value": "RE", "label": "R\u00e9union" },
- { "value": "RO", "label": "Romania" },
- { "value": "RU", "label": "Russian Federation" },
- { "value": "RW", "label": "Rwanda" },
- { "value": "BL", "label": "Saint Barth\u00e9lemy" },
- { "value": "SH", "label": "Saint Helena, Ascension and Tristan da Cunha" },
- { "value": "KN", "label": "Saint Kitts and Nevis" },
- { "value": "LC", "label": "Saint Lucia" },
- { "value": "MF", "label": "Saint Martin (French part)" },
- { "value": "PM", "label": "Saint Pierre and Miquelon" },
- { "value": "VC", "label": "Saint Vincent and the Grenadines" },
- { "value": "WS", "label": "Samoa" },
- { "value": "SM", "label": "San Marino" },
- { "value": "ST", "label": "Sao Tome and Principe" },
- { "value": "SA", "label": "Saudi Arabia" },
- { "value": "SN", "label": "Senegal" },
- { "value": "RS", "label": "Serbia" },
- { "value": "SC", "label": "Seychelles" },
- { "value": "SL", "label": "Sierra Leone" },
- { "value": "SG", "label": "Singapore" },
- { "value": "SX", "label": "Sint Maarten (Dutch part)" },
- { "value": "SK", "label": "Slovakia" },
- { "value": "SI", "label": "Slovenia" },
- { "value": "SB", "label": "Solomon Islands" },
- { "value": "SO", "label": "Somalia" },
- { "value": "ZA", "label": "South Africa" },
- { "value": "GS", "label": "South Georgia and the South Sandwich Islands" },
- { "value": "SS", "label": "South Sudan" },
- { "value": "ES", "label": "Spain" },
- { "value": "LK", "label": "Sri Lanka" },
- { "value": "SD", "label": "Sudan" },
- { "value": "SR", "label": "Suriname" },
- { "value": "SJ", "label": "Svalbard and Jan Mayen" },
- { "value": "SZ", "label": "Swaziland" },
- { "value": "SE", "label": "Sweden" },
- { "value": "CH", "label": "Switzerland" },
- { "value": "SY", "label": "Syrian Arab Republic" },
- { "value": "TW", "label": "Taiwan, Province of China" },
- { "value": "TJ", "label": "Tajikistan" },
- { "value": "TZ", "label": "Tanzania, United Republic of" },
- { "value": "TH", "label": "Thailand" },
- { "value": "TL", "label": "Timor-Leste" },
- { "value": "TG", "label": "Togo" },
- { "value": "TK", "label": "Tokelau" },
- { "value": "TO", "label": "Tonga" },
- { "value": "TT", "label": "Trinidad and Tobago" },
- { "value": "TN", "label": "Tunisia" },
- { "value": "TR", "label": "Turkey" },
- { "value": "TM", "label": "Turkmenistan" },
- { "value": "TC", "label": "Turks and Caicos Islands" },
- { "value": "TV", "label": "Tuvalu" },
- { "value": "UG", "label": "Uganda" },
- { "value": "UA", "label": "Ukraine" },
- { "value": "AE", "label": "United Arab Emirates" },
- { "value": "GB", "label": "United Kingdom" },
- { "value": "US", "label": "United States" },
- { "value": "UM", "label": "United States Minor Outlying Islands" },
- { "value": "UY", "label": "Uruguay" },
- { "value": "UZ", "label": "Uzbekistan" },
- { "value": "VU", "label": "Vanuatu" },
- { "value": "VE", "label": "Venezuela, Bolivarian Republic of" },
- { "value": "VN", "label": "Viet Nam" },
- { "value": "VG", "label": "Virgin Islands, British" },
- { "value": "VI", "label": "Virgin Islands, U.S." },
- { "value": "WF", "label": "Wallis and Futuna" },
- { "value": "EH", "label": "Western Sahara" },
- { "value": "YE", "label": "Yemen" },
- { "value": "ZM", "label": "Zambia" },
- { "value": "ZW", "label": "Zimbabwe" }
+ {
+ "value": "AF",
+ "label": "Afghanistan"
+ },
+ {
+ "value": "AX",
+ "label": "\u00c5land Islands"
+ },
+ {
+ "value": "AL",
+ "label": "Albania"
+ },
+ {
+ "value": "DZ",
+ "label": "Algeria"
+ },
+ {
+ "value": "AS",
+ "label": "American Samoa"
+ },
+ {
+ "value": "AD",
+ "label": "Andorra"
+ },
+ {
+ "value": "AO",
+ "label": "Angola"
+ },
+ {
+ "value": "AI",
+ "label": "Anguilla"
+ },
+ {
+ "value": "AQ",
+ "label": "Antarctica"
+ },
+ {
+ "value": "AG",
+ "label": "Antigua and Barbuda"
+ },
+ {
+ "value": "AR",
+ "label": "Argentina"
+ },
+ {
+ "value": "AM",
+ "label": "Armenia"
+ },
+ {
+ "value": "AW",
+ "label": "Aruba"
+ },
+ {
+ "value": "AC",
+ "label": "Ascension Island"
+ },
+ {
+ "value": "AU",
+ "label": "Australia"
+ },
+ {
+ "value": "AT",
+ "label": "Austria"
+ },
+ {
+ "value": "AZ",
+ "label": "Azerbaijan"
+ },
+ {
+ "value": "BS",
+ "label": "Bahamas"
+ },
+ {
+ "value": "BH",
+ "label": "Bahrain"
+ },
+ {
+ "value": "BD",
+ "label": "Bangladesh"
+ },
+ {
+ "value": "BB",
+ "label": "Barbados"
+ },
+ {
+ "value": "BY",
+ "label": "Belarus"
+ },
+ {
+ "value": "BE",
+ "label": "Belgium"
+ },
+ {
+ "value": "BZ",
+ "label": "Belize"
+ },
+ {
+ "value": "BJ",
+ "label": "Benin"
+ },
+ {
+ "value": "BM",
+ "label": "Bermuda"
+ },
+ {
+ "value": "BT",
+ "label": "Bhutan"
+ },
+ {
+ "value": "BO",
+ "label": "Bolivia, Plurinational State of"
+ },
+ {
+ "value": "BQ",
+ "label": "Bonaire, Sint Eustatius and Saba"
+ },
+ {
+ "value": "BA",
+ "label": "Bosnia and Herzegovina"
+ },
+ {
+ "value": "BW",
+ "label": "Botswana"
+ },
+ {
+ "value": "BV",
+ "label": "Bouvet Island"
+ },
+ {
+ "value": "BR",
+ "label": "Brazil"
+ },
+ {
+ "value": "IO",
+ "label": "British Indian Ocean Territory"
+ },
+ {
+ "value": "BN",
+ "label": "Brunei Darussalam"
+ },
+ {
+ "value": "BG",
+ "label": "Bulgaria"
+ },
+ {
+ "value": "BF",
+ "label": "Burkina Faso"
+ },
+ {
+ "value": "BI",
+ "label": "Burundi"
+ },
+ {
+ "value": "KH",
+ "label": "Cambodia"
+ },
+ {
+ "value": "CM",
+ "label": "Cameroon"
+ },
+ {
+ "value": "CA",
+ "label": "Canada"
+ },
+ {
+ "value": "CV",
+ "label": "Cape Verde"
+ },
+ {
+ "value": "KY",
+ "label": "Cayman Islands"
+ },
+ {
+ "value": "CF",
+ "label": "Central African Republic"
+ },
+ {
+ "value": "TD",
+ "label": "Chad"
+ },
+ {
+ "value": "CL",
+ "label": "Chile"
+ },
+ {
+ "value": "CN",
+ "label": "China"
+ },
+ {
+ "value": "CX",
+ "label": "Christmas Island"
+ },
+ {
+ "value": "CC",
+ "label": "Cocos (Keeling) Islands"
+ },
+ {
+ "value": "CO",
+ "label": "Colombia"
+ },
+ {
+ "value": "KM",
+ "label": "Comoros"
+ },
+ {
+ "value": "CG",
+ "label": "Congo"
+ },
+ {
+ "value": "CD",
+ "label": "Congo, the Democratic Republic of the"
+ },
+ {
+ "value": "CK",
+ "label": "Cook Islands"
+ },
+ {
+ "value": "CR",
+ "label": "Costa Rica"
+ },
+ {
+ "value": "CI",
+ "label": "C\u00f4te d'Ivoire"
+ },
+ {
+ "value": "HR",
+ "label": "Croatia"
+ },
+ {
+ "value": "CU",
+ "label": "Cuba"
+ },
+ {
+ "value": "CW",
+ "label": "Cura\u00e7ao"
+ },
+ {
+ "value": "CY",
+ "label": "Cyprus"
+ },
+ {
+ "value": "CZ",
+ "label": "Czech Republic"
+ },
+ {
+ "value": "DK",
+ "label": "Denmark"
+ },
+ {
+ "value": "DG",
+ "label": "Diego Garcia"
+ },
+ {
+ "value": "DJ",
+ "label": "Djibouti"
+ },
+ {
+ "value": "DM",
+ "label": "Dominica"
+ },
+ {
+ "value": "DO",
+ "label": "Dominican Republic"
+ },
+ {
+ "value": "EC",
+ "label": "Ecuador"
+ },
+ {
+ "value": "EG",
+ "label": "Egypt"
+ },
+ {
+ "value": "SV",
+ "label": "El Salvador"
+ },
+ {
+ "value": "GQ",
+ "label": "Equatorial Guinea"
+ },
+ {
+ "value": "ER",
+ "label": "Eritrea"
+ },
+ {
+ "value": "EE",
+ "label": "Estonia"
+ },
+ {
+ "value": "ET",
+ "label": "Ethiopia"
+ },
+ {
+ "value": "FK",
+ "label": "Falkland Islands (Malvinas)"
+ },
+ {
+ "value": "FO",
+ "label": "Faroe Islands"
+ },
+ {
+ "value": "FJ",
+ "label": "Fiji"
+ },
+ {
+ "value": "FI",
+ "label": "Finland"
+ },
+ {
+ "value": "FR",
+ "label": "France"
+ },
+ {
+ "value": "GF",
+ "label": "French Guiana"
+ },
+ {
+ "value": "PF",
+ "label": "French Polynesia"
+ },
+ {
+ "value": "TF",
+ "label": "French Southern Territories"
+ },
+ {
+ "value": "GA",
+ "label": "Gabon"
+ },
+ {
+ "value": "GM",
+ "label": "Gambia"
+ },
+ {
+ "value": "GE",
+ "label": "Georgia"
+ },
+ {
+ "value": "DE",
+ "label": "Germany"
+ },
+ {
+ "value": "GH",
+ "label": "Ghana"
+ },
+ {
+ "value": "GI",
+ "label": "Gibraltar"
+ },
+ {
+ "value": "GR",
+ "label": "Greece"
+ },
+ {
+ "value": "GL",
+ "label": "Greenland"
+ },
+ {
+ "value": "GD",
+ "label": "Grenada"
+ },
+ {
+ "value": "GP",
+ "label": "Guadeloupe"
+ },
+ {
+ "value": "GU",
+ "label": "Guam"
+ },
+ {
+ "value": "GT",
+ "label": "Guatemala"
+ },
+ {
+ "value": "GG",
+ "label": "Guernsey"
+ },
+ {
+ "value": "GN",
+ "label": "Guinea"
+ },
+ {
+ "value": "GW",
+ "label": "Guinea-Bissau"
+ },
+ {
+ "value": "GY",
+ "label": "Guyana"
+ },
+ {
+ "value": "HT",
+ "label": "Haiti"
+ },
+ {
+ "value": "HM",
+ "label": "Heard Island and McDonald Islands"
+ },
+ {
+ "value": "VA",
+ "label": "Holy See (Vatican City State)"
+ },
+ {
+ "value": "HN",
+ "label": "Honduras"
+ },
+ {
+ "value": "HK",
+ "label": "Hong Kong"
+ },
+ {
+ "value": "HU",
+ "label": "Hungary"
+ },
+ {
+ "value": "IS",
+ "label": "Iceland"
+ },
+ {
+ "value": "IN",
+ "label": "India"
+ },
+ {
+ "value": "ID",
+ "label": "Indonesia"
+ },
+ {
+ "value": "IR",
+ "label": "Iran, Islamic Republic of"
+ },
+ {
+ "value": "IQ",
+ "label": "Iraq"
+ },
+ {
+ "value": "IE",
+ "label": "Ireland"
+ },
+ {
+ "value": "IM",
+ "label": "Isle of Man"
+ },
+ {
+ "value": "IL",
+ "label": "Israel"
+ },
+ {
+ "value": "IT",
+ "label": "Italy"
+ },
+ {
+ "value": "JM",
+ "label": "Jamaica"
+ },
+ {
+ "value": "JP",
+ "label": "Japan"
+ },
+ {
+ "value": "JE",
+ "label": "Jersey"
+ },
+ {
+ "value": "JO",
+ "label": "Jordan"
+ },
+ {
+ "value": "KZ",
+ "label": "Kazakhstan"
+ },
+ {
+ "value": "KE",
+ "label": "Kenya"
+ },
+ {
+ "value": "KI",
+ "label": "Kiribati"
+ },
+ {
+ "value": "KP",
+ "label": "Korea, Democratic People's Republic of"
+ },
+ {
+ "value": "KR",
+ "label": "Korea, Republic of"
+ },
+ {
+ "value": "XK",
+ "label": "Kosovo"
+ },
+ {
+ "value": "KW",
+ "label": "Kuwait"
+ },
+ {
+ "value": "KG",
+ "label": "Kyrgyzstan"
+ },
+ {
+ "value": "LA",
+ "label": "Lao People's Democratic Republic"
+ },
+ {
+ "value": "LV",
+ "label": "Latvia"
+ },
+ {
+ "value": "LB",
+ "label": "Lebanon"
+ },
+ {
+ "value": "LS",
+ "label": "Lesotho"
+ },
+ {
+ "value": "LR",
+ "label": "Liberia"
+ },
+ {
+ "value": "LY",
+ "label": "Libya"
+ },
+ {
+ "value": "LI",
+ "label": "Liechtenstein"
+ },
+ {
+ "value": "LT",
+ "label": "Lithuania"
+ },
+ {
+ "value": "LU",
+ "label": "Luxembourg"
+ },
+ {
+ "value": "MO",
+ "label": "Macao"
+ },
+ {
+ "value": "MK",
+ "label": "Macedonia, the Former Yugoslav Republic of"
+ },
+ {
+ "value": "MG",
+ "label": "Madagascar"
+ },
+ {
+ "value": "MW",
+ "label": "Malawi"
+ },
+ {
+ "value": "MY",
+ "label": "Malaysia"
+ },
+ {
+ "value": "MV",
+ "label": "Maldives"
+ },
+ {
+ "value": "ML",
+ "label": "Mali"
+ },
+ {
+ "value": "MT",
+ "label": "Malta"
+ },
+ {
+ "value": "MH",
+ "label": "Marshall Islands"
+ },
+ {
+ "value": "MQ",
+ "label": "Martinique"
+ },
+ {
+ "value": "MR",
+ "label": "Mauritania"
+ },
+ {
+ "value": "MU",
+ "label": "Mauritius"
+ },
+ {
+ "value": "YT",
+ "label": "Mayotte"
+ },
+ {
+ "value": "MX",
+ "label": "Mexico"
+ },
+ {
+ "value": "FM",
+ "label": "Micronesia, Federated States of"
+ },
+ {
+ "value": "MD",
+ "label": "Moldova, Republic of"
+ },
+ {
+ "value": "MC",
+ "label": "Monaco"
+ },
+ {
+ "value": "MN",
+ "label": "Mongolia"
+ },
+ {
+ "value": "ME",
+ "label": "Montenegro"
+ },
+ {
+ "value": "MS",
+ "label": "Montserrat"
+ },
+ {
+ "value": "MA",
+ "label": "Morocco"
+ },
+ {
+ "value": "MZ",
+ "label": "Mozambique"
+ },
+ {
+ "value": "MM",
+ "label": "Myanmar"
+ },
+ {
+ "value": "NA",
+ "label": "Namibia"
+ },
+ {
+ "value": "NR",
+ "label": "Nauru"
+ },
+ {
+ "value": "NP",
+ "label": "Nepal"
+ },
+ {
+ "value": "NL",
+ "label": "Netherlands"
+ },
+ {
+ "value": "NC",
+ "label": "New Caledonia"
+ },
+ {
+ "value": "NZ",
+ "label": "New Zealand"
+ },
+ {
+ "value": "NI",
+ "label": "Nicaragua"
+ },
+ {
+ "value": "NE",
+ "label": "Niger"
+ },
+ {
+ "value": "NG",
+ "label": "Nigeria"
+ },
+ {
+ "value": "NU",
+ "label": "Niue"
+ },
+ {
+ "value": "NF",
+ "label": "Norfolk Island"
+ },
+ {
+ "value": "MP",
+ "label": "Northern Mariana Islands"
+ },
+ {
+ "value": "NO",
+ "label": "Norway"
+ },
+ {
+ "value": "OM",
+ "label": "Oman"
+ },
+ {
+ "value": "PK",
+ "label": "Pakistan"
+ },
+ {
+ "value": "PW",
+ "label": "Palau"
+ },
+ {
+ "value": "PS",
+ "label": "Palestine, State of"
+ },
+ {
+ "value": "PA",
+ "label": "Panama"
+ },
+ {
+ "value": "PG",
+ "label": "Papua New Guinea"
+ },
+ {
+ "value": "PY",
+ "label": "Paraguay"
+ },
+ {
+ "value": "PE",
+ "label": "Peru"
+ },
+ {
+ "value": "PH",
+ "label": "Philippines"
+ },
+ {
+ "value": "PN",
+ "label": "Pitcairn"
+ },
+ {
+ "value": "PL",
+ "label": "Poland"
+ },
+ {
+ "value": "PT",
+ "label": "Portugal"
+ },
+ {
+ "value": "PR",
+ "label": "Puerto Rico"
+ },
+ {
+ "value": "QA",
+ "label": "Qatar"
+ },
+ {
+ "value": "RE",
+ "label": "R\u00e9union"
+ },
+ {
+ "value": "RO",
+ "label": "Romania"
+ },
+ {
+ "value": "RU",
+ "label": "Russian Federation"
+ },
+ {
+ "value": "RW",
+ "label": "Rwanda"
+ },
+ {
+ "value": "BL",
+ "label": "Saint Barth\u00e9lemy"
+ },
+ {
+ "value": "SH",
+ "label": "Saint Helena, Ascension and Tristan da Cunha"
+ },
+ {
+ "value": "KN",
+ "label": "Saint Kitts and Nevis"
+ },
+ {
+ "value": "LC",
+ "label": "Saint Lucia"
+ },
+ {
+ "value": "MF",
+ "label": "Saint Martin (French part)"
+ },
+ {
+ "value": "PM",
+ "label": "Saint Pierre and Miquelon"
+ },
+ {
+ "value": "VC",
+ "label": "Saint Vincent and the Grenadines"
+ },
+ {
+ "value": "WS",
+ "label": "Samoa"
+ },
+ {
+ "value": "SM",
+ "label": "San Marino"
+ },
+ {
+ "value": "ST",
+ "label": "Sao Tome and Principe"
+ },
+ {
+ "value": "SA",
+ "label": "Saudi Arabia"
+ },
+ {
+ "value": "SN",
+ "label": "Senegal"
+ },
+ {
+ "value": "RS",
+ "label": "Serbia"
+ },
+ {
+ "value": "SC",
+ "label": "Seychelles"
+ },
+ {
+ "value": "SL",
+ "label": "Sierra Leone"
+ },
+ {
+ "value": "SG",
+ "label": "Singapore"
+ },
+ {
+ "value": "SX",
+ "label": "Sint Maarten (Dutch part)"
+ },
+ {
+ "value": "SK",
+ "label": "Slovakia"
+ },
+ {
+ "value": "SI",
+ "label": "Slovenia"
+ },
+ {
+ "value": "SB",
+ "label": "Solomon Islands"
+ },
+ {
+ "value": "SO",
+ "label": "Somalia"
+ },
+ {
+ "value": "ZA",
+ "label": "South Africa"
+ },
+ {
+ "value": "GS",
+ "label": "South Georgia and the South Sandwich Islands"
+ },
+ {
+ "value": "SS",
+ "label": "South Sudan"
+ },
+ {
+ "value": "ES",
+ "label": "Spain"
+ },
+ {
+ "value": "LK",
+ "label": "Sri Lanka"
+ },
+ {
+ "value": "SD",
+ "label": "Sudan"
+ },
+ {
+ "value": "SR",
+ "label": "Suriname"
+ },
+ {
+ "value": "SJ",
+ "label": "Svalbard and Jan Mayen"
+ },
+ {
+ "value": "SZ",
+ "label": "Swaziland"
+ },
+ {
+ "value": "SE",
+ "label": "Sweden"
+ },
+ {
+ "value": "CH",
+ "label": "Switzerland"
+ },
+ {
+ "value": "SY",
+ "label": "Syrian Arab Republic"
+ },
+ {
+ "value": "TW",
+ "label": "Taiwan, Province of China"
+ },
+ {
+ "value": "TJ",
+ "label": "Tajikistan"
+ },
+ {
+ "value": "TZ",
+ "label": "Tanzania, United Republic of"
+ },
+ {
+ "value": "TH",
+ "label": "Thailand"
+ },
+ {
+ "value": "TL",
+ "label": "Timor-Leste"
+ },
+ {
+ "value": "TG",
+ "label": "Togo"
+ },
+ {
+ "value": "TK",
+ "label": "Tokelau"
+ },
+ {
+ "value": "TO",
+ "label": "Tonga"
+ },
+ {
+ "value": "TT",
+ "label": "Trinidad and Tobago"
+ },
+ {
+ "value": "TN",
+ "label": "Tunisia"
+ },
+ {
+ "value": "TR",
+ "label": "Turkey"
+ },
+ {
+ "value": "TM",
+ "label": "Turkmenistan"
+ },
+ {
+ "value": "TC",
+ "label": "Turks and Caicos Islands"
+ },
+ {
+ "value": "TV",
+ "label": "Tuvalu"
+ },
+ {
+ "value": "UG",
+ "label": "Uganda"
+ },
+ {
+ "value": "UA",
+ "label": "Ukraine"
+ },
+ {
+ "value": "AE",
+ "label": "United Arab Emirates"
+ },
+ {
+ "value": "GB",
+ "label": "United Kingdom"
+ },
+ {
+ "value": "US",
+ "label": "United States"
+ },
+ {
+ "value": "UM",
+ "label": "United States Minor Outlying Islands"
+ },
+ {
+ "value": "UY",
+ "label": "Uruguay"
+ },
+ {
+ "value": "UZ",
+ "label": "Uzbekistan"
+ },
+ {
+ "value": "VU",
+ "label": "Vanuatu"
+ },
+ {
+ "value": "VE",
+ "label": "Venezuela, Bolivarian Republic of"
+ },
+ {
+ "value": "VN",
+ "label": "Viet Nam"
+ },
+ {
+ "value": "VG",
+ "label": "Virgin Islands, British"
+ },
+ {
+ "value": "VI",
+ "label": "Virgin Islands, U.S."
+ },
+ {
+ "value": "WF",
+ "label": "Wallis and Futuna"
+ },
+ {
+ "value": "EH",
+ "label": "Western Sahara"
+ },
+ {
+ "value": "YE",
+ "label": "Yemen"
+ },
+ {
+ "value": "ZM",
+ "label": "Zambia"
+ },
+ {
+ "value": "ZW",
+ "label": "Zimbabwe"
+ }
]
-}
+}
\ No newline at end of file
diff --git a/src/data/Extensions.json b/src/data/Extensions.json
index 3c61cbbba574..3009704f99cd 100644
--- a/src/data/Extensions.json
+++ b/src/data/Extensions.json
@@ -29,8 +29,8 @@
"mappingRequired": true,
"links": [
{
- "name": "Sherweb Cloud Services for MSPs",
- "url": "https://info.sherweb.com/sherweb-cloud-services-for-msps"
+ "name": "Sherweb CIPP Integration",
+ "url": "https://info.sherweb.com/sherweb-cipp-integration"
}
],
"SettingOptions": [
@@ -81,12 +81,12 @@
{
"type": "autoComplete",
"name": "Sherweb.AllowedCustomRoles",
- "label": "Select custom roles that are allowed to purchase licenses",
+ "label": "Select CIPP roles that are allowed to purchase licenses",
"api": {
"url": "/api/ListCustomRole",
"queryKey": "CustomRoles",
- "labelField": "RowKey",
- "valueField": "RowKey"
+ "labelField": "RoleName",
+ "valueField": "RoleName"
},
"multiple": true,
"condition": {
@@ -334,10 +334,37 @@
"action": "disable"
}
},
+ {
+ "type": "textField",
+ "name": "HaloPSA.ClientID",
+ "label": "HaloPSA Client ID",
+ "placeholder": "Enter your HaloPSA Client ID",
+ "required": true,
+ "condition": {
+ "field": "HaloPSA.Enabled",
+ "compareType": "is",
+ "compareValue": true,
+ "action": "disable"
+ }
+ },
+ {
+ "type": "password",
+ "name": "HaloPSA.APIKey",
+ "label": "HaloPSA Client Secret",
+ "placeholder": "Enter your client Secret. Leave blank to keep your current key.",
+ "required": true,
+ "condition": {
+ "field": "HaloPSA.Enabled",
+ "compareType": "is",
+ "compareValue": true,
+ "action": "disable"
+ }
+ },
{
"type": "autoComplete",
"name": "HaloPSA.TicketType",
- "label": "Select your HaloPSA Ticket Type, leave blank for default.",
+ "label": "HaloPSA Ticket Type",
+ "placeholder": "Select your HaloPSA Ticket Type, leave blank for default",
"multiple": false,
"api": {
"url": "/api/ExecExtensionMapping",
@@ -358,26 +385,24 @@
}
},
{
- "type": "textField",
- "name": "HaloPSA.ClientID",
- "label": "HaloPSA Client ID",
- "placeholder": "Enter your HaloPSA Client ID",
- "required": true,
- "condition": {
- "field": "HaloPSA.Enabled",
- "compareType": "is",
- "compareValue": true,
- "action": "disable"
- }
- },
- {
- "type": "password",
- "name": "HaloPSA.APIKey",
- "label": "HaloPSA Client Secret",
- "placeholder": "Enter your client Secret. Leave blank to keep your current key.",
- "required": true,
+ "type": "autoComplete",
+ "name": "HaloPSA.Outcome",
+ "label": "HaloPSA Outcome",
+ "placeholder": "Select your HaloPSA Outcome, leave blank for default",
+ "multiple": false,
+ "api": {
+ "url": "/api/ExecExtensionMapping",
+ "data": {
+ "List": "HaloPSAFields"
+ },
+ "queryKey": "HaloOutcomes",
+ "dataKey": "Outcomes",
+ "labelField": "buttonname",
+ "valueField": "id",
+ "showRefresh": true
+ },
"condition": {
- "field": "HaloPSA.Enabled",
+ "field": "HaloPSA.ConsolidateTickets",
"compareType": "is",
"compareValue": true,
"action": "disable"
@@ -599,6 +624,18 @@
"compareType": "is",
"compareValue": true
}
+ },
+ {
+ "type": "datePicker",
+ "name": "Hudu.NextSync",
+ "label": "Reschedule next sync date",
+ "helperText": "Set a future date to delay the next scheduled sync. Leave blank to sync during the next scheduled sync.",
+ "condition": {
+ "field": "Hudu.Enabled",
+ "compareType": "is",
+ "compareValue": true,
+ "action": "disable"
+ }
}
],
"mappingRequired": true,
@@ -841,7 +878,7 @@
"logo": "/assets/integrations/github.png",
"logoDark": "/assets/integrations/github_dark.png",
"description": "Enable the GitHub integration to manage your repositories from CIPP.",
- "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositorities functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.",
+ "helpText": "This integration allows you to manage GitHub repositories from CIPP, including the Community Repositories functionality. Requires a GitHub Personal Access Token (PAT) with a minimum of repo:public_repo permissions. If you plan on saving your templates to GitHub or accessing private/internal repositories, you will need to grant the whole repo scope. You can create a PAT in your GitHub account settings, see the GitHub Token documentation for more info. If you do not enable the extension, a read-only API will be provided.",
"links": [
{
"name": "GitHub Token",
diff --git a/src/data/GDAPRoles.json b/src/data/GDAPRoles.json
index 22553236b533..1d3ca9b38094 100644
--- a/src/data/GDAPRoles.json
+++ b/src/data/GDAPRoles.json
@@ -63,6 +63,22 @@
"Name": "Attribute Definition Reader",
"ObjectId": "1d336d2c-4ae8-42ef-9711-b3604ce3fc2c"
},
+ {
+ "ExtensionData": {},
+ "Description": "Read audit logs and configure diagnostic settings for events related to custom security attributes.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Attribute Log Administrator",
+ "ObjectId": "5b784334-f94b-471a-a387-e7219fc49ca2"
+ },
+ {
+ "ExtensionData": {},
+ "Description": "Read audit logs related to custom security attributes.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Attribute Log Reader",
+ "ObjectId": "9c99539d-8186-4804-835f-fd51ef9e2dcd"
+ },
{
"ExtensionData": {},
"Description": "Allowed to view, set and reset authentication method information for any non-admin user.",
@@ -79,6 +95,14 @@
"Name": "Authentication Policy Administrator",
"ObjectId": "0526716b-113d-4c15-b2c8-68e3c22b9f80"
},
+ {
+ "ExtensionData": {},
+ "Description": "Customize sign in and sign up experiences for users by creating and managing custom authentication extensions.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Authentication Extensibility Administrator",
+ "ObjectId": "25a516ed-2fa0-40ea-a2d0-12923a21473a"
+ },
{
"ExtensionData": {},
"Description": "Users assigned to this role are added to the local administrators group on Azure AD-joined devices.",
@@ -255,6 +279,14 @@
"Name": "Dynamics 365 Administrator",
"ObjectId": "44367163-eba1-44c3-98af-f5787879f96a"
},
+ {
+ "ExtensionData": {},
+ "Description": "Access and perform all administrative tasks on Dynamics 365 Business Central environments.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Dynamics 365 Business Central Administrator",
+ "ObjectId": "963797fb-eb3b-4cde-8ce3-5878b3f32a3f"
+ },
{
"ExtensionData": {},
"Description": "Manage all aspects of Microsoft Edge.",
@@ -311,6 +343,14 @@
"Name": "Global Reader",
"ObjectId": "f2ef992c-3afb-46b9-b7cf-a126ee74c451"
},
+ {
+ "ExtensionData": {},
+ "Description": "Create and manage all aspects of Microsoft Entra Internet Access and Microsoft Entra Private Access, including managing access to public and private endpoints.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Global Secure Access Administrator",
+ "ObjectId": "ac434307-12b9-4fa1-a708-88bf58caabc1"
+ },
{
"ExtensionData": {},
"Description": "Members of this role can create/manage groups, create/manage groups settings like naming and expiration policies, and view groups activity and audit reports.",
@@ -439,6 +479,30 @@
"Name": "Message Center Reader",
"ObjectId": "790c1fb9-7f7d-4f88-86a1-ef1f95c05c1b"
},
+ {
+ "ExtensionData": {},
+ "Description": "Perform all migration functionality to migrate content to Microsoft 365 using Migration Manager.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Microsoft 365 Migration Administrator",
+ "ObjectId": "8c8b803f-96e1-4129-9349-20738d9f9652"
+ },
+ {
+ "ExtensionData": {},
+ "Description": "Create and manage all aspects warranty claims and entitlements for Microsoft manufactured hardware, like Surface and HoloLens.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Microsoft Hardware Warranty Administrator",
+ "ObjectId": "1501b917-7653-4ff9-a4b5-203eaf33784f"
+ },
+ {
+ "ExtensionData": {},
+ "Description": "Create and read warranty claims for Microsoft manufactured hardware, like Surface and HoloLens.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Microsoft Hardware Warranty Specialist",
+ "ObjectId": "281fe777-fb20-4fbb-b7a3-ccebce5b0d96"
+ },
{
"ExtensionData": {},
"Description": "Can manage network locations and review enterprise network design insights for Microsoft 365 Software as a Service applications.",
@@ -455,6 +519,14 @@
"Name": "Office Apps Administrator",
"ObjectId": "2b745bdf-0803-4d80-aa65-822c4493daac"
},
+ {
+ "ExtensionData": {},
+ "Description": "Write, publish, manage, and review the organizational messages for end-users through Microsoft product surfaces.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Organizational Messages Writer",
+ "ObjectId": "507f53e4-4e52-4077-abd3-d2e1558b6ea2"
+ },
{
"ExtensionData": {},
"Description": "Can reset passwords for non-administrators and Password Administrators.",
@@ -583,6 +655,14 @@
"Name": "SharePoint Administrator",
"ObjectId": "f28a1f50-f6e7-4571-818b-6a12f2af6b6c"
},
+ {
+ "ExtensionData": {},
+ "Description": "Manage all aspects of SharePoint Embedded containers.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "SharePoint Embedded Administrator",
+ "ObjectId": "1a7d78b6-429f-476b-8eb-35fb715fffd4"
+ },
{
"ExtensionData": {},
"Description": "Can manage all aspects of the Skype for Business product.",
@@ -631,6 +711,22 @@
"Name": "Teams Devices Administrator",
"ObjectId": "3d762c5a-1b6c-493f-843e-55a3b42923d4"
},
+ {
+ "ExtensionData": {},
+ "Description": "Manage voice and telephony features and troubleshoot communication issues within the Microsoft Teams service.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Teams Telephony Administrator",
+ "ObjectId": "aa38014f-0993-46e9-9b45-30501a20909d"
+ },
+ {
+ "ExtensionData": {},
+ "Description": "Create new Microsoft Entra or Azure AD B2C tenants.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "Tenant Creator",
+ "ObjectId": "112ca1a2-15ad-4102-995e-45b0bc479a6a"
+ },
{
"ExtensionData": {},
"Description": "Can see only tenant level aggregates in Microsoft 365 Usage Analytics and Productivity Score.",
@@ -647,6 +743,14 @@
"Name": "User Administrator",
"ObjectId": "fe930be7-5e62-47db-91af-98c3a49a38b1"
},
+ {
+ "ExtensionData": {},
+ "Description": "View product feedback, survey results, and reports to find training and communication opportunities.",
+ "IsEnabled": true,
+ "IsSystem": true,
+ "Name": "User Experience Success Manager",
+ "ObjectId": "27460883-1df1-4691-b032-3b79643e5e63"
+ },
{
"ExtensionData": {},
"Description": "Manage and share Virtual Visits information and metrics from admin centers or the Virtual Visits app.",
diff --git a/src/data/GraphExplorerPresets.json b/src/data/GraphExplorerPresets.json
index f5c549347713..a6939606d285 100644
--- a/src/data/GraphExplorerPresets.json
+++ b/src/data/GraphExplorerPresets.json
@@ -34,7 +34,7 @@
"id": "e7fdc49a-72a9-4a70-9dbf-a74152495d80",
"params": {
"endpoint": "/devices",
- "$select": "deviceId,DisplayName,profileType,registrationDateTime,trustType",
+ "$select": "deviceId,displayName,profileType,registrationDateTime,trustType",
"$filter": ""
},
"isBuiltin": true
@@ -44,7 +44,7 @@
"id": "f1844e3d-cb3e-4611-9bab-f5f42169bcd0",
"params": {
"endpoint": "/contacts",
- "$select": "CompanyName,DisplayName,Mail,ProxyAddresses",
+ "$select": "companyName,displayName,mail,proxyAddresses",
"$filter": ""
},
"isBuiltin": true
@@ -151,5 +151,23 @@
"NoPagination": true
},
"isBuiltin": true
+ },
+ {
+ "name": "User Report with Sign in Activity (Entra ID P1+)",
+ "id": "7a44a7cf-bf40-4a8a-8eaa-ffd8203f3216",
+ "params": {
+ "endpoint": "users",
+ "$select": "id,displayName,userPrincipalName,accountEnabled,mail,proxyAddresses,createdDateTime,signInActivity.lastSuccessfulSignInDateTime"
+ },
+ "isBuiltin": true
+ },
+ {
+ "name": "Users with Mailbox Type (Custom Data)",
+ "id": "f632d3b8-29f3-470c-8483-54cb88004674",
+ "params": {
+ "endpoint": "users",
+ "$select": "id,displayName,userPrincipalName,accountEnabled,mail,proxyAddresses,createdDateTime,%cippuserschema%.mailboxType"
+ },
+ "isBuiltin": true
}
-]
+]
\ No newline at end of file
diff --git a/src/data/M365Licenses-additional.json b/src/data/M365Licenses-additional.json
new file mode 100644
index 000000000000..09733867f764
--- /dev/null
+++ b/src/data/M365Licenses-additional.json
@@ -0,0 +1,250 @@
+[
+ {
+ "Product_Display_Name": "Office 365 Education E3 for Faculty",
+ "String_Id": "ENTERPRISEPACK_FACULTY",
+ "GUID": "e4fa3838-3d01-42df-aa28-5e0a4c68604b",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for Faculty",
+ "String_Id": "STANDARDWOFFPACK_FACULTY",
+ "GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A5 for Faculty",
+ "String_Id": "ENTERPRISEPREMIUM_FACULTY",
+ "GUID": "a4585165-0533-458a-97e3-c400570268c4",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A5 without Audio Conferencing for faculty",
+ "String_Id": "ENTERPRISEPREMIUM_NOPSTNCONF_FACULTY",
+ "GUID": "9a320620-ca3d-4705-a79d-27c135c96e05",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 Education E1 for Faculty",
+ "String_Id": "STANDARDPACK_FACULTY",
+ "GUID": "a19037fc-48b4-4d57-b079-ce44b7832473",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A3 for Faculty",
+ "String_Id": "M365EDU_A3_FACULTY",
+ "GUID": "4b590615-0888-425a-a965-b3bf7789848d",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 without Audio Conferencing for Faculty",
+ "String_Id": "M365EDU_A5_NOPSTNCONF_FACULTY",
+ "GUID": "65200ac3-f927-4407-a3d5-c63562dff461",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 Education for Homeschool for Faculty",
+ "String_Id": "STANDARDWOFFPACK_HOMESCHOOL_FAC",
+ "GUID": "43e691ad-1491-4e8c-8dc9-da6b8262c03b",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for Faculty (for Device)",
+ "String_Id": "STANDARDWOFFPACK_FACULTY_DEVICE",
+ "GUID": "af4e28de-6b52-4fd3-a5f4-6bf708a304d3",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft Teams Rooms Basic for EDU",
+ "String_Id": "Microsoft_Teams_Rooms_Basic_FAC",
+ "GUID": "a4e376bd-c61e-4618-9901-3fc0cb1b88bb",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft Teams Rooms Basic without Audio Conferencing for EDU",
+ "String_Id": "Microsoft_Teams_Rooms_Basic_without_Audio_Conferencing_FAC",
+ "GUID": "7da0ac23-26f8-4d04-8731-9016d9883340",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU",
+ "String_Id": "Microsoft_Teams_Rooms_Pro_FAC",
+ "GUID": "c25e2b36-e161-4946-bef2-69239729f690",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft Teams Rooms Pro without Audio Conferencing for EDU",
+ "String_Id": "Microsoft_Teams_Rooms_Pro_without_Audio_Conferencing_FAC",
+ "GUID": "271f6b1a-de32-4849-bcf4-b79b8a7c2cfe",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 Education E3 for Students",
+ "String_Id": "ENTERPRISEPACK_STUDENT",
+ "GUID": "8fc2205d-4e51-4401-97f0-5c89ef1aafb",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for Students",
+ "String_Id": "STANDARDWOFFPACK_STUDENT",
+ "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A5 for Students",
+ "String_Id": "ENTERPRISEPREMIUM_STUDENT",
+ "GUID": "ee656612-49fa-43e5-b67e-cb1fdf7699df",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A5 without PSTN Conferencing for Students",
+ "String_Id": "ENTERPRISEPREMIUM_NOPSTNCONF_STUDENT",
+ "GUID": "1164451b-e2e5-4c9e-8fa6-e5122d90dbdc",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 Education E1 for Students",
+ "String_Id": "STANDARDPACK_STUDENT",
+ "GUID": "d37ba356-38c5-4c82-90da-3d714f72a382",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A3 for Students",
+ "String_Id": "M365EDU_A3_STUDENT",
+ "GUID": "7cfd9a2b-e110-4c39-bf20-c6a3f36a3121",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A3 for Students use benefits",
+ "String_Id": "M365EDU_A3_STUUSEBNFT",
+ "GUID": "18250162-5d87-4436-a834-d795c15c80f3",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 Student use benefits",
+ "String_Id": "M365EDU_A5_STUUSEBNFT",
+ "GUID": "31d57bc7-3a05-4867-ab53-97a17835a411",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 without Audio Conferencing for Students",
+ "String_Id": "M365EDU_A5_NOPSTNCONF_STUDENT",
+ "GUID": "a25c01ce-bab1-47e9-a6d0-ebe939b99ff9",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 without Audio Conferencing for Students use benefit",
+ "String_Id": "M365EDU_A5_NOPSTNCONF_STUUSEBNFT",
+ "GUID": "81441ae1-0b31-4185-a6c0-32b6b84d419f",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A3 for Students",
+ "String_Id": "ENTERPRISEPACKPLUS_STUDENT",
+ "GUID": "98b6e773-24d4-4c0d-a968-6e787a1f8204",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A3 Student use benefit",
+ "String_Id": "ENTERPRISEPACKPLUS_STUUSEBNFT",
+ "GUID": "476aad1e-7a7f-473c-9d20-35665a5cbd4f",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A5 Student use benefit",
+ "String_Id": "ENTERPRISEPREMIUM_STUUSEBNFT",
+ "GUID": "f6e603f1-1a6d-4d32-a730-34b809cb9731",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A5 without Audio Conferencing for Students use benefit",
+ "String_Id": "ENTERPRISEPREMIUM_NOPSTNCONF_STUUSEBNFT",
+ "GUID": "bc86c9cd-3058-43ba-9972-141678675ac1",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 Education for Homeschool for Students",
+ "String_Id": "STANDARDWOFFPACK_HOMESCHOOL_STU",
+ "GUID": "afbb89a7-db5f-45fb-8af0-1bc5c5015709",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for Students (for Device)",
+ "String_Id": "STANDARDWOFFPACK_STUDENT_DEVICE",
+ "GUID": "160d609e-ab08-4fce-bc1c-ea13321942ac",
+ "Service_Plan_Name": "",
+ "Service_Plan_Id": "",
+ "Service_Plans_Included_Friendly_Names": ""
+ }
+]
diff --git a/src/data/M365Licenses.json b/src/data/M365Licenses.json
index aa7ef62fb439..7f24d98cdf4f 100644
--- a/src/data/M365Licenses.json
+++ b/src/data/M365Licenses.json
@@ -1,4 +1,20 @@
[
+ {
+ "Product_Display_Name": "10-Year Audit Log Retention Add On",
+ "String_Id": "10_ALR_ADDON",
+ "GUID": "c2e41e49-e2a2-4c55-832a-cf13ffba1d6a",
+ "Service_Plan_Name": "Auditing_10Year_ Retention_ Add_On",
+ "Service_Plan_Id": "7d16094b-4db8-41ff-a182-372a90a85407",
+ "Service_Plans_Included_Friendly_Names": "Auditing 10Year Retention Add On"
+ },
+ {
+ "Product_Display_Name": "Advanced Communications",
+ "String_Id": "ADV_COMMS",
+ "GUID": "e4654015-5daf-4a48-9b37-4f309dddd88b",
+ "Service_Plan_Name": "TEAMS_ADVCOMMS",
+ "Service_Plan_Id": "604ec28a-ae18-4bc6-91b0-11da94504ba9",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Advanced Communications"
+ },
{
"Product_Display_Name": "AI Builder Capacity add-on",
"String_Id": "CDSAICAPACITY",
@@ -55,6 +71,30 @@
"Service_Plan_Id": "2e6ffd72-52d1-4541-8f6c-938f9a8d4cdc",
"Service_Plans_Included_Friendly_Names": "Microsoft Application Protection and Governance (D)"
},
+ {
+ "Product_Display_Name": "Azure Information Protection Premium P1 for Government",
+ "String_Id": "RIGHTSMANAGEMENT_CE_GOV\t",
+ "GUID": "78362de1-6942-4bb8-83a1-a32aa67e6e2c",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION_GOV",
+ "Service_Plan_Id": "922ba911-5694-4e99-a794-73aed9bfeec8",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation for Government"
+ },
+ {
+ "Product_Display_Name": "Azure Information Protection Premium P1 for Government",
+ "String_Id": "RIGHTSMANAGEMENT_CE_GOV\t",
+ "GUID": "78362de1-6942-4bb8-83a1-a32aa67e6e2c",
+ "Service_Plan_Name": "RMS_S_PREMIUM_GOV",
+ "Service_Plan_Id": "1b66aedf-8ca1-4f73-af76-ec76c6180f98",
+ "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P1 for GCC"
+ },
+ {
+ "Product_Display_Name": "Azure Information Protection Premium P1 for Government",
+ "String_Id": "RIGHTSMANAGEMENT_CE_GOV\t",
+ "GUID": "78362de1-6942-4bb8-83a1-a32aa67e6e2c",
+ "Service_Plan_Name": "RMS_S_ENTERPRISE_GOV",
+ "Service_Plan_Id": "6a76346d-5d6e-4051-9fe3-ed3f312b5597",
+ "Service_Plans_Included_Friendly_Names": "Azure Rights Management"
+ },
{
"Product_Display_Name": "Career Coach for faculty",
"String_Id": "CAREERCOACH_FACULTY",
@@ -127,6 +167,14 @@
"Service_Plan_Id": "f7e5b77d-f293-410a-bae8-f941f19fe680",
"Service_Plans_Included_Friendly_Names": "OneDrive for Business (Clipchamp)"
},
+ {
+ "Product_Display_Name": "Clipchamp Premium Add-on",
+ "String_Id": "Clipchamp_Premium_Add_on",
+ "GUID": "4b2c20e4-939d-4bf4-9dd8-6870240cfe19",
+ "Service_Plan_Name": "CLIPCHAMP_PREMIUM",
+ "Service_Plan_Id": "430b908f-78e1-4812-b045-cf83320e7d5d",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp Premium"
+ },
{
"Product_Display_Name": "Microsoft 365 Audio Conferencing",
"String_Id": "MCOMEETADV",
@@ -479,6 +527,14 @@
"Service_Plan_Id": "3a117d30-cfac-4f00-84ac-54f8b6a18d78",
"Service_Plans_Included_Friendly_Names": "Compliance Manager Premium Assessment Add-On"
},
+ {
+ "Product_Display_Name": "Compliance Program for Microsoft Cloud",
+ "String_Id": "Compliance_Program_for_Microsoft_Cloud",
+ "GUID": "10dd46b2-c5ad-4de3-865c-a6fa1363fb51",
+ "Service_Plan_Name": "CPMC",
+ "Service_Plan_Id": "1265e154-5544-4197-bba1-03ef69c3b180",
+ "Service_Plans_Included_Friendly_Names": "Compliance Program for Microsoft Cloud"
+ },
{
"Product_Display_Name": "Defender Threat Intelligence",
"String_Id": "Defender_Threat_Intelligence",
@@ -1279,6 +1335,30 @@
"Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
"Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
},
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Service Voice Channel Add-in",
+ "String_Id": "DYN365_CS_VOICE",
+ "GUID": "dadd2312-b5b1-4fa0-8c15-0903de3e2303",
+ "Service_Plan_Name": "DYN365_CS_VOICE",
+ "Service_Plan_Id": "f6ec6dfa-2402-468d-a455-89be11116d43",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Customer Service Voice Add-in"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Service Voice Channel Add-in",
+ "String_Id": "DYN365_CS_VOICE",
+ "GUID": "dadd2312-b5b1-4fa0-8c15-0903de3e2303",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_D365_CS_VOICE",
+ "Service_Plan_Id": "a3dce1be-e9ca-453a-9483-e69a5b46ce98",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Customer Service Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Service Voice Channel Add-in",
+ "String_Id": "DYN365_CS_VOICE",
+ "GUID": "dadd2312-b5b1-4fa0-8c15-0903de3e2303",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
{
"Product_Display_Name": "Dynamics 365 Customer Insights Standalone",
"String_Id": "DYN365_CUSTOMER_INSIGHTS_BASE",
@@ -1855,6 +1935,30 @@
"Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
"Service_Plans_Included_Friendly_Names": "Exchange Foundation"
},
+ {
+ "Product_Display_Name": "Dynamics 365 Field Service Contractor",
+ "String_Id": "D365_FIELD_SERVICE_CONTRACTOR",
+ "GUID": "23e6e135-e869-4ce4-9ae4-5710cd69ac13",
+ "Service_Plan_Name": "CDS_FIELD_SERVICE_CONTRACTOR",
+ "Service_Plan_Id": "f4614a66-d632-443a-bc77-afe92987b322",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service Field service Part Time Contractors"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Field Service Contractor",
+ "String_Id": "D365_FIELD_SERVICE_CONTRACTOR",
+ "GUID": "23e6e135-e869-4ce4-9ae4-5710cd69ac13",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Field Service Contractor",
+ "String_Id": "D365_FIELD_SERVICE_CONTRACTOR",
+ "GUID": "23e6e135-e869-4ce4-9ae4-5710cd69ac13",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
{
"Product_Display_Name": "Dynamics 365 Field Service Contractor for Government",
"String_Id": "D365_FIELD_SERVICE_CONTRACTOR_GOV",
@@ -1967,6 +2071,118 @@
"Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
"Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
},
+ {
+ "Product_Display_Name": "Dynamics 365 for Finance Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_FINANCE_ATTACH",
+ "GUID": "d721f2e4-099b-4105-b40e-872e46cad402",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Finance Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_FINANCE_ATTACH",
+ "GUID": "d721f2e4-099b-4105-b40e-872e46cad402",
+ "Service_Plan_Name": "CDS_AI_Capacity_FI",
+ "Service_Plan_Id": "5d85ec34-44e5-43b6-a9aa-d1b4c1d3aa3b",
+ "Service_Plans_Included_Friendly_Names": "AI Builder Capacity Add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Finance Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_FINANCE_ATTACH",
+ "GUID": "d721f2e4-099b-4105-b40e-872e46cad402",
+ "Service_Plan_Name": "DYN365_CDS_FINANCE",
+ "Service_Plan_Id": "e95d7060-d4d9-400a-a2bd-a244bf0b609e",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service for Dynamics 365 Finance"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Finance Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_FINANCE_ATTACH",
+ "GUID": "d721f2e4-099b-4105-b40e-872e46cad402",
+ "Service_Plan_Name": "DYN365_REGULATORY_SERVICE",
+ "Service_Plan_Id": "c7657ae3-c0b0-4eed-8c1d-6a7967bd9c65",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Finance and Operations, Enterprise edition - Regulatory Service"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Finance Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_FINANCE_ATTACH",
+ "GUID": "d721f2e4-099b-4105-b40e-872e46cad402",
+ "Service_Plan_Name": "D365_Finance_Attach",
+ "Service_Plan_Id": "223e33cb-eee0-462d-b1bd-e9a5febf8e85",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Finance Attach"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Finance Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_FINANCE_ATTACH",
+ "GUID": "d721f2e4-099b-4105-b40e-872e46cad402",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "D365_ProjectOperationsCDSAttach",
+ "Service_Plan_Id": "e564d403-7eaf-4c91-b92f-bb0dc62026e1",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Project Operations CDS Attach"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "CDS_AI_Capacity_FI",
+ "Service_Plan_Id": "5d85ec34-44e5-43b6-a9aa-d1b4c1d3aa3b",
+ "Service_Plans_Included_Friendly_Names": "AI Builder Capacity Add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "D365_Finance_Attach",
+ "Service_Plan_Id": "223e33cb-eee0-462d-b1bd-e9a5febf8e85",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Finance Attach"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "D365_ProjectOperationsAttach",
+ "Service_Plan_Id": "fa7675bd-6717-40e7-8172-d0bbcbe1ab12",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Project Operations Attach"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "PROJECT_FOR_PROJECT_OPERATIONS_ATTACH",
+ "Service_Plan_Id": "6d8e07c6-9613-484f-8cc1-a66c5c3979bb",
+ "Service_Plans_Included_Friendly_Names": "Project for Project Operations Attach"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Finance Attach to Qualifying Base Offer Embedded with Project Management & Accounting",
+ "String_Id": "DYN365_FINANCE_ATTACH_ISVEMB_PROJOPS",
+ "GUID": "db5bd06c-b99a-4c54-98e9-90fea5164c88",
+ "Service_Plan_Name": "SHAREPOINTSTANDARD",
+ "Service_Plan_Id": "c7699d2e-19aa-44de-8edf-1736da088ca1",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 1)"
+ },
{
"Product_Display_Name": "Dynamics 365 for Case Management Enterprise Edition",
"String_Id": "DYN365_ENTERPRISE_CASE_MANAGEMENT",
@@ -2263,6 +2479,542 @@
"Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
"Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
},
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "DYN365_CC",
+ "Service_Plan_Id": "2a9d72b3-1714-440f-babf-bf92bf9683d8",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Contact Center"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "DYN365_CS_MESSAGING_TPS",
+ "Service_Plan_Id": "47c2b191-a5fb-4129-b690-00c474d2f623",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Service Digital Messaging add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "DYN365_CS_VOICE",
+ "Service_Plan_Id": "f6ec6dfa-2402-468d-a455-89be11116d43",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Customer Service Voice Add-in"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_D365_CS_VOICE",
+ "Service_Plan_Id": "a3dce1be-e9ca-453a-9483-e69a5b46ce98",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Customer Service Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER",
+ "GUID": "dfb1700c-013e-4132-8bce-0d319c43a95d",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "DYN365_CC",
+ "Service_Plan_Id": "2a9d72b3-1714-440f-babf-bf92bf9683d8",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Contact Center"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "DYN365_CS_MESSAGING_TPS",
+ "Service_Plan_Id": "47c2b191-a5fb-4129-b690-00c474d2f623",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Service Digital Messaging add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "DYN365_CS_VOICE",
+ "Service_Plan_Id": "f6ec6dfa-2402-468d-a455-89be11116d43",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Customer Service Voice Add-in"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_D365_CS_VOICE",
+ "Service_Plan_Id": "a3dce1be-e9ca-453a-9483-e69a5b46ce98",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Customer Service Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "39a78eb6-3a8a-4e1e-878a-575a5c8984e7",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "DYN365_CC_DIGITAL",
+ "Service_Plan_Id": "0ef2b4e3-0a2b-450d-8c5f-a52203c40f50",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Contact Center Digital"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "DYN365_CS_MESSAGING_TPS",
+ "Service_Plan_Id": "47c2b191-a5fb-4129-b690-00c474d2f623",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Service Digital Messaging add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "DYN365_CS_MESSAGING",
+ "Service_Plan_Id": "43b076f2-1123-45ba-a339-2e170ee58c53",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Service Digital Messaging Application Integration"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL",
+ "GUID": "59d3d0bf-df39-4b8b-8601-ea6c09a7fd66",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "DYN365_CC_DIGITAL",
+ "Service_Plan_Id": "0ef2b4e3-0a2b-450d-8c5f-a52203c40f50",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Contact Center Digital"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "DYN365_CS_MESSAGING_TPS",
+ "Service_Plan_Id": "47c2b191-a5fb-4129-b690-00c474d2f623",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Service Digital Messaging add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "DYN365_CS_MESSAGING",
+ "Service_Plan_Id": "43b076f2-1123-45ba-a339-2e170ee58c53",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Service Digital Messaging Application Integration"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Digital Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_DIGITAL_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "7e6e6091-1680-4532-9370-6cd4598483ac",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "DYN365_CC_VOICE",
+ "Service_Plan_Id": "57517633-b4ad-4db8-8c1a-65f443424490",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Contact Center Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "DYN365_CS_VOICE",
+ "Service_Plan_Id": "f6ec6dfa-2402-468d-a455-89be11116d43",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Customer Service Voice Add-in"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_D365_CS_VOICE",
+ "Service_Plan_Id": "a3dce1be-e9ca-453a-9483-e69a5b46ce98",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Customer Service Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE",
+ "GUID": "79e2368c-4568-48d5-a352-b0344afabcf8",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "DYN365_CC_VOICE",
+ "Service_Plan_Id": "57517633-b4ad-4db8-8c1a-65f443424490",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Contact Center Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "DYN365_CS_VOICE",
+ "Service_Plan_Id": "f6ec6dfa-2402-468d-a455-89be11116d43",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Customer Service Voice Add-in"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_D365_CS_VOICE",
+ "Service_Plan_Id": "a3dce1be-e9ca-453a-9483-e69a5b46ce98",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Customer Service Voice"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Contact Center Voice Add-on for Customer Service Enterprise",
+ "String_Id": "DYNAMICS_365_CONTACT_CENTER_VOICE_ADD_ON_FOR_CUSTOMER_SERVICE_ENTERPRISE",
+ "GUID": "73e8b747-20bf-463d-8ffd-274a7d65d0bc",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Attach",
+ "String_Id": "Dynamics_365_Customer_Insights_Attach_New",
+ "GUID": "ff22b8d4-5073-4b24-ba45-84ad5d9b6642",
+ "Service_Plan_Name": "CDS_CUSTOMER_INSIGHTS_BASE",
+ "Service_Plan_Id": "d04ca659-b119-4a92-b8fc-3ede584a9d65",
+ "Service_Plans_Included_Friendly_Names": "Dataverse for Cust InsightsΒ BASE"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Attach",
+ "String_Id": "Dynamics_365_Customer_Insights_Attach_New",
+ "GUID": "ff22b8d4-5073-4b24-ba45-84ad5d9b6642",
+ "Service_Plan_Name": "CDS_CUSTOMER_INSIGHTS_COMBINED_BASE",
+ "Service_Plan_Id": "d66ee5da-07d5-49d6-a1d8-45662c3f37be",
+ "Service_Plans_Included_Friendly_Names": "Dataverse for Customer Insights Combined Base"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Attach",
+ "String_Id": "Dynamics_365_Customer_Insights_Attach_New",
+ "GUID": "ff22b8d4-5073-4b24-ba45-84ad5d9b6642",
+ "Service_Plan_Name": "DYN365_CUSTOMER_INSIGHTS_JOURNEYS_BASE",
+ "Service_Plan_Id": "1720c3f7-7da3-4a11-8324-92aad283eb68",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Customer Insights Journeys"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Attach",
+ "String_Id": "Dynamics_365_Customer_Insights_Attach_New",
+ "GUID": "ff22b8d4-5073-4b24-ba45-84ad5d9b6642",
+ "Service_Plan_Name": "Forms_Pro_Marketing_App",
+ "Service_Plan_Id": "22b657cf-0a9e-467b-8a91-5e31f21bc570",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Dynamics 365 Customer Voice for Marketing Application"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Attach",
+ "String_Id": "Dynamics_365_Customer_Insights_Attach_New",
+ "GUID": "ff22b8d4-5073-4b24-ba45-84ad5d9b6642",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Journeys T3 Interacted People",
+ "String_Id": "Dynamics_365_Customer_Insights_Journeys_T3_Interacted_People",
+ "GUID": "05735051-46c0-4c84-9107-bb13d77d0b88",
+ "Service_Plan_Name": "CDS_CUSTOMER_INSIGHTS_JOURNEYS_ADD-ON",
+ "Service_Plan_Id": "2f2e81a6-15de-4041-9f33-73c06fed3801",
+ "Service_Plans_Included_Friendly_Names": "Dataverse for Customer Insights Journeys add-on"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Journeys T3 Interacted People",
+ "String_Id": "Dynamics_365_Customer_Insights_Journeys_T3_Interacted_People",
+ "GUID": "05735051-46c0-4c84-9107-bb13d77d0b88",
+ "Service_Plan_Name": "DYN365_MARKETING_50K_CONTACT_ADDON",
+ "Service_Plan_Id": "e626a4ec-1ba2-409e-bf75-9bc0bc30cca7",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Marketing 50K Addnl Contacts"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights Journeys T3 Interacted People",
+ "String_Id": "Dynamics_365_Customer_Insights_Journeys_T3_Interacted_People",
+ "GUID": "05735051-46c0-4c84-9107-bb13d77d0b88",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights User License",
+ "String_Id": "Dynamics_365_Customer_Insights_User_License",
+ "GUID": "12b5a442-a6f2-49e4-868b-2d7408c2356f",
+ "Service_Plan_Name": "DYN365_MARKETING_MSE_USER",
+ "Service_Plan_Id": "2824c69a-1ac5-4397-8592-eae51cb8b581",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Marketing MSE User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights User License",
+ "String_Id": "Dynamics_365_Customer_Insights_User_License",
+ "GUID": "12b5a442-a6f2-49e4-868b-2d7408c2356f",
+ "Service_Plan_Name": "DYN365_MARKETING_USER",
+ "Service_Plan_Id": "5d7a6abc-eebd-46ab-96e1-e4a2f54a2248",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Marketing USL"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Customer Insights User License",
+ "String_Id": "Dynamics_365_Customer_Insights_User_License",
+ "GUID": "12b5a442-a6f2-49e4-868b-2d7408c2356f",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
{
"Product_Display_Name": "Dynamics 365 for Customer Service Chat",
"String_Id": "DYN365_CS_CHAT",
@@ -2727,6 +3479,62 @@
"Service_Plan_Id": "26fa8a18-2812-4b3d-96b4-864818ce26be",
"Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365 Mixed Reality"
},
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "Forms_Pro_Talent",
+ "Service_Plan_Id": "1c4ae475-5608-43fa-b3f7-d20e07cf24b4",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Dynamics 365 Customer Voice for Talent"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "D365_HR_SELF_SERVICE_OPS",
+ "Service_Plan_Id": "835b837b-63c1-410e-bf6b-bdef201ad129",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resource Self Service"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "D365_HR_OPS",
+ "Service_Plan_Id": "8b21a5dc-5485-49ed-a2d4-0e772c830f6d",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resources"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "D365_HR_Attach",
+ "Service_Plan_Id": "3219525a-4064-45ec-9c35-a33ea6b39a49",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resources Attach"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "D365_HR_ATTACH_OPS",
+ "Service_Plan_Id": "90d8cb62-e98a-4639-8342-8c7d2c8215ba",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Human Resources Attach License"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Human Resources Attach to Qualifying Dynamics 365 Base Offer",
+ "String_Id": "DYN365_HUMAN_RESOURCES_ATTACH",
+ "GUID": "83c489a4-94b6-4dcc-9fdc-ff9b107a4621",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
{
"Product_Display_Name": "Dynamics 365 Hybrid Connector",
"String_Id": "CRM_HYBRIDCONNECTOR",
@@ -2759,6 +3567,22 @@
"Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
"Service_Plans_Included_Friendly_Names": "Exchange Foundation"
},
+ {
+ "Product_Display_Name": "Dynamics 365 for Marketing Addnl Contacts Tier 1",
+ "String_Id": "DYN365_MARKETING_CONTACT_ADDON",
+ "GUID": "fc4581aa-6b1f-459d-95b6-84bd49d6f843",
+ "Service_Plan_Name": "DYN365_MARKETING_CONTACT_ADDON",
+ "Service_Plan_Id": "18db5075-2c70-408d-a82b-929059d782af",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Marketing Additional Contacts Tier 1"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Marketing Addnl Contacts Tier 1",
+ "String_Id": "DYN365_MARKETING_CONTACT_ADDON",
+ "GUID": "fc4581aa-6b1f-459d-95b6-84bd49d6f843",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
{
"Product_Display_Name": "Dynamics 365 for Marketing Addnl Contacts Tier 3",
"String_Id": "DYN365_MARKETING_CONTACT_ADDON_T3",
@@ -3215,6 +4039,78 @@
"Service_Plan_Id": "3089c02b-e533-4b73-96a5-01fa648c3c3c",
"Service_Plans_Included_Friendly_Names": "PowerApps for Dynamics 365 for Government"
},
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "DYN365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "Service_Plan_Id": "1d8c8e0e-4308-4db5-8a41-b129dbdaea20",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Project Service Automation for Government"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "Forms_Pro_PS_GCC",
+ "Service_Plan_Id": "e98256c5-17d0-4987-becc-e991c52d55c6",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Dynamics 365 Customer Voice for Project Service Automation for GCC"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION_GOV",
+ "Service_Plan_Id": "922ba911-5694-4e99-a794-73aed9bfeec8",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation for Government"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "SHAREPOINTWAC_GOV",
+ "Service_Plan_Id": "8f9f0f3b-ca90-406c-a842-95579171f8ec",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web for Government"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "PROJECT_CLIENT_SUBSCRIPTION_GOV",
+ "Service_Plan_Id": "45c6831b-ad74-4c7f-bd03-7c2b3fa39067",
+ "Service_Plans_Included_Friendly_Names": "Project Online Desktop Client"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "SHAREPOINT_PROJECT_GOV",
+ "Service_Plan_Id": "e57afa78-1f19-4542-ba13-b32cd4d8f472",
+ "Service_Plans_Included_Friendly_Names": "Project Online Service for Government"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE_GOV",
+ "Service_Plan_Id": "153f85dd-d912-4762-af6c-d6e0fb4f6692",
+ "Service_Plans_Included_Friendly_Names": "SharePoint Plan 2G"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "FLOW_DYN_APPS_GOV",
+ "Service_Plan_Id": "2c6af4f1-e7b6-4d59-bbc8-eaa884f42d69",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365 for Government"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 for Project Service Automation for Government",
+ "String_Id": "D365_ENTERPRISE_PROJECT_SERVICE_AUTOMATION_GOV",
+ "GUID": "6c827f0a-42cb-4cff-b1cd-f4104c16ede3",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS_GOV",
+ "Service_Plan_Id": "3089c02b-e533-4b73-96a5-01fa648c3c3c",
+ "Service_Plans_Included_Friendly_Names": "PowerApps for Dynamics 365 for Government"
+ },
{
"Product_Display_Name": "Dynamics 365 for Sales and Customer Service Enterprise Edition",
"String_Id": "DYN365_ENTERPRISE_SALES_CUSTOMERSERVICE",
@@ -3927,6 +4823,110 @@
"Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
"Service_Plans_Included_Friendly_Names": "POWERAPPS FOR DYNAMICS 365"
},
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "DYN365_CDS_SUPPLYCHAINMANAGEMENT",
+ "Service_Plan_Id": "b6a8b974-2956-4e14-ae81-f0384c363528",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service for Dynamics 365 Supply Chain Management"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "FLOW_FOR_IOM_USL",
+ "Service_Plan_Id": "9e6d1620-dce9-4655-8933-af8fa5bccc9c",
+ "Service_Plans_Included_Friendly_Names": "Data Integration for IOM with Power Automate USL"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "CDS_FOR_IOM",
+ "Service_Plan_Id": "2bb89402-51e9-4c5a-be33-e954a9dd1ba6",
+ "Service_Plans_Included_Friendly_Names": "Dataverse for IOM"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "D365_DemandPlanning",
+ "Service_Plan_Id": "e8b616eb-1a6d-42b4-84c7-b63870791349",
+ "Service_Plans_Included_Friendly_Names": "DO NOT USE - Dynamics 365 Supply Chain Management Premium"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "DYN365_REGULATORY_SERVICE",
+ "Service_Plan_Id": "c7657ae3-c0b0-4eed-8c1d-6a7967bd9c65",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Finance and Operations, Enterprise edition - Regulatory Service"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "D365_SCM",
+ "Service_Plan_Id": "1224eae4-0d91-474a-8a52-27ec96a63fe7",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 for Supply Chain Management"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "DYN365_IOM",
+ "Service_Plan_Id": "616cf6e2-f52f-4738-b463-10003061fcd3",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Intelligent Order Management"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "DYN365_IOM_USER",
+ "Service_Plan_Id": "81375e2f-5ef7-4773-96aa-e3279f50bd21",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Intelligent Order Management USL"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "D365_SCM_Premium",
+ "Service_Plan_Id": "0363c8e5-c30d-4d7c-a621-7b6cab5e0482",
+ "Service_Plans_Included_Friendly_Names": "Dynamics 365 Supply Chain Management Premium"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "POWERAPPS_DYN_APPS",
+ "Service_Plan_Id": "874fc546-6efe-4d22-90b8-5c4e7aa59f4b",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Dynamics 365"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Dynamics 365 Supply Chain Management Premium",
+ "String_Id": "Dynamics_365_Supply_Chain_Management_Premium",
+ "GUID": "9467fd84-2758-4287-b1fa-6a908c441b8a",
+ "Service_Plan_Name": "FLOW_DYN_APPS",
+ "Service_Plan_Id": "7e6d7d78-73de-46ba-83b1-6d25117334ba",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Dynamics 365"
+ },
{
"Product_Display_Name": "Dynamics 365 for Talent",
"String_Id": "SKU_Dynamics_365_for_HCM_Trial",
@@ -5263,6 +6263,22 @@
"Service_Plan_Id": "882e1d05-acd1-4ccb-8708-6ee03664b117",
"Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365"
},
+ {
+ "Product_Display_Name": "Exchange Online (Plan 2) for GCC",
+ "String_Id": "EXCHANGEENTERPRISE_GOV",
+ "GUID": "7be8dc28-4da4-4e6d-b9b9-c60f2806df8a",
+ "Service_Plan_Name": "EXCHANGE_S_ENTERPRISE_GOV",
+ "Service_Plan_Id": "8c3069c0-ccdb-44be-ab77-986203a67df2",
+ "Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2) for Government"
+ },
+ {
+ "Product_Display_Name": "Exchange Online (Plan 2) for GCC",
+ "String_Id": "EXCHANGEENTERPRISE_GOV",
+ "GUID": "7be8dc28-4da4-4e6d-b9b9-c60f2806df8a",
+ "Service_Plan_Name": "INTUNE_O365",
+ "Service_Plan_Id": "882e1d05-acd1-4ccb-8708-6ee03664b117",
+ "Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365"
+ },
{
"Product_Display_Name": "Exchange Online (Plan 2)",
"String_Id": "EXCHANGEENTERPRISE",
@@ -5367,6 +6383,30 @@
"Service_Plan_Id": "326e2b78-9d27-42c9-8509-46c827743a17",
"Service_Plans_Included_Friendly_Names": "Exchange Online Protection"
},
+ {
+ "Product_Display_Name": "Flow Plan 1 for Government",
+ "String_Id": "FLOW_P1_GOV",
+ "GUID": "2b3b0c87-36af-4d15-8124-04a691cc2546",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION_GOV",
+ "Service_Plan_Id": "922ba911-5694-4e99-a794-73aed9bfeec8",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation for Government"
+ },
+ {
+ "Product_Display_Name": "Flow Plan 1 for Government",
+ "String_Id": "FLOW_P1_GOV",
+ "GUID": "2b3b0c87-36af-4d15-8124-04a691cc2546",
+ "Service_Plan_Name": "DYN365_CDS_P1_GOV",
+ "Service_Plan_Id": "ce361df2-f2a5-4713-953f-4050ba09aad8",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service for Government"
+ },
+ {
+ "Product_Display_Name": "Flow Plan 1 for Government",
+ "String_Id": "FLOW_P1_GOV",
+ "GUID": "2b3b0c87-36af-4d15-8124-04a691cc2546",
+ "Service_Plan_Name": "FLOW_P1_GOV",
+ "Service_Plan_Id": "774da41c-a8b3-47c1-8322-b9c1ab68be9f",
+ "Service_Plans_Included_Friendly_Names": "Power Automate (Plan 1) for Government"
+ },
{
"Product_Display_Name": "Intune",
"String_Id": "INTUNE_A",
@@ -6904,15 +7944,15 @@
"Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "AAD_BASIC_EDU",
"Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education"
+ "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "RMS_S_ENTERPRISE",
@@ -6920,7 +7960,7 @@
"Service_Plans_Included_Friendly_Names": "Azure Rights Management"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "CDS_O365_P3",
@@ -6928,7 +7968,7 @@
"Service_Plans_Included_Friendly_Names": "Common Data Service for Teams"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "LOCKBOX_ENTERPRISE",
@@ -6936,7 +7976,15 @@
"Service_Plans_Included_Friendly_Names": "Customer Lockbox"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "CustomerLockboxA_Enterprise",
+ "Service_Plan_Id": "3ec18638-bd4c-4d3b-8905-479ed636b83e",
+ "Service_Plans_Included_Friendly_Names": "Customer Lockbox (A)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MIP_S_Exchange",
@@ -6944,7 +7992,15 @@
"Service_Plans_Included_Friendly_Names": "Data Classification in Microsoft 365"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "COMMON_DEFENDER_PLATFORM_FOR_OFFICE",
+ "Service_Plan_Id": "a312bdeb-1e21-40d0-84b1-0e73f128144f",
+ "Service_Plans_Included_Friendly_Names": "Defender Platform for Office 365"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "EducationAnalyticsP1",
@@ -6952,7 +8008,7 @@
"Service_Plans_Included_Friendly_Names": "Education Analytics"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "EXCHANGE_S_ENTERPRISE",
@@ -6960,7 +8016,15 @@
"Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX",
+ "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757",
+ "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "INFORMATION_BARRIERS",
@@ -6968,7 +8032,7 @@
"Service_Plans_Included_Friendly_Names": "Information Barriers"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "Content_Explorer",
@@ -6976,7 +8040,7 @@
"Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics - Premium"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "ContentExplorer_Standard",
@@ -6984,7 +8048,7 @@
"Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics β Standard"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MIP_S_CLP2",
@@ -6992,7 +8056,7 @@
"Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Premium"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MIP_S_CLP1",
@@ -7000,7 +8064,7 @@
"Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Standard"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "M365_ADVANCED_AUDITING",
@@ -7008,15 +8072,15 @@
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Advanced Auditing"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "OFFICESUBSCRIPTION",
"Service_Plan_Id": "43de0ff5-c92c-492b-9116-175376d08c38",
- "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for Enterprise"
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for enterprise"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MCOMEETADV",
@@ -7024,7 +8088,15 @@
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Audio Conferencing"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "M365_AUDIT_PLATFORM",
+ "Service_Plan_Id": "f6de4823-28fa-440b-b886-4783fa86ddba",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audit Platform"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MICROSOFT_COMMUNICATION_COMPLIANCE",
@@ -7032,7 +8104,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Communication Compliance"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MTP",
@@ -7040,7 +8112,15 @@
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Defender"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1",
+ "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MCOEV",
@@ -7048,7 +8128,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Phone System"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MICROSOFTBOOKINGS",
@@ -7056,7 +8136,15 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Bookings"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "CLIPCHAMP",
+ "Service_Plan_Id": "a1ace008-72f3-4ea0-8dac-33b3a23a2472",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "COMMUNICATIONS_DLP",
@@ -7064,7 +8152,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Communications DLP"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "CUSTOMER_KEY",
@@ -7072,15 +8160,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Customer Key"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
- "String_Id": "M365EDU_A5_FACULTY",
- "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
- "Service_Plan_Name": "DATA_INVESTIGATIONS",
- "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9",
- "Service_Plans_Included_Friendly_Names": "Microsoft Data Investigations"
- },
- {
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "ATP_ENTERPRISE",
@@ -7088,7 +8168,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 1)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "THREAT_INTELLIGENCE",
@@ -7096,7 +8176,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 2)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "EXCEL_PREMIUM",
@@ -7104,7 +8184,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Excel Advanced Analytics"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "OFFICE_FORMS_PLAN_3",
@@ -7112,7 +8192,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Forms (Plan 3)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "INFO_GOVERNANCE",
@@ -7120,7 +8200,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Information Governance"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "INSIDER_RISK",
@@ -7128,7 +8208,15 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT",
+ "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management - Exchange"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "KAIZALA_STANDALONE",
@@ -7136,7 +8224,15 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "MICROSOFT_LOOP",
+ "Service_Plan_Id": "c4b8c31a-fb44-4c65-9837-a21f55fcabda",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Loop"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "ML_CLASSIFICATION",
@@ -7144,7 +8240,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft ML-Based Classification"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "EXCHANGE_ANALYTICS",
@@ -7152,7 +8248,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft MyAnalytics (Full)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "PROJECTWORKMANAGEMENT",
@@ -7160,7 +8256,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Planner"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "RECORDS_MANAGEMENT",
@@ -7168,7 +8264,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Records Management"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MICROSOFT_SEARCH",
@@ -7176,7 +8272,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Search"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "Deskless",
@@ -7184,7 +8280,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft StaffHub"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "STREAM_O365_E5",
@@ -7192,7 +8288,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Stream for Office 365 E5"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "TEAMS1",
@@ -7200,7 +8296,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Teams"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MINECRAFT_EDUCATION_EDITION",
@@ -7208,7 +8304,7 @@
"Service_Plans_Included_Friendly_Names": "Minecraft Education Edition"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "INTUNE_O365",
@@ -7216,7 +8312,7 @@
"Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "Nucleus",
@@ -7224,7 +8320,7 @@
"Service_Plans_Included_Friendly_Names": "Nucleus"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "EQUIVIO_ANALYTICS",
@@ -7232,7 +8328,7 @@
"Service_Plans_Included_Friendly_Names": "Office 365 Advanced eDiscovery"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "ADALLOM_S_O365",
@@ -7240,7 +8336,7 @@
"Service_Plans_Included_Friendly_Names": "Office 365 Cloud App Security"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "PAM_ENTERPRISE",
@@ -7248,7 +8344,7 @@
"Service_Plans_Included_Friendly_Names": "Office 365 Privileged Access Management"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "SAFEDOCS",
@@ -7256,7 +8352,7 @@
"Service_Plans_Included_Friendly_Names": "Office 365 SafeDocs"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "SHAREPOINTWAC_EDU",
@@ -7264,7 +8360,7 @@
"Service_Plans_Included_Friendly_Names": "Office for the Web for Education"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "POWERAPPS_O365_P3",
@@ -7272,7 +8368,7 @@
"Service_Plans_Included_Friendly_Names": "Power Apps for Office 365 (Plan 3)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "BI_AZURE_P2",
@@ -7280,7 +8376,7 @@
"Service_Plans_Included_Friendly_Names": "Power BI Pro"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "PREMIUM_ENCRYPTION",
@@ -7288,7 +8384,7 @@
"Service_Plans_Included_Friendly_Names": "Premium Encryption in Office 365"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "PROJECT_O365_P3",
@@ -7296,23 +8392,39 @@
"Service_Plans_Included_Friendly_Names": "Project for Office (Plan E5)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "PURVIEW_DISCOVERY",
+ "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522",
+ "Service_Plans_Included_Friendly_Names": "Purview Discovery"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "Bing_Chat_Enterprise",
+ "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "COMMUNICATIONS_COMPLIANCE",
"Service_Plan_Id": "41fcdd7d-4733-4863-9cf4-c65b83ce2df4",
- "Service_Plans_Included_Friendly_Names": "Microsoft Communications Compliance"
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Microsoft Communications Compliance"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
- "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT",
- "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34",
- "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management"
+ "Service_Plan_Name": "DATA_INVESTIGATIONS",
+ "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9",
+ "Service_Plans_Included_Friendly_Names": "Retired - Microsoft Data Investigations"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "SCHOOL_DATA_SYNC_P2",
@@ -7320,7 +8432,7 @@
"Service_Plans_Included_Friendly_Names": "School Data Sync (Plan 2)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "SHAREPOINTENTERPRISE_EDU",
@@ -7328,7 +8440,7 @@
"Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2) for Education"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MCOSTANDARD",
@@ -7336,7 +8448,7 @@
"Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "SWAY",
@@ -7344,7 +8456,7 @@
"Service_Plans_Included_Friendly_Names": "Sway"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "BPOS_S_TODO_3",
@@ -7352,7 +8464,7 @@
"Service_Plans_Included_Friendly_Names": "To-Do (Plan 3)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "VIVA_LEARNING_SEEDED",
@@ -7360,7 +8472,7 @@
"Service_Plans_Included_Friendly_Names": "Viva Learning Seeded"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "WHITEBOARD_PLAN3",
@@ -7368,7 +8480,7 @@
"Service_Plans_Included_Friendly_Names": "Whiteboard (Plan 3)"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "YAMMER_EDU",
@@ -7376,7 +8488,7 @@
"Service_Plans_Included_Friendly_Names": "Yammer for Academic"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "WINDEFATP",
@@ -7384,7 +8496,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Defender for Endpoint"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MICROSOFTENDPOINTDLP",
@@ -7392,7 +8504,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Endpoint DLP"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "UNIVERSAL_PRINT_01",
@@ -7400,7 +8512,7 @@
"Service_Plans_Included_Friendly_Names": "Universal Print"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "Virtualization Rights for Windows 10 (E3/E5+VDA)",
@@ -7408,7 +8520,7 @@
"Service_Plans_Included_Friendly_Names": "Windows 10/11 Enterprise"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "WINDOWSUPDATEFORBUSINESS_DEPLOYMENTSERVICE",
@@ -7416,23 +8528,7 @@
"Service_Plans_Included_Friendly_Names": "Windows Update for Business Deployment Service"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
- "String_Id": "M365EDU_A5_FACULTY",
- "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
- "Service_Plan_Name": "AAD_PREMIUM",
- "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1"
- },
- {
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
- "String_Id": "M365EDU_A5_FACULTY",
- "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
- "Service_Plan_Name": "AAD_PREMIUM_P2",
- "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2"
- },
- {
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "RMS_S_PREMIUM",
@@ -7440,7 +8536,7 @@
"Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P1"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "RMS_S_PREMIUM2",
@@ -7448,7 +8544,7 @@
"Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P2"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "DYN365_CDS_O365_P3",
@@ -7456,7 +8552,15 @@
"Service_Plans_Included_Friendly_Names": "Common Data Service"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "Intune_ServiceNow",
+ "Service_Plan_Id": "3eeb8536-fecf-41bf-a3f8-d6f17a9f3efc",
+ "Service_Plans_Included_Friendly_Names": "Intune ServiceNow Integration"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "MFA_PREMIUM",
@@ -7464,7 +8568,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Azure Multi-Factor Authentication"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "ADALLOM_S_STANDALONE",
@@ -7472,7 +8576,7 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Defender for Cloud Apps"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "ATA",
@@ -7480,23 +8584,39 @@
"Service_Plans_Included_Friendly_Names": "Microsoft Defender for Identity"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "AAD_PREMIUM",
+ "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "AAD_PREMIUM_P2",
+ "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "INTUNE_A",
"Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5",
- "Service_Plans_Included_Friendly_Names": "Microsoft Intune"
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "INTUNE_EDU",
"Service_Plan_Id": "da24caf9-af8e-485c-b7c8-e73336da2693",
- "Service_Plans_Included_Friendly_Names": "Microsoft Intune for Education"
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1 for Education"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "FLOW_O365_P3",
@@ -7504,20 +8624,28 @@
"Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
},
{
- "Product_Display_Name": "Microsoft 365 A5 for Faculty",
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
"String_Id": "M365EDU_A5_FACULTY",
"GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
"Service_Plan_Name": "POWER_VIRTUAL_AGENTS_O365_P3",
"Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014",
"Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for faculty",
+ "String_Id": "M365EDU_A5_FACULTY",
+ "GUID": "e97c048c-37a4-45fb-ab50-922fbf07a370",
+ "Service_Plan_Name": "REMOTE_HELP",
+ "Service_Plan_Id": "a4c6cf29-1168-4076-ba5c-e8fe0e62b17e",
+ "Service_Plans_Included_Friendly_Names": "Remote help"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
"GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
"Service_Plan_Name": "AAD_BASIC_EDU",
"Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education"
+ "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education"
},
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
@@ -7543,6 +8671,14 @@
"Service_Plan_Id": "9f431833-0334-42de-a7dc-70aa40db46db",
"Service_Plans_Included_Friendly_Names": "Customer Lockbox"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "CustomerLockboxA_Enterprise",
+ "Service_Plan_Id": "3ec18638-bd4c-4d3b-8905-479ed636b83e",
+ "Service_Plans_Included_Friendly_Names": "Customer Lockbox (A)"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7551,6 +8687,14 @@
"Service_Plan_Id": "cd31b152-6326-4d1b-ae1b-997b625182e6",
"Service_Plans_Included_Friendly_Names": "Data Classification in Microsoft 365"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "COMMON_DEFENDER_PLATFORM_FOR_OFFICE",
+ "Service_Plan_Id": "a312bdeb-1e21-40d0-84b1-0e73f128144f",
+ "Service_Plans_Included_Friendly_Names": "Defender Platform for Office 365"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7567,6 +8711,14 @@
"Service_Plan_Id": "efb87545-963c-4e0d-99df-69c6916d9eb0",
"Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2)"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX",
+ "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757",
+ "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7621,7 +8773,7 @@
"GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
"Service_Plan_Name": "OFFICESUBSCRIPTION",
"Service_Plan_Id": "43de0ff5-c92c-492b-9116-175376d08c38",
- "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for Enterprise"
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for enterprise"
},
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
@@ -7631,6 +8783,14 @@
"Service_Plan_Id": "3e26ee1f-8a5f-4d52-aee2-b81ce45c8f40",
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Audio Conferencing"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "M365_AUDIT_PLATFORM",
+ "Service_Plan_Id": "f6de4823-28fa-440b-b886-4783fa86ddba",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audit Platform"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7647,6 +8807,14 @@
"Service_Plan_Id": "bf28f719-7844-4079-9c78-c1307898e192",
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Defender"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1",
+ "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7663,6 +8831,14 @@
"Service_Plan_Id": "199a5c09-e0ca-4e37-8f7c-b05d533e1ea2",
"Service_Plans_Included_Friendly_Names": "Microsoft Bookings"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "CLIPCHAMP",
+ "Service_Plan_Id": "a1ace008-72f3-4ea0-8dac-33b3a23a2472",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7679,14 +8855,6 @@
"Service_Plan_Id": "6db1f1db-2b46-403f-be40-e39395f08dbb",
"Service_Plans_Included_Friendly_Names": "Microsoft Customer Key"
},
- {
- "Product_Display_Name": "Microsoft 365 A5 for Students",
- "String_Id": "M365EDU_A5_STUDENT",
- "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
- "Service_Plan_Name": "DATA_INVESTIGATIONS",
- "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9",
- "Service_Plans_Included_Friendly_Names": "Microsoft Data Investigations"
- },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7735,6 +8903,14 @@
"Service_Plan_Id": "d587c7a3-bda9-4f99-8776-9bcf59c84f75",
"Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT",
+ "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management - Exchange"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7743,6 +8919,14 @@
"Service_Plan_Id": "0898bdbb-73b0-471a-81e5-20f1fe4dd66e",
"Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "MICROSOFT_LOOP",
+ "Service_Plan_Id": "c4b8c31a-fb44-4c65-9837-a21f55fcabda",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Loop"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7895,6 +9079,22 @@
"Service_Plan_Id": "b21a6b06-1988-436e-a07b-51ec6d9f52ad",
"Service_Plans_Included_Friendly_Names": "Project for Office (Plan E5)"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "PURVIEW_DISCOVERY",
+ "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522",
+ "Service_Plans_Included_Friendly_Names": "Purview Discovery"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "Bing_Chat_Enterprise",
+ "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -7907,9 +9107,9 @@
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
"GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
- "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT",
- "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34",
- "Service_Plans_Included_Friendly_Names": "RETIRED - Microsoft Insider Risk Management"
+ "Service_Plan_Name": "DATA_INVESTIGATIONS",
+ "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9",
+ "Service_Plans_Included_Friendly_Names": "Retired - Microsoft Data Investigations"
},
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
@@ -8007,22 +9207,6 @@
"Service_Plan_Id": "7bf960f6-2cd9-443a-8046-5dbff9558365",
"Service_Plans_Included_Friendly_Names": "Windows Update for Business Deployment Service"
},
- {
- "Product_Display_Name": "Microsoft 365 A5 for Students",
- "String_Id": "M365EDU_A5_STUDENT",
- "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
- "Service_Plan_Name": "AAD_PREMIUM",
- "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1"
- },
- {
- "Product_Display_Name": "Microsoft 365 A5 for Students",
- "String_Id": "M365EDU_A5_STUDENT",
- "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
- "Service_Plan_Name": "AAD_PREMIUM_P2",
- "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2"
- },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -8047,6 +9231,14 @@
"Service_Plan_Id": "28b0fa46-c39a-4188-89e2-58e979a6b014",
"Service_Plans_Included_Friendly_Names": "Common Data Service"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "Intune_ServiceNow",
+ "Service_Plan_Id": "3eeb8536-fecf-41bf-a3f8-d6f17a9f3efc",
+ "Service_Plans_Included_Friendly_Names": "Intune ServiceNow Integration"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
@@ -8071,13 +9263,29 @@
"Service_Plan_Id": "14ab5db5-e6c4-4b20-b4bc-13e36fd2227f",
"Service_Plans_Included_Friendly_Names": "Microsoft Defender for Identity"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "AAD_PREMIUM",
+ "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "AAD_PREMIUM_P2",
+ "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
"String_Id": "M365EDU_A5_STUDENT",
"GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
"Service_Plan_Name": "INTUNE_A",
"Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5",
- "Service_Plans_Included_Friendly_Names": "Microsoft Intune"
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1"
},
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
@@ -8085,7 +9293,7 @@
"GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
"Service_Plan_Name": "INTUNE_EDU",
"Service_Plan_Id": "da24caf9-af8e-485c-b7c8-e73336da2693",
- "Service_Plans_Included_Friendly_Names": "Microsoft Intune for Education"
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1 for Education"
},
{
"Product_Display_Name": "Microsoft 365 A5 for Students",
@@ -8103,6 +9311,14 @@
"Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014",
"Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365"
},
+ {
+ "Product_Display_Name": "Microsoft 365 A5 for Students",
+ "String_Id": "M365EDU_A5_STUDENT",
+ "GUID": "46c119d4-0379-4a9d-85e4-97c66d3f909e",
+ "Service_Plan_Name": "REMOTE_HELP",
+ "Service_Plan_Id": "a4c6cf29-1168-4076-ba5c-e8fe0e62b17e",
+ "Service_Plans_Included_Friendly_Names": "Remote help"
+ },
{
"Product_Display_Name": "Microsoft 365 A5 for students use benefit",
"String_Id": "M365EDU_A5_STUUSEBNFT",
@@ -13079,6 +14295,102 @@
"Service_Plan_Id": "89f1c4c8-0878-40f7-804d-869c9128ab5d",
"Service_Plans_Included_Friendly_Names": "Power Platform Connectors in Microsoft 365 Copilot"
},
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "COPILOT_STUDIO_IN_COPILOT_FOR_M365",
+ "Service_Plan_Id": "fe6c28b3-d468-44ea-bbd0-a10a5167435c",
+ "Service_Plans_Included_Friendly_Names": "Copilot Studio in Copilot for M365"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "GRAPH_CONNECTORS_COPILOT",
+ "Service_Plan_Id": "82d30987-df9b-4486-b146-198b21d164c7",
+ "Service_Plans_Included_Friendly_Names": "Graph Connectors in Microsoft 365 Copilot"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "M365_COPILOT_INTELLIGENT_SEARCH",
+ "Service_Plan_Id": "931e4a88-a67f-48b5-814f-16a5f1e6028d",
+ "Service_Plans_Included_Friendly_Names": "Intelligent Search"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "M365_COPILOT_SHAREPOINT",
+ "Service_Plan_Id": "0aedf20c-091d-420b-aadf-30c042609612",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Copilot for SharePoint"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "M365_COPILOT_TEAMS",
+ "Service_Plan_Id": "b95945de-b3bd-46db-8437-f2beb6ea2347",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Copilot in Microsoft Teams"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "M365_COPILOT_APPS",
+ "Service_Plan_Id": "a62f8878-de10-42f3-b68f-6149a25ceb97",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Copilot in Productivity Apps"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "Microsoft_Copilot_for_Sales",
+ "Service_Plan_Id": "a2194428-ead1-4fc1-bb81-ab8675125f42",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Copilot for Sales"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "Microsoft_Copilot_for_Sales_PowerAutomate",
+ "Service_Plan_Id": "0c1c2af2-6c51-43c7-9c55-fa487ac147ff",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Copilot for Sales with Power Automate"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "M365_COPILOT_BUSINESS_CHAT",
+ "Service_Plan_Id": "3f30311c-6b1e-48a4-ab79-725b469da960",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Copilot with Graph-grounded chat"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "WORKPLACE_ANALYTICS_INSIGHTS_USER",
+ "Service_Plan_Id": "b622badb-1b45-48d5-920f-4b27a2c0996c",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Viva Insights"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "WORKPLACE_ANALYTICS_INSIGHTS_BACKEND",
+ "Service_Plan_Id": "ff7b261f-d98b-415b-827c-42a3fdf015af",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Viva Insights Backend"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 Copilot for Sales",
+ "String_Id": "Microsoft_Copilot_for_Sales",
+ "GUID": "15f2e9fc-b782-4f73-bf51-81d8b7fff6f4",
+ "Service_Plan_Name": "M365_COPILOT_CONNECTORS",
+ "Service_Plan_Id": "89f1c4c8-0878-40f7-804d-869c9128ab5d",
+ "Service_Plans_Included_Friendly_Names": "Power Platform Connectors in Microsoft 365 Copilot"
+ },
{
"Product_Display_Name": "Microsoft Copilot for Microsoft 365",
"String_Id": "M365_Copilot",
@@ -16695,6 +18007,702 @@
"Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014",
"Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365"
},
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MESH_AVATARS_FOR_TEAMS",
+ "Service_Plan_Id": "dcf9d2f4-772e-4434-b757-77a453cfbc02",
+ "Service_Plans_Included_Friendly_Names": "Avatars for Teams"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MESH_AVATARS_ADDITIONAL_FOR_TEAMS",
+ "Service_Plan_Id": "3efbd4ed-8958-4824-8389-1321f8730af8",
+ "Service_Plans_Included_Friendly_Names": "Avatars for Teams (additional)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "CDS_O365_P3",
+ "Service_Plan_Id": "afa73018-811e-46e9-988f-f75d2b1b8430",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service for Teams"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "LOCKBOX_ENTERPRISE",
+ "Service_Plan_Id": "9f431833-0334-42de-a7dc-70aa40db46db",
+ "Service_Plans_Included_Friendly_Names": "Customer Lockbox"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "CustomerLockboxA_Enterprise",
+ "Service_Plan_Id": "3ec18638-bd4c-4d3b-8905-479ed636b83e",
+ "Service_Plans_Included_Friendly_Names": "Customer Lockbox (A)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MIP_S_Exchange",
+ "Service_Plan_Id": "cd31b152-6326-4d1b-ae1b-997b625182e6",
+ "Service_Plans_Included_Friendly_Names": "Data Classification in Microsoft 365"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "EXCHANGE_S_ENTERPRISE",
+ "Service_Plan_Id": "efb87545-963c-4e0d-99df-69c6916d9eb0",
+ "Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX",
+ "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757",
+ "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MESH_IMMERSIVE_FOR_TEAMS",
+ "Service_Plan_Id": "f0ff6ac6-297d-49cd-be34-6dfef97f0c28",
+ "Service_Plans_Included_Friendly_Names": "Immersive spaces for Teams"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "INFORMATION_BARRIERS",
+ "Service_Plan_Id": "c4801e8a-cb58-4c35-aca6-f2dcc106f287",
+ "Service_Plans_Included_Friendly_Names": "Information Barriers"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "Content_Explorer",
+ "Service_Plan_Id": "d9fa6af4-e046-4c89-9226-729a0786685d",
+ "Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics - Premium"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "ContentExplorer_Standard",
+ "Service_Plan_Id": "2b815d45-56e4-4e3a-b65c-66cb9175b560",
+ "Service_Plans_Included_Friendly_Names": "Information Protection and Governance Analytics β Standard"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MIP_S_CLP2",
+ "Service_Plan_Id": "efb0351d-3b08-4503-993d-383af8de41e3",
+ "Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Premium"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MIP_S_CLP1",
+ "Service_Plan_Id": "5136a095-5cf0-4aff-bec3-e84448b38ea5",
+ "Service_Plans_Included_Friendly_Names": "Information Protection for Office 365 - Standard"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MYANALYTICS_P2",
+ "Service_Plan_Id": "33c4f319-9bdd-48d6-9c4d-410b750a4a5a",
+ "Service_Plans_Included_Friendly_Names": "Insights by MyAnalytics"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "M365_ADVANCED_AUDITING",
+ "Service_Plan_Id": "2f442157-a11c-46b9-ae5b-6e39ff4e5849",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Advanced Auditing"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "OFFICESUBSCRIPTION",
+ "Service_Plan_Id": "43de0ff5-c92c-492b-9116-175376d08c38",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Apps for enterprise"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MCOMEETADV",
+ "Service_Plan_Id": "3e26ee1f-8a5f-4d52-aee2-b81ce45c8f40",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audio Conferencing"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "M365_AUDIT_PLATFORM",
+ "Service_Plan_Id": "f6de4823-28fa-440b-b886-4783fa86ddba",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Audit Platform"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MICROSOFT_COMMUNICATION_COMPLIANCE",
+ "Service_Plan_Id": "a413a9ff-720c-4822-98ef-2f37c2a21f4c",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Communication Compliance"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MTP",
+ "Service_Plan_Id": "bf28f719-7844-4079-9c78-c1307898e192",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Defender"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1",
+ "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MCOEV",
+ "Service_Plan_Id": "4828c8ec-dc2e-4779-b502-87ac9ce28ab7",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Phone System"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MICROSOFTBOOKINGS",
+ "Service_Plan_Id": "199a5c09-e0ca-4e37-8f7c-b05d533e1ea2",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Bookings"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "CLIPCHAMP",
+ "Service_Plan_Id": "a1ace008-72f3-4ea0-8dac-33b3a23a2472",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Clipchamp"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "COMMUNICATIONS_DLP",
+ "Service_Plan_Id": "6dc145d6-95dd-4191-b9c3-185575ee6f6b",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Communications DLP"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "CUSTOMER_KEY",
+ "Service_Plan_Id": "6db1f1db-2b46-403f-be40-e39395f08dbb",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Customer Key"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "ATP_ENTERPRISE",
+ "Service_Plan_Id": "f20fedf3-f3c3-43c3-8267-2bfdd51c0939",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "THREAT_INTELLIGENCE",
+ "Service_Plan_Id": "8e0c0a52-6a6c-4d40-8370-dd62790dcd70",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Office 365 (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "EXCEL_PREMIUM",
+ "Service_Plan_Id": "531ee2f8-b1cb-453b-9c21-d2180d014ca5",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Excel Advanced Analytics"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "FORMS_PLAN_E5",
+ "Service_Plan_Id": "e212cbc7-0961-4c40-9825-01117710dcb1",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Forms (Plan E5)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "INFO_GOVERNANCE",
+ "Service_Plan_Id": "e26c2fcc-ab91-4a61-b35c-03cdc8dddf66",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Information Governance"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "INSIDER_RISK",
+ "Service_Plan_Id": "d587c7a3-bda9-4f99-8776-9bcf59c84f75",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "INSIDER_RISK_MANAGEMENT",
+ "Service_Plan_Id": "9d0c4ee5-e4a1-4625-ab39-d82b619b1a34",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Insider Risk Management - Exchange"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "KAIZALA_STANDALONE",
+ "Service_Plan_Id": "0898bdbb-73b0-471a-81e5-20f1fe4dd66e",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MICROSOFT_LOOP",
+ "Service_Plan_Id": "c4b8c31a-fb44-4c65-9837-a21f55fcabda",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Loop"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "ML_CLASSIFICATION",
+ "Service_Plan_Id": "d2d51368-76c9-4317-ada2-a12c004c432f",
+ "Service_Plans_Included_Friendly_Names": "Microsoft ML-Based Classification"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "EXCHANGE_ANALYTICS",
+ "Service_Plan_Id": "34c0d7a0-a70f-4668-9238-47f9fc208882",
+ "Service_Plans_Included_Friendly_Names": "Microsoft MyAnalytics (Full)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PROJECTWORKMANAGEMENT",
+ "Service_Plan_Id": "b737dad2-2f6c-4c65-90e3-ca563267e8b9",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Planner"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "RECORDS_MANAGEMENT",
+ "Service_Plan_Id": "65cc641f-cccd-4643-97e0-a17e3045e541",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Records Management"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MICROSOFT_SEARCH",
+ "Service_Plan_Id": "94065c59-bc8e-4e8b-89e5-5138d471eaff",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Search"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "Deskless",
+ "Service_Plan_Id": "8c7d2df8-86f0-4902-b2ed-a0458298f3b3",
+ "Service_Plans_Included_Friendly_Names": "Microsoft StaffHub"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "STREAM_O365_E5",
+ "Service_Plan_Id": "6c6042f5-6f01-4d67-b8c1-eb99d36eed3e",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Stream for Office 365 E5"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "INTUNE_O365",
+ "Service_Plan_Id": "882e1d05-acd1-4ccb-8708-6ee03664b117",
+ "Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "Nucleus",
+ "Service_Plan_Id": "db4d623d-b514-490b-b7ef-8885eee514de",
+ "Service_Plans_Included_Friendly_Names": "Nucleus"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "EQUIVIO_ANALYTICS",
+ "Service_Plan_Id": "4de31727-a228-4ec3-a5bf-8e45b5ca48cc",
+ "Service_Plans_Included_Friendly_Names": "Office 365 Advanced eDiscovery"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "ADALLOM_S_O365",
+ "Service_Plan_Id": "8c098270-9dd4-4350-9b30-ba4703f3b36b",
+ "Service_Plans_Included_Friendly_Names": "Office 365 Cloud App Security"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PAM_ENTERPRISE",
+ "Service_Plan_Id": "b1188c4c-1b36-4018-b48b-ee07604f6feb",
+ "Service_Plans_Included_Friendly_Names": "Office 365 Privileged Access Management"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "SAFEDOCS",
+ "Service_Plan_Id": "bf6f5520-59e3-4f82-974b-7dbbc4fd27c7",
+ "Service_Plans_Included_Friendly_Names": "Office 365 SafeDocs"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PEOPLE_SKILLS_FOUNDATION",
+ "Service_Plan_Id": "13b6da2c-0d84-450e-9f69-a33e221387ca",
+ "Service_Plans_Included_Friendly_Names": "People Skills - Foundation"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "POWERAPPS_O365_P3",
+ "Service_Plan_Id": "9c0dab89-a30c-4117-86e7-97bda240acd2",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365 (Plan 3)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "BI_AZURE_P2",
+ "Service_Plan_Id": "70d33638-9c74-4d01-bfd3-562de28bd4ba",
+ "Service_Plans_Included_Friendly_Names": "Power BI Pro"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PREMIUM_ENCRYPTION",
+ "Service_Plan_Id": "617b097b-4b93-4ede-83de-5f075bb5fb2f",
+ "Service_Plans_Included_Friendly_Names": "Premium Encryption in Office 365"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PROJECT_O365_P3",
+ "Service_Plan_Id": "b21a6b06-1988-436e-a07b-51ec6d9f52ad",
+ "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E5)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PURVIEW_DISCOVERY",
+ "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522",
+ "Service_Plans_Included_Friendly_Names": "Purview Discovery"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "Bing_Chat_Enterprise",
+ "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "COMMUNICATIONS_COMPLIANCE",
+ "Service_Plan_Id": "41fcdd7d-4733-4863-9cf4-c65b83ce2df4",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Microsoft Communications Compliance"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "DATA_INVESTIGATIONS",
+ "Service_Plan_Id": "46129a58-a698-46f0-aa5b-17f6586297d9",
+ "Service_Plans_Included_Friendly_Names": "Retired - Microsoft Data Investigations"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "PLACES_CORE",
+ "Service_Plan_Id": "1fe6227d-3e01-46d0-9510-0acad4ff6e94",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Places Core"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "SHAREPOINTENTERPRISE",
+ "Service_Plan_Id": "5dbe027f-2339-4123-9542-606e4d348a72",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MCOSTANDARD",
+ "Service_Plan_Id": "0feaeb32-d00e-4d66-bd5a-43b5b83db82c",
+ "Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "SWAY",
+ "Service_Plan_Id": "a23b959c-7ce8-4e57-9140-b90eb88a9e97",
+ "Service_Plans_Included_Friendly_Names": "Sway"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "BPOS_S_TODO_3",
+ "Service_Plan_Id": "3fb82609-8c27-4f7b-bd51-30634711ee67",
+ "Service_Plans_Included_Friendly_Names": "To-Do (Plan 3)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "VIVAENGAGE_CORE",
+ "Service_Plan_Id": "a82fbf69-b4d7-49f4-83a6-915b2cf354f4",
+ "Service_Plans_Included_Friendly_Names": "Viva Engage Core"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "VIVA_LEARNING_SEEDED",
+ "Service_Plan_Id": "b76fb638-6ba6-402a-b9f9-83d28acb3d86",
+ "Service_Plans_Included_Friendly_Names": "Viva Learning Seeded"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "WHITEBOARD_PLAN3",
+ "Service_Plan_Id": "4a51bca5-1eff-43f5-878c-177680f191af",
+ "Service_Plans_Included_Friendly_Names": "Whiteboard (Plan 3)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "YAMMER_ENTERPRISE",
+ "Service_Plan_Id": "7547a3fe-08ee-4ccb-b430-5077c5041653",
+ "Service_Plans_Included_Friendly_Names": "Yammer Enterprise"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "WINDEFATP",
+ "Service_Plan_Id": "871d91ec-ec1a-452b-a83f-bd76c7d770ef",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Endpoint"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MICROSOFTENDPOINTDLP",
+ "Service_Plan_Id": "64bfac92-2b17-4482-b5e5-a0304429de3e",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Endpoint DLP"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "UNIVERSAL_PRINT_01",
+ "Service_Plan_Id": "795f6fe0-cc4d-4773-b050-5dde4dc704c9",
+ "Service_Plans_Included_Friendly_Names": "Universal Print"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "WIN10_PRO_ENT_SUB",
+ "Service_Plan_Id": "21b439ba-a0ca-424f-a6cc-52f954a5b111",
+ "Service_Plans_Included_Friendly_Names": "Windows 10/11 Enterprise (Original)"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "Windows_Autopatch",
+ "Service_Plan_Id": "9a6eeb79-0b4b-4bf0-9808-39d99a2cd5a3",
+ "Service_Plans_Included_Friendly_Names": "Windows Autopatch"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "WINDOWSUPDATEFORBUSINESS_DEPLOYMENTSERVICE",
+ "Service_Plan_Id": "7bf960f6-2cd9-443a-8046-5dbff9558365",
+ "Service_Plans_Included_Friendly_Names": "Windows Update for Business Deployment Service"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "RMS_S_PREMIUM",
+ "Service_Plan_Id": "6c57d4b6-3b23-47a5-9bc9-69f17b4947b3",
+ "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P1"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "RMS_S_PREMIUM2",
+ "Service_Plan_Id": "5689bec4-755d-4753-8b61-40975025187c",
+ "Service_Plans_Included_Friendly_Names": "Azure Information Protection Premium P2"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "RMS_S_ENTERPRISE",
+ "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90",
+ "Service_Plans_Included_Friendly_Names": "Azure Rights Management"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "DYN365_CDS_O365_P3",
+ "Service_Plan_Id": "28b0fa46-c39a-4188-89e2-58e979a6b014",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "Defender_for_Iot_Enterprise",
+ "Service_Plan_Id": "99cd49a9-0e54-4e07-aea1-d8d9f5f704f5",
+ "Service_Plans_Included_Friendly_Names": "Defender for IoT - Enterprise IoT Security"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "MFA_PREMIUM",
+ "Service_Plan_Id": "8a256a2b-b617-496d-b51b-e76466e88db0",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Azure Multi-Factor Authentication"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "ADALLOM_S_STANDALONE",
+ "Service_Plan_Id": "2e2ddb96-6af9-4b1d-a3f0-d6ecfd22edb2",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Cloud Apps"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "ATA",
+ "Service_Plan_Id": "14ab5db5-e6c4-4b20-b4bc-13e36fd2227f",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Defender for Identity"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "AAD_PREMIUM",
+ "Service_Plan_Id": "41781fb2-bc02-4b7c-bd55-b576c07bb09d",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P1"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "AAD_PREMIUM_P2",
+ "Service_Plan_Id": "eec0eb4f-6444-4f95-aba0-50c24d67f998",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID P2"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "INTUNE_A",
+ "Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "FLOW_O365_P3",
+ "Service_Plan_Id": "07699545-9485-468e-95b6-2fca3738be01",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
+ },
+ {
+ "Product_Display_Name": "Microsoft 365 E5 (no Teams)",
+ "String_Id": "Microsoft_365_E5_(no_Teams)",
+ "GUID": "18a4bd3f-0b5b-4887-b04f-61dd0ee15f5e",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_O365_P3",
+ "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365"
+ },
{
"Product_Display_Name": "Microsoft 365 E5 EEA (no Teams) with Calling Minutes",
"String_Id": "Microsoft_365_E5_EEA_(no_Teams)_with_Calling_Minutes",
@@ -28135,6 +30143,46 @@
"Service_Plan_Id": "e866a266-3cff-43a3-acca-0c90a7e00c8b",
"Service_Plans_Included_Friendly_Names": "Entra Identity Governance"
},
+ {
+ "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2",
+ "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2",
+ "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37",
+ "Service_Plan_Name": "Entra_Premium_Internet_Access",
+ "Service_Plan_Id": "8d23cb83-ab07-418f-8517-d7aca77307dc",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra Internet Access"
+ },
+ {
+ "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2",
+ "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2",
+ "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37",
+ "Service_Plan_Name": "Entra_Premium_Private_Access",
+ "Service_Plan_Id": "f057aab1-b184-49b2-85c0-881b02a405c5",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra Private Access"
+ },
+ {
+ "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2",
+ "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2",
+ "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37",
+ "Service_Plan_Name": "Verifiable_Credentials_Service_Request",
+ "Service_Plan_Id": "aae826b7-14cd-4691-8178-2b312f7072ea",
+ "Service_Plans_Included_Friendly_Names": "Verifiable Credentials Service Request"
+ },
+ {
+ "Product_Display_Name": "Microsoft Entra Suite Add-on for Microsoft Entra ID P2",
+ "String_Id": "Microsoft_Entra_Suite_Step_Up_for_Microsoft_Entra_ID_P2",
+ "GUID": "2ef3064c-c95c-426c-96dd-9ffeaa2f2c37",
+ "Service_Plan_Name": "Entra_Identity_Governance",
+ "Service_Plan_Id": "e866a266-3cff-43a3-acca-0c90a7e00c8b",
+ "Service_Plans_Included_Friendly_Names": "Entra Identity Governance"
+ },
+ {
+ "Product_Display_Name": "Microsoft Entra Workload ID",
+ "String_Id": "Workload_Identities_P2",
+ "GUID": "52cdf00e-8303-4223-a749-ff69a13e2dd0",
+ "Service_Plan_Name": "AAD_WRKLDID_P2",
+ "Service_Plan_Id": "7dc0e92d-bf15-401d-907e-0884efe7c760",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Entra Workload ID"
+ },
{
"Product_Display_Name": "Microsoft Fabric (Free)",
"String_Id": "POWER_BI_STANDARD",
@@ -28151,6 +30199,14 @@
"Service_Plan_Id": "2049e525-b859-401b-b2a0-e0a31c4b1fe4",
"Service_Plans_Included_Friendly_Names": "Power BI (free)"
},
+ {
+ "Product_Display_Name": "Microsoft Fabric (Free)",
+ "String_Id": "POWER_BI_STANDARD",
+ "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235",
+ "Service_Plan_Name": "PURVIEW_DISCOVERY",
+ "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522",
+ "Service_Plans_Included_Friendly_Names": "Purview Discovery"
+ },
{
"Product_Display_Name": "Microsoft Fabric (Free) for faculty",
"String_Id": "POWER_BI_STANDARD_FACULTY",
@@ -28191,6 +30247,14 @@
"Service_Plan_Id": "d736def0-1fde-43f0-a5be-e3f8b2de6e41",
"Service_Plans_Included_Friendly_Names": "MS IMAGINE ACADEMY"
},
+ {
+ "Product_Display_Name": "Microsoft Intune Advanced Analytics",
+ "String_Id": "Microsoft_Intune_Advanced_Analytics",
+ "GUID": "5e36d0d4-e9e5-4052-aba0-0257465c9b86",
+ "Service_Plan_Name": "Intune_AdvancedEA",
+ "Service_Plan_Id": "2a4baa0e-5e99-4c38-b1f2-6864960f1bd1",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Advanced Analytics"
+ },
{
"Product_Display_Name": "Microsoft Intune Device",
"String_Id": "INTUNE_A_D",
@@ -28239,6 +30303,22 @@
"Service_Plan_Id": "d216f254-796f-4dab-bbfa-710686e646b9",
"Service_Plans_Included_Friendly_Names": "Microsoft Intune G"
},
+ {
+ "Product_Display_Name": "Microsoft Intune Plan 1 A VL",
+ "String_Id": "INTUNE_A_VL",
+ "GUID": "99fc2803-fa72-42d3-ae78-b055e177d275",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Microsoft Intune Plan 1 A VL",
+ "String_Id": "INTUNE_A_VL",
+ "GUID": "99fc2803-fa72-42d3-ae78-b055e177d275",
+ "Service_Plan_Name": "INTUNE_A_VL",
+ "Service_Plan_Id": "3e170737-c728-4eae-bbb9-3f3360f7184c",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1"
+ },
{
"Product_Display_Name": "Microsoft Intune Plan 1 A VL_USGOV_GCCHIGH",
"String_Id": "INTUNE_A_VL_USGOV_GCCHIGH",
@@ -28639,6 +30719,38 @@
"Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
"Service_Plans_Included_Friendly_Names": "Exchange Foundation"
},
+ {
+ "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus",
+ "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON",
+ "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a",
+ "Service_Plan_Name": "MCS_BIZAPPS_CLOUD_FOR_SUSTAINABILITY_USL_PLUS",
+ "Service_Plan_Id": "beaf5b5c-d11c-4417-b5cb-cd9f9e6719b0",
+ "Service_Plans_Included_Friendly_Names": "MCS - BizApps Cloud for Sustainability USL Plus"
+ },
+ {
+ "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus",
+ "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON",
+ "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a",
+ "Service_Plan_Name": "POWER_APPS_FOR_MCS_USL_PLUS",
+ "Service_Plan_Id": "c5502fe7-406d-442a-827f-4948b821ba08",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Cloud for Sustainability USL Plus"
+ },
+ {
+ "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus",
+ "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON",
+ "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a",
+ "Service_Plan_Name": "POWER_AUTOMATE_FOR_MCS_USL_PLUS",
+ "Service_Plan_Id": "1c22bb50-96fb-49e5-baa6-195cab19eee2",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Cloud for Sustainability USL Plus"
+ },
+ {
+ "Product_Display_Name": "Microsoft Sustainability Manager Premium USL Plus",
+ "String_Id": "MICROSOFT_SUSTAINABILITY_MANAGER_PREMIUM_USL_ADDON",
+ "GUID": "9d576ffb-dd32-4c33-91ee-91625b61424a",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
{
"Product_Display_Name": "Microsoft Sustainability Manager USL Essentials",
"String_Id": "Microsoft_Cloud_for_Sustainability_USL",
@@ -28743,6 +30855,22 @@
"Service_Plan_Id": "346d83bf-6fe6-42ca-b424-b9300d2e21bf",
"Service_Plans_Included_Friendly_Names": "Microsoft 365 Domestic Calling Plan (240 min)"
},
+ {
+ "Product_Display_Name": "Microsoft Teams Domestic Calling Plan for GCC",
+ "String_Id": "MCOPSTN_1_GOV",
+ "GUID": "923f58ab-fca1-46a1-92f9-89fda21238a8",
+ "Service_Plan_Name": "MCOPSTN1_GOV",
+ "Service_Plan_Id": "3c8a8792-7866-409b-bb61-1b20ace0368b",
+ "Service_Plans_Included_Friendly_Names": "Domestic Calling Plan for Government"
+ },
+ {
+ "Product_Display_Name": "Microsoft Teams Domestic Calling Plan for GCC",
+ "String_Id": "MCOPSTN_1_GOV",
+ "GUID": "923f58ab-fca1-46a1-92f9-89fda21238a8",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION_GOV",
+ "Service_Plan_Id": "922ba911-5694-4e99-a794-73aed9bfeec8",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation for Government"
+ },
{
"Product_Display_Name": "Microsoft Teams Essentials",
"String_Id": "Teams_Ess",
@@ -29525,7 +31653,7 @@
"GUID": "c25e2b36-e161-4946-bef2-69239729f690",
"Service_Plan_Name": "AAD_BASIC_EDU",
"Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education"
+ "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education"
},
{
"Product_Display_Name": "Microsoft Teams Rooms Pro for EDU",
@@ -29567,6 +31695,14 @@
"Service_Plan_Id": "0feaeb32-d00e-4d66-bd5a-43b5b83db82c",
"Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)"
},
+ {
+ "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU",
+ "String_Id": "Microsoft_Teams_Rooms_Pro_FAC",
+ "GUID": "c25e2b36-e161-4946-bef2-69239729f690",
+ "Service_Plan_Name": "Teams_Rooms_Pro",
+ "Service_Plan_Id": "0374d34c-6be4-4dbb-b3f0-26105db0b28a",
+ "Service_Plans_Included_Friendly_Names": "Teams Rooms Pro"
+ },
{
"Product_Display_Name": "Microsoft Teams Rooms Pro for EDU",
"String_Id": "Microsoft_Teams_Rooms_Pro_FAC",
@@ -29615,6 +31751,14 @@
"Service_Plan_Id": "c1ec4a95-1f05-45b3-a911-aa3fa01094f5",
"Service_Plans_Included_Friendly_Names": "Microsoft Intune Plan 1"
},
+ {
+ "Product_Display_Name": "Microsoft Teams Rooms Pro for EDU",
+ "String_Id": "Microsoft_Teams_Rooms_Pro_FAC",
+ "GUID": "c25e2b36-e161-4946-bef2-69239729f690",
+ "Service_Plan_Name": "SPECIALTY_DEVICES",
+ "Service_Plan_Id": "cfce7ae3-4b41-4438-999c-c0e91f3b7fb9",
+ "Service_Plans_Included_Friendly_Names": "Specialty devices"
+ },
{
"Product_Display_Name": "Microsoft Teams Rooms Pro for GCC",
"String_Id": "Microsoft_Teams_Rooms_Pro_GCC",
@@ -30373,15 +32517,15 @@
"GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
"Service_Plan_Name": "AAD_BASIC_EDU",
"Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education"
+ "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education"
},
{
"Product_Display_Name": "Office 365 A1 for faculty",
"String_Id": "STANDARDWOFFPACK_FACULTY",
"GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
- "Service_Plan_Name": "DYN365_CDS_O365_P1",
- "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4",
- "Service_Plans_Included_Friendly_Names": "Common Data Service - O365 P1"
+ "Service_Plan_Name": "RMS_S_ENTERPRISE",
+ "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90",
+ "Service_Plans_Included_Friendly_Names": "Azure Rights Management"
},
{
"Product_Display_Name": "Office 365 A1 for faculty",
@@ -30411,9 +32555,9 @@
"Product_Display_Name": "Office 365 A1 for faculty",
"String_Id": "STANDARDWOFFPACK_FACULTY",
"GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
- "Service_Plan_Name": "RMS_S_ENTERPRISE",
- "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90",
- "Service_Plans_Included_Friendly_Names": "Microsoft Microsoft Entra Rights"
+ "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1",
+ "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)"
},
{
"Product_Display_Name": "Office 365 A1 for faculty",
@@ -30429,7 +32573,7 @@
"GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
"Service_Plan_Name": "KAIZALA_O365_P2",
"Service_Plan_Id": "54fc630f-5a40-48ee-8965-af0503c1386e",
- "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro Plan 2"
+ "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro"
},
{
"Product_Display_Name": "Office 365 A1 for faculty",
@@ -30507,25 +32651,17 @@
"Product_Display_Name": "Office 365 A1 for faculty",
"String_Id": "STANDARDWOFFPACK_FACULTY",
"GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
- "Service_Plan_Name": "POWERAPPS_O365_P2",
- "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792",
- "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365"
- },
- {
- "Product_Display_Name": "Office 365 A1 for faculty",
- "String_Id": "STANDARDWOFFPACK_FACULTY",
- "GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
- "Service_Plan_Name": "FLOW_O365_P2",
- "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9",
- "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
+ "Service_Plan_Name": "PROJECT_O365_P1",
+ "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2",
+ "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)"
},
{
"Product_Display_Name": "Office 365 A1 for faculty",
"String_Id": "STANDARDWOFFPACK_FACULTY",
"GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
- "Service_Plan_Name": "PROJECT_O365_P1",
- "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2",
- "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)"
+ "Service_Plan_Name": "Bing_Chat_Enterprise",
+ "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot"
},
{
"Product_Display_Name": "Office 365 A1 for faculty",
@@ -30591,6 +32727,30 @@
"Service_Plan_Id": "2078e8df-cff6-4290-98cb-5408261a760a",
"Service_Plans_Included_Friendly_Names": "Yammer for Academic"
},
+ {
+ "Product_Display_Name": "Office 365 A1 for faculty",
+ "String_Id": "STANDARDWOFFPACK_FACULTY",
+ "GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
+ "Service_Plan_Name": "DYN365_CDS_O365_P1",
+ "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service"
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for faculty",
+ "String_Id": "STANDARDWOFFPACK_FACULTY",
+ "GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
+ "Service_Plan_Name": "POWERAPPS_O365_P2",
+ "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365"
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for faculty",
+ "String_Id": "STANDARDWOFFPACK_FACULTY",
+ "GUID": "94763226-9b3c-4e75-a931-5c89701abe66",
+ "Service_Plan_Name": "FLOW_O365_P2",
+ "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
+ },
{
"Product_Display_Name": "Office 365 A1 Plus for faculty",
"String_Id": "STANDARDWOFFPACK_IW_FACULTY",
@@ -30813,15 +32973,15 @@
"GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
"Service_Plan_Name": "AAD_BASIC_EDU",
"Service_Plan_Id": "1d0f309f-fdf9-4b2a-9ae7-9c48b91f1426",
- "Service_Plans_Included_Friendly_Names": "Microsoft Entra ID Basic for Education"
+ "Service_Plans_Included_Friendly_Names": "Azure Active Directory Basic for Education"
},
{
"Product_Display_Name": "Office 365 A1 for students",
"String_Id": "STANDARDWOFFPACK_STUDENT",
"GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
- "Service_Plan_Name": "DYN365_CDS_O365_P1",
- "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4",
- "Service_Plans_Included_Friendly_Names": "Common Data Service - O365 P1"
+ "Service_Plan_Name": "RMS_S_ENTERPRISE",
+ "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90",
+ "Service_Plans_Included_Friendly_Names": "Azure Rights Management"
},
{
"Product_Display_Name": "Office 365 A1 for students",
@@ -30851,9 +33011,9 @@
"Product_Display_Name": "Office 365 A1 for students",
"String_Id": "STANDARDWOFFPACK_STUDENT",
"GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
- "Service_Plan_Name": "RMS_S_ENTERPRISE",
- "Service_Plan_Id": "bea4c11e-220a-4e6d-8eb8-8ea15d019f90",
- "Service_Plans_Included_Friendly_Names": "Microsoft Microsoft Entra Rights"
+ "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1",
+ "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)"
},
{
"Product_Display_Name": "Office 365 A1 for students",
@@ -30869,7 +33029,7 @@
"GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
"Service_Plan_Name": "KAIZALA_O365_P2",
"Service_Plan_Id": "54fc630f-5a40-48ee-8965-af0503c1386e",
- "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro Plan 2"
+ "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro"
},
{
"Product_Display_Name": "Office 365 A1 for students",
@@ -30939,25 +33099,17 @@
"Product_Display_Name": "Office 365 A1 for students",
"String_Id": "STANDARDWOFFPACK_STUDENT",
"GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
- "Service_Plan_Name": "POWERAPPS_O365_P2",
- "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792",
- "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365"
- },
- {
- "Product_Display_Name": "Office 365 A1 for students",
- "String_Id": "STANDARDWOFFPACK_STUDENT",
- "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
- "Service_Plan_Name": "FLOW_O365_P2",
- "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9",
- "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
+ "Service_Plan_Name": "PROJECT_O365_P1",
+ "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2",
+ "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)"
},
{
"Product_Display_Name": "Office 365 A1 for students",
"String_Id": "STANDARDWOFFPACK_STUDENT",
"GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
- "Service_Plan_Name": "PROJECT_O365_P1",
- "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2",
- "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)"
+ "Service_Plan_Name": "Bing_Chat_Enterprise",
+ "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot"
},
{
"Product_Display_Name": "Office 365 A1 for students",
@@ -31015,6 +33167,30 @@
"Service_Plan_Id": "2078e8df-cff6-4290-98cb-5408261a760a",
"Service_Plans_Included_Friendly_Names": "Yammer for Academic"
},
+ {
+ "Product_Display_Name": "Office 365 A1 for students",
+ "String_Id": "STANDARDWOFFPACK_STUDENT",
+ "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
+ "Service_Plan_Name": "DYN365_CDS_O365_P1",
+ "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service"
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for students",
+ "String_Id": "STANDARDWOFFPACK_STUDENT",
+ "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
+ "Service_Plan_Name": "POWERAPPS_O365_P2",
+ "Service_Plan_Id": "c68f8d98-5534-41c8-bf36-22fa496fa792",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365"
+ },
+ {
+ "Product_Display_Name": "Office 365 A1 for students",
+ "String_Id": "STANDARDWOFFPACK_STUDENT",
+ "GUID": "314c4481-f395-4525-be8b-2ec4bb1e9d91",
+ "Service_Plan_Name": "FLOW_O365_P2",
+ "Service_Plan_Id": "76846ad7-7776-4c40-a281-a386362dd1b9",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
+ },
{
"Product_Display_Name": "Office 365 A1 Plus for students",
"String_Id": "STANDARDWOFFPACK_IW_STUDENT",
@@ -33215,6 +35391,310 @@
"Service_Plan_Id": "0683001c-0492-4d59-9515-d9a6426b5813",
"Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365"
},
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MESH_AVATARS_FOR_TEAMS",
+ "Service_Plan_Id": "dcf9d2f4-772e-4434-b757-77a453cfbc02",
+ "Service_Plans_Included_Friendly_Names": "Avatars for Teams"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MESH_AVATARS_ADDITIONAL_FOR_TEAMS",
+ "Service_Plan_Id": "3efbd4ed-8958-4824-8389-1321f8730af8",
+ "Service_Plans_Included_Friendly_Names": "Avatars for Teams (additional)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "CDS_O365_P1",
+ "Service_Plan_Id": "bed136c6-b799-4462-824d-fc045d3a9d25",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service for Teams"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MICROSOFT_MYANALYTICS_FULL",
+ "Service_Plan_Id": "0403bb98-9d17-4f94-b53e-eca56a7698a6",
+ "Service_Plans_Included_Friendly_Names": "DO NOT USE - Microsoft MyAnalytics (Full)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "EXCHANGE_S_STANDARD",
+ "Service_Plan_Id": "9aaf7827-d63c-4b61-89c3-182f06f82e5c",
+ "Service_Plans_Included_Friendly_Names": "Exchange Online (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "GRAPH_CONNECTORS_SEARCH_INDEX",
+ "Service_Plan_Id": "a6520331-d7d4-4276-95f5-15c0933bc757",
+ "Service_Plans_Included_Friendly_Names": "Graph Connectors Search with Index"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MESH_IMMERSIVE_FOR_TEAMS",
+ "Service_Plan_Id": "f0ff6ac6-297d-49cd-be34-6dfef97f0c28",
+ "Service_Plans_Included_Friendly_Names": "Immersive spaces for Teams"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MYANALYTICS_P2",
+ "Service_Plan_Id": "33c4f319-9bdd-48d6-9c4d-410b750a4a5a",
+ "Service_Plans_Included_Friendly_Names": "Insights by MyAnalytics"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "INSIGHTS_BY_MYANALYTICS",
+ "Service_Plan_Id": "b088306e-925b-44ab-baa0-63291c629a91",
+ "Service_Plans_Included_Friendly_Names": "Insights by MyAnalytics Backend"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "M365_LIGHTHOUSE_CUSTOMER_PLAN1",
+ "Service_Plan_Id": "6f23d6a9-adbf-481c-8538-b4c095654487",
+ "Service_Plans_Included_Friendly_Names": "Microsoft 365 Lighthouse (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MICROSOFTBOOKINGS",
+ "Service_Plan_Id": "199a5c09-e0ca-4e37-8f7c-b05d533e1ea2",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Bookings"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "FORMS_PLAN_E1",
+ "Service_Plan_Id": "159f4cd6-e380-449f-a816-af1a9ef76344",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Forms (Plan E1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "KAIZALA_O365_P2",
+ "Service_Plan_Id": "54fc630f-5a40-48ee-8965-af0503c1386e",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Kaizala Pro"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "PROJECTWORKMANAGEMENT",
+ "Service_Plan_Id": "b737dad2-2f6c-4c65-90e3-ca563267e8b9",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Planner"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MICROSOFT_SEARCH",
+ "Service_Plan_Id": "94065c59-bc8e-4e8b-89e5-5138d471eaff",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Search"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "Deskless",
+ "Service_Plan_Id": "8c7d2df8-86f0-4902-b2ed-a0458298f3b3",
+ "Service_Plans_Included_Friendly_Names": "Microsoft StaffHub"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "INTUNE_O365",
+ "Service_Plan_Id": "882e1d05-acd1-4ccb-8708-6ee03664b117",
+ "Service_Plans_Included_Friendly_Names": "Mobile Device Management for Office 365"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "Nucleus",
+ "Service_Plan_Id": "db4d623d-b514-490b-b7ef-8885eee514de",
+ "Service_Plans_Included_Friendly_Names": "Nucleus"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "SHAREPOINTWAC",
+ "Service_Plan_Id": "e95bec33-7c88-4a70-8e19-b10bd9d0c014",
+ "Service_Plans_Included_Friendly_Names": "Office for the Web"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "OFFICEMOBILE_SUBSCRIPTION",
+ "Service_Plan_Id": "c63d4d19-e8cb-460e-b37c-4d6c34603745",
+ "Service_Plans_Included_Friendly_Names": "Office Mobile Apps for Office 365"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "PEOPLE_SKILLS_FOUNDATION",
+ "Service_Plan_Id": "13b6da2c-0d84-450e-9f69-a33e221387ca",
+ "Service_Plans_Included_Friendly_Names": "People Skills - Foundation"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "PROJECT_O365_P1",
+ "Service_Plan_Id": "a55dfd10-0864-46d9-a3cd-da5991a3e0e2",
+ "Service_Plans_Included_Friendly_Names": "Project for Office (Plan E1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "Bing_Chat_Enterprise",
+ "Service_Plan_Id": "0d0c0d31-fae7-41f2-b909-eaf4d7f26dba",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Commercial data protection for Microsoft Copilot"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "PLACES_CORE",
+ "Service_Plan_Id": "1fe6227d-3e01-46d0-9510-0acad4ff6e94",
+ "Service_Plans_Included_Friendly_Names": "RETIRED - Places Core"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "SHAREPOINTSTANDARD",
+ "Service_Plan_Id": "c7699d2e-19aa-44de-8edf-1736da088ca1",
+ "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "MCOSTANDARD",
+ "Service_Plan_Id": "0feaeb32-d00e-4d66-bd5a-43b5b83db82c",
+ "Service_Plans_Included_Friendly_Names": "Skype for Business Online (Plan 2)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "SWAY",
+ "Service_Plan_Id": "a23b959c-7ce8-4e57-9140-b90eb88a9e97",
+ "Service_Plans_Included_Friendly_Names": "Sway"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "BPOS_S_TODO_1",
+ "Service_Plan_Id": "5e62787c-c316-451f-b873-1d05acd4d12c",
+ "Service_Plans_Included_Friendly_Names": "To-Do (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "VIVAENGAGE_CORE",
+ "Service_Plan_Id": "a82fbf69-b4d7-49f4-83a6-915b2cf354f4",
+ "Service_Plans_Included_Friendly_Names": "Viva Engage Core"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "VIVA_LEARNING_SEEDED",
+ "Service_Plan_Id": "b76fb638-6ba6-402a-b9f9-83d28acb3d86",
+ "Service_Plans_Included_Friendly_Names": "Viva Learning Seeded"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "WHITEBOARD_PLAN1",
+ "Service_Plan_Id": "b8afc642-032e-4de5-8c0a-507a7bba7e5d",
+ "Service_Plans_Included_Friendly_Names": "Whiteboard (Plan 1)"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "YAMMER_ENTERPRISE",
+ "Service_Plan_Id": "7547a3fe-08ee-4ccb-b430-5077c5041653",
+ "Service_Plans_Included_Friendly_Names": "Yammer Enterprise"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "DYN365_CDS_O365_P1",
+ "Service_Plan_Id": "40b010bb-0b69-4654-ac5e-ba161433f4b4",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "RMS_S_BASIC",
+ "Service_Plan_Id": "31cf2cfc-6b0d-4adc-a336-88b724ed8122",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Azure Rights Management Service"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "STREAM_O365_E1",
+ "Service_Plan_Id": "743dd19e-1ce3-4c62-a3ad-49ba8f63a2f6",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Stream for Office 365 E1"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "POWERAPPS_O365_P1",
+ "Service_Plan_Id": "92f7a6f3-b89b-4bbd-8c30-809e6da5ad1c",
+ "Service_Plans_Included_Friendly_Names": "Power Apps for Office 365"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "FLOW_O365_P1",
+ "Service_Plan_Id": "0f9b09cb-62d1-4ff4-9129-43f4996f83f4",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Office 365"
+ },
+ {
+ "Product_Display_Name": "Office 365 E1 (no Teams)",
+ "String_Id": "Office_365_E1_(no_Teams)",
+ "GUID": "f8ced641-8e17-4dc5-b014-f5a2d53f6ac8",
+ "Service_Plan_Name": "POWER_VIRTUAL_AGENTS_O365_P1",
+ "Service_Plan_Id": "0683001c-0492-4d59-9515-d9a6426b5813",
+ "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365"
+ },
{
"Product_Display_Name": "Office 365 E1 EEA (no Teams)",
"String_Id": "Office_365_w/o_Teams_Bundle_E1",
@@ -39109,7 +41589,7 @@
"GUID": "0f13a262-dc6f-4800-8dc6-a62f72c95fad",
"Service_Plan_Name": "CDSAICAPACITY_PERUSER",
"Service_Plan_Id": "91f50f7b-2204-4803-acac-5cf5668b8b39",
- "Service_Plans_Included_Friendly_Names": "DO NOT USE - AI Builder capacity Per User add-on"
+ "Service_Plans_Included_Friendly_Names": "AI Builder capacity Per User add-on"
},
{
"Product_Display_Name": "PowerApps & Flow GCC Test - O365 & Dyn365 Plans",
@@ -39531,9 +42011,9 @@
"Product_Display_Name": "Power Apps Premium",
"String_Id": "POWERAPPS_PER_USER",
"GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579",
- "Service_Plan_Name": "DYN365_CDS_P2",
- "Service_Plan_Id": "6ea4c1ef-c259-46df-bce2-943342cd3cb2",
- "Service_Plans_Included_Friendly_Names": "Common Data Service - P2"
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
},
{
"Product_Display_Name": "Power Apps Premium",
@@ -39543,6 +42023,30 @@
"Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
"Service_Plans_Included_Friendly_Names": "Exchange Foundation"
},
+ {
+ "Product_Display_Name": "Power Apps Premium",
+ "String_Id": "POWERAPPS_PER_USER",
+ "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579",
+ "Service_Plan_Name": "CDSAICAPACITY_PERUSER_NEW",
+ "Service_Plan_Id": "74d93933-6f22-436e-9441-66d205435abb",
+ "Service_Plans_Included_Friendly_Names": "AI Builder capacity Per User add-on"
+ },
+ {
+ "Product_Display_Name": "Power Apps Premium",
+ "String_Id": "POWERAPPS_PER_USER",
+ "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579",
+ "Service_Plan_Name": "DYN365_CDS_P2",
+ "Service_Plan_Id": "6ea4c1ef-c259-46df-bce2-943342cd3cb2",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service"
+ },
+ {
+ "Product_Display_Name": "Power Apps Premium",
+ "String_Id": "POWERAPPS_PER_USER",
+ "GUID": "b30411f5-fea1-4a59-9ad9-3db7c7ead579",
+ "Service_Plan_Name": "CDSAICAPACITY_PERUSER",
+ "Service_Plan_Id": "91f50f7b-2204-4803-acac-5cf5668b8b39",
+ "Service_Plans_Included_Friendly_Names": "DO NOT USE - AI Builder capacity Per User add-on"
+ },
{
"Product_Display_Name": "Power Apps Premium",
"String_Id": "POWERAPPS_PER_USER",
@@ -39559,6 +42063,46 @@
"Service_Plan_Id": "dc789ed8-0170-4b65-a415-eb77d5bb350a",
"Service_Plans_Included_Friendly_Names": "Power Automate for Power Apps per User Plan"
},
+ {
+ "Product_Display_Name": "Power Apps Premium embedded",
+ "String_Id": "POWERAPPS_PER_USER_ISVEMB",
+ "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba",
+ "Service_Plan_Name": "Power_Pages_Internal_User",
+ "Service_Plan_Id": "60bf28f9-2b70-4522-96f7-335f5e06c941",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Internal User"
+ },
+ {
+ "Product_Display_Name": "Power Apps Premium embedded",
+ "String_Id": "POWERAPPS_PER_USER_ISVEMB",
+ "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba",
+ "Service_Plan_Name": "EXCHANGE_S_FOUNDATION",
+ "Service_Plan_Id": "113feb6c-3fe4-4440-bddc-54d774bf0318",
+ "Service_Plans_Included_Friendly_Names": "Exchange Foundation"
+ },
+ {
+ "Product_Display_Name": "Power Apps Premium embedded",
+ "String_Id": "POWERAPPS_PER_USER_ISVEMB",
+ "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba",
+ "Service_Plan_Name": "DYN365_CDS_P2",
+ "Service_Plan_Id": "6ea4c1ef-c259-46df-bce2-943342cd3cb2",
+ "Service_Plans_Included_Friendly_Names": "Common Data Service"
+ },
+ {
+ "Product_Display_Name": "Power Apps Premium embedded",
+ "String_Id": "POWERAPPS_PER_USER_ISVEMB",
+ "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba",
+ "Service_Plan_Name": "POWERAPPS_PER_USER",
+ "Service_Plan_Id": "ea2cf03b-ac60-46ae-9c1d-eeaeb63cec86",
+ "Service_Plans_Included_Friendly_Names": "Power Apps per User Plan"
+ },
+ {
+ "Product_Display_Name": "Power Apps Premium embedded",
+ "String_Id": "POWERAPPS_PER_USER_ISVEMB",
+ "GUID": "2a6fb3c6-30cc-4558-a69d-032425c1a3ba",
+ "Service_Plan_Name": "Flow_PowerApps_PerUser",
+ "Service_Plan_Id": "dc789ed8-0170-4b65-a415-eb77d5bb350a",
+ "Service_Plans_Included_Friendly_Names": "Power Automate for Power Apps per User Plan"
+ },
{
"Product_Display_Name": "Power Apps Premium for Government",
"String_Id": "POWERAPPS_PER_USER_GCC",
@@ -40095,6 +42639,22 @@
"Service_Plan_Id": "0bf3c642-7bb5-4ccc-884e-59d09df0266c",
"Service_Plans_Included_Friendly_Names": "Power BI Premium Per User"
},
+ {
+ "Product_Display_Name": "Power BI Premium Per User Add-On for Faculty",
+ "String_Id": "PBI_PREMIUM_PER_USER_ADDON_FACULTY",
+ "GUID": "c05b235f-be75-4029-8851-6a4170758eef",
+ "Service_Plan_Name": "BI_AZURE_P3",
+ "Service_Plan_Id": "0bf3c642-7bb5-4ccc-884e-59d09df0266c",
+ "Service_Plans_Included_Friendly_Names": "Power BI Premium Per User"
+ },
+ {
+ "Product_Display_Name": "Power BI Premium Per User Add-On for Faculty",
+ "String_Id": "PBI_PREMIUM_PER_USER_ADDON_FACULTY",
+ "GUID": "c05b235f-be75-4029-8851-6a4170758eef",
+ "Service_Plan_Name": "PURVIEW_DISCOVERY",
+ "Service_Plan_Id": "c948ea65-2053-4a5a-8a62-9eaaaf11b522",
+ "Service_Plans_Included_Friendly_Names": "Purview Discovery"
+ },
{
"Product_Display_Name": "Power BI Premium Per User Add-On for GCC",
"String_Id": "PBI_PREMIUM_PER_USER_ADDON_CE_GCC",
@@ -40311,6 +42871,22 @@
"Service_Plan_Id": "18e74ca2-b5f0-4802-9a8b-00d2ff1e8322",
"Service_Plans_Included_Friendly_Names": "Power Pages Authenticated Users per site monthly capacity GCCH"
},
+ {
+ "Product_Display_Name": "Power Pages authenticated users T1 100 users/per site/month capacity pack CN_CN",
+ "String_Id": "Power Pages authenticated users T1_CN_CN",
+ "GUID": "9a3c2a19-06c0-41b1-b2ea-13528d7b2e17",
+ "Service_Plan_Name": "DV_PowerPages_Authenticated_User",
+ "Service_Plan_Id": "7aae746a-3463-4737-b295-3c1a16c31438",
+ "Service_Plans_Included_Friendly_Names": "Dataverse for Power Pages Authenticated users per site"
+ },
+ {
+ "Product_Display_Name": "Power Pages authenticated users T1 100 users/per site/month capacity pack CN_CN",
+ "String_Id": "Power Pages authenticated users T1_CN_CN",
+ "GUID": "9a3c2a19-06c0-41b1-b2ea-13528d7b2e17",
+ "Service_Plan_Name": "PowerPages_Authenticated_User_CN",
+ "Service_Plan_Id": "967d9574-a076-4bb7-ab89-f41f64bc142e",
+ "Service_Plans_Included_Friendly_Names": "Power Pages Authenticated Users per site monthly capacity China"
+ },
{
"Product_Display_Name": "Power Pages authenticated users T1 100 users/per site/month capacity pack_GCC",
"String_Id": "Power_Pages_authenticated_users_T1_100_users/per_site/month_capacity_pack_GCC",
@@ -42447,6 +45023,70 @@
"Service_Plan_Id": "78b58230-ec7e-4309-913c-93a45cc4735b",
"Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Webinar"
},
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "MICROSOFT_ECDN",
+ "Service_Plan_Id": "85704d55-2e73-47ee-93b4-4b8ea14db92b",
+ "Service_Plans_Included_Friendly_Names": "Microsoft eCDN"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "TEAMSPRO_MGMT",
+ "Service_Plan_Id": "0504111f-feb8-4a3c-992a-70280f9a2869",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Intelligent"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "TEAMSPRO_CUST",
+ "Service_Plan_Id": "cc8c0802-a325-43df-8cba-995d0c6cb373",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Personalized"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "TEAMSPRO_PROTECTION",
+ "Service_Plan_Id": "f8b44f54-18bb-46a3-9658-44ab58712968",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Secure"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "TEAMSPRO_VIRTUALAPPT",
+ "Service_Plan_Id": "9104f592-f2a7-4f77-904c-ca5a5715883f",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Virtual Appointment"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "MCO_VIRTUAL_APPT",
+ "Service_Plan_Id": "711413d0-b36e-4cd4-93db-0a50a4ab7ea3",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Virtual Appointments"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "TEAMSPRO_WEBINAR",
+ "Service_Plan_Id": "78b58230-ec7e-4309-913c-93a45cc4735b",
+ "Service_Plans_Included_Friendly_Names": "Microsoft Teams Premium Webinar"
+ },
+ {
+ "Product_Display_Name": "Teams Premium for Faculty",
+ "String_Id": "Teams_Premium_for_Faculty",
+ "GUID": "960a972f-d017-4a17-8f64-b42c8035bc7d",
+ "Service_Plan_Name": "QUEUES_APP",
+ "Service_Plan_Id": "ab2d4fb5-f80a-4bf1-a11d-7f1da254041b",
+ "Service_Plans_Included_Friendly_Names": "Queues app for Microsoft Teams"
+ },
{
"Product_Display_Name": "Teams Rooms Premium",
"String_Id": "MTR_PREM",
diff --git a/src/data/alerts.json b/src/data/alerts.json
index 4779d23e58e1..082fe856f297 100644
--- a/src/data/alerts.json
+++ b/src/data/alerts.json
@@ -9,6 +9,16 @@
"label": "Alert on admins without any form of MFA",
"recommendedRunInterval": "1d"
},
+ {
+ "name": "LicenseAssignmentErrors",
+ "label": "Alert on license assignment errors",
+ "recommendedRunInterval": "1d"
+ },
+ {
+ "name": "AlertSmtpAuthSuccess",
+ "label": "Alert on SMTP AUTH usage with success, helps to phase out SMTP AUTH (Entra P1 Required)",
+ "recommendedRunInterval": "1d"
+ },
{
"name": "NoCAConfig",
"label": "Alert on tenants without a Conditional Access policy, while having Conditional Access licensing available.",
@@ -28,6 +38,15 @@
"inputName": "InactiveLicensedUsersExcludeDisabled",
"recommendedRunInterval": "1d"
},
+ {
+ "name": "EntraConnectSyncStatus",
+ "label": "Alert if Entra Connect sync is enabled and has not run in the last X hours",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Hours(Default:72)",
+ "inputName": "EntraConnectSyncStatusHours",
+ "recommendedRunInterval": "1d"
+ },
{
"name": "QuotaUsed",
"label": "Alert on % mailbox quota used",
@@ -46,6 +65,15 @@
"inputName": "SharePointQuota",
"recommendedRunInterval": "4h"
},
+ {
+ "name": "OneDriveQuota",
+ "label": "Alert on % OneDrive quota used",
+ "requiresInput": true,
+ "inputType": "textField",
+ "inputLabel": "Enter quota percentage (default: 90)",
+ "inputName": "OneDriveQuota",
+ "recommendedRunInterval": "4h"
+ },
{
"name": "ExpiringLicenses",
"label": "Alert on licenses expiring in 30 days",
@@ -76,6 +104,16 @@
"label": "Alert on new Defender Incidents found",
"recommendedRunInterval": "4h"
},
+ {
+ "name": "Vulnerabilities",
+ "label": "Alert on vulnerabilities older than X hours",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Alert on vulnerabilities first seen more than X hours ago (default: 24)",
+ "inputName": "AgeInHours",
+ "recommendedRunInterval": "4h",
+ "description": "Monitors for software vulnerabilities that were first discovered more than the specified number of hours ago. This helps identify lingering vulnerabilities that may have been missed or not yet remediated. Requires Defender for Endpoint/Business."
+ },
{
"name": "UnusedLicenses",
"label": "Alert on unused licenses",
@@ -140,10 +178,73 @@
"label": "Alert on (new) potentially breached passwords. Generates an alert if a password is found to be breached.",
"recommendedRunInterval": "7d"
},
+ {
+ "name": "LicensedUsersWithRoles",
+ "label": "Alert on licensed users with any administrator roles",
+ "recommendedRunInterval": "7d"
+ },
{
"name": "HuntressRogueApps",
"label": "Alert on Huntress Rogue Apps detected",
"recommendedRunInterval": "4h",
- "description": "Huntress has provided a repository of known rogue apps that are commonly used in BEC, data exfiltration and other Microsoft 365 attacks. This alert will notify you if any of these apps are detected in the selected tenant(s). For more information, see https://huntresslabs.github.io/rogueapps/."
+ "description": "Huntress has provided a repository of known rogue apps that are commonly used in BEC, data exfiltration and other Microsoft 365 attacks. This alert will notify you if any of these apps are detected in the selected tenant(s). For more information, see https://huntresslabs.github.io/rogueapps/.",
+ "requiresInput": true,
+ "inputType": "switch",
+ "inputLabel": "Ignore Disabled Apps?",
+ "inputName": "IgnoreDisabledApps"
+ },
+ {
+ "name": "TERRL",
+ "label": "Alert when Tenant External Recipient Rate Limit exceeds X %",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Alert % (default: 80)",
+ "inputName": "TERRLThreshold",
+ "recommendedRunInterval": "1h",
+ "description": "Monitors tenant outbound email volume against Microsoft's TERRL limits. Tenant data is updated every hour."
+ },
+ {
+ "name": "LowDomainScore",
+ "label": "Alert on domains with low security score",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Alert when score is below % (default: 70)",
+ "inputName": "InputValue",
+ "recommendedRunInterval": "7d",
+ "description": "Monitors domain security scores from the DomainAnalyser and alerts when scores fall below the specified threshold."
+ },
+ {
+ "name": "MXRecordChanged",
+ "label": "Alert on MX record changes",
+ "recommendedRunInterval": "1d",
+ "description": "Monitors MX records for all domains and alerts when changes are detected. This helps identify potential mail routing changes that could indicate security issues or unauthorized modifications."
+ },
+ {
+ "name": "GlobalAdminNoAltEmail",
+ "label": "Alert on Global Admin accounts without alternate email address",
+ "recommendedRunInterval": "7d",
+ "description": "Monitors Global Admin accounts and alerts when they don't have an alternate email address set, which is important for password recovery of key accounts."
+ },
+ {
+ "name": "NewRiskyUsers",
+ "label": "Alert on new risky users (P2 License Required)",
+ "recommendedRunInterval": "30m",
+ "description": "Monitors for new risky users in the tenant. Risky users are defined as users who have performed actions that are considered risky, such as password resets, MFA failures, or suspicious activity."
+ },
+ {
+ "name": "LowTenantAlignment",
+ "label": "Alert on low tenant alignment percentage",
+ "requiresInput": true,
+ "inputType": "number",
+ "inputLabel": "Alert when alignment is below % (default: 99)",
+ "inputName": "InputValue",
+ "recommendedRunInterval": "1d",
+ "description": "Monitors tenant alignment scores against standards templates and alerts when the alignment percentage falls below the specified threshold. This helps ensure compliance across all managed tenants."
+ },
+ {
+ "name": "RestrictedUsers",
+ "label": "Alert on users restricted from sending email",
+ "recommendedRunInterval": "30m",
+ "description": "Monitors for users who have been restricted from sending email due to exceeding outbound spam limits. These users typically indicate a compromised account that needs immediate attention."
}
]
diff --git a/src/data/cipp-roles.json b/src/data/cipp-roles.json
new file mode 100644
index 000000000000..f95e32fa18c6
--- /dev/null
+++ b/src/data/cipp-roles.json
@@ -0,0 +1,23 @@
+{
+ "readonly": {
+ "include": ["*.Read"],
+ "exclude": ["CIPP.SuperAdmin.*"]
+ },
+ "editor": {
+ "include": ["*.Read", "*.ReadWrite"],
+ "exclude": [
+ "CIPP.SuperAdmin.*",
+ "CIPP.Admin.*",
+ "CIPP.AppSettings.*",
+ "Tenant.Standards.ReadWrite"
+ ]
+ },
+ "admin": {
+ "include": ["*"],
+ "exclude": ["CIPP.SuperAdmin.*"]
+ },
+ "superadmin": {
+ "include": ["*"],
+ "exclude": []
+ }
+}
diff --git a/src/data/portals.json b/src/data/portals.json
index 2a3d78cdd3ae..5c8011ebff77 100644
--- a/src/data/portals.json
+++ b/src/data/portals.json
@@ -79,5 +79,23 @@
"target": "_blank",
"external": true,
"icon": "ShieldMoon"
+ },
+ {
+ "label": "Power Platform Portal",
+ "name": "Power_Platform_Portal",
+ "url": "https://admin.powerplatform.microsoft.com/account/login/customerId",
+ "variable": "customerId",
+ "target": "_blank",
+ "external": true,
+ "icon": "PrecisionManufacturing"
+ },
+ {
+ "label": "Power BI Portal",
+ "name": "Power_BI_Portal",
+ "url": "https://app.powerbi.com/admin-portal?ctid=customerId",
+ "variable": "customerId",
+ "target": "_blank",
+ "external": true,
+ "icon": "BarChart"
}
]
\ No newline at end of file
diff --git a/src/data/standards.json b/src/data/standards.json
index 6af613f0a7eb..3706598d7d45 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -5,6 +5,7 @@
"tag": [],
"helpText": "Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information.",
"docsDescription": "",
+ "executiveText": "Establishes designated contact email addresses for receiving important Microsoft 365 subscription updates and notifications. This ensures proper communication channels are maintained for general, security, marketing, and technical matters, improving organizational responsiveness to critical system updates.",
"addedComponent": [
{
"type": "textField",
@@ -38,11 +39,86 @@
"powershellEquivalent": "Set-MsolCompanyContactInformation",
"recommendedBy": []
},
+ {
+ "name": "standards.DeployMailContact",
+ "cat": "Exchange Standards",
+ "tag": [],
+ "helpText": "Creates a new mail contact in Exchange Online across all selected tenants. The contact will be visible in the Global Address List.",
+ "docsDescription": "This standard creates a new mail contact in Exchange Online. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios.",
+ "executiveText": "Automatically creates external email contacts in the organization's address book, enabling seamless communication with external partners and vendors. This standardizes contact management across all company locations and improves collaboration efficiency.",
+ "addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.DeployMailContact.ExternalEmailAddress",
+ "label": "External Email Address",
+ "required": true
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployMailContact.DisplayName",
+ "label": "Display Name",
+ "required": true
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployMailContact.FirstName",
+ "label": "First Name",
+ "required": false
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployMailContact.LastName",
+ "label": "Last Name",
+ "required": false
+ }
+ ],
+ "label": "Deploy Mail Contact",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2024-03-19",
+ "powershellEquivalent": "New-MailContact",
+ "recommendedBy": ["CIPP"]
+ },
+ {
+ "name": "standards.DeployContactTemplates",
+ "cat": "Exchange Standards",
+ "tag": [],
+ "helpText": "Creates new mail contacts in Exchange Online across all selected tenants based on the selected templates. The contact will be visible in the Global Address List unless hidden.",
+ "docsDescription": "This standard creates new mail contacts in Exchange Online based on the selected templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios.",
+ "executiveText": "Deploys standardized external contact templates across all company locations, ensuring consistent communication channels with key external partners, vendors, and stakeholders. This streamlines contact management and maintains uniform business relationships.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": true,
+ "creatable": false,
+ "label": "Select Mail Contact Templates",
+ "name": "standards.DeployContactTemplates.templateIds",
+ "api": {
+ "url": "/api/ListContactTemplates",
+ "labelField": "name",
+ "valueField": "GUID",
+ "queryKey": "Contact Templates"
+ }
+ }
+ ],
+ "label": "Deploy Mail Contact Template",
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-05-31",
+ "powershellEquivalent": "New-MailContact",
+ "recommendedBy": ["CIPP"]
+ },
{
"name": "standards.AuditLog",
"cat": "Global Standards",
- "tag": ["CIS", "mip_search_auditlog"],
+ "tag": ["CIS M365 5.0 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"],
"helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.",
+ "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.",
"addedComponent": [],
"label": "Enable the Unified Audit Log",
"impact": "Low Impact",
@@ -51,12 +127,28 @@
"powershellEquivalent": "Enable-OrganizationCustomization",
"recommendedBy": ["CIS", "CIPP"]
},
+ {
+ "name": "standards.RestrictThirdPartyStorageServices",
+ "cat": "Global Standards",
+ "tag": ["CIS M365 5.0 (1.3.7)"],
+ "helpText": "Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers.",
+ "docsDescription": "Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so.",
+ "executiveText": "Prevents employees from using external cloud storage services like Dropbox, Google Drive, and Box within Microsoft 365, reducing data security risks and ensuring all company data remains within controlled corporate systems. This helps maintain data governance and prevents potential data leaks to unauthorized platforms.",
+ "addedComponent": [],
+ "label": "Restrict third-party storage services in Microsoft 365 on the web",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-06-06",
+ "powershellEquivalent": "New-MgServicePrincipal and Update-MgServicePrincipal",
+ "recommendedBy": ["CIS"]
+ },
{
"name": "standards.ProfilePhotos",
"cat": "Global Standards",
"tag": [],
"helpText": "Controls whether users can set their own profile photos in Microsoft 365.",
"docsDescription": "Controls whether users can set their own profile photos in Microsoft 365. When disabled, only User and Global administrators can update profile photos for users.",
+ "executiveText": "Manages user profile photo permissions within Microsoft 365, allowing organizations to control whether employees can upload their own photos or require administrative approval. This helps maintain professional appearance standards and prevents inappropriate images in corporate directories.",
"addedComponent": [
{
"type": "autoComplete",
@@ -88,6 +180,7 @@
"cat": "Global Standards",
"tag": [],
"helpText": "Adds branding to the logon page that only appears if the url is not login.microsoftonline.com. This potentially prevents AITM attacks via EvilNginx. This will also automatically generate alerts if a clone of your login page has been found when set to Remediate.",
+ "executiveText": "Implements advanced phishing protection by adding visual indicators to login pages that help users identify legitimate Microsoft login pages versus fraudulent copies. This security measure protects against sophisticated phishing attacks that attempt to steal employee credentials.",
"addedComponent": [],
"label": "Enable Phishing Protection system via branding CSS",
"impact": "Low Impact",
@@ -106,6 +199,7 @@
"cat": "Global Standards",
"tag": [],
"helpText": "Sets the branding for the tenant. This includes the login page, and the Office 365 portal.",
+ "executiveText": "Customizes Microsoft 365 login pages and portals with company branding, including logos, colors, and messaging. This creates a consistent corporate identity experience for employees and reinforces brand recognition while maintaining professional appearance across all Microsoft services.",
"addedComponent": [
{
"type": "textField",
@@ -161,9 +255,10 @@
{
"name": "standards.EnableCustomerLockbox",
"cat": "Global Standards",
- "tag": ["CIS", "CustomerLockBoxEnabled"],
+ "tag": ["CIS M365 5.0 (1.3.6)", "CustomerLockBoxEnabled"],
"helpText": "Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data",
"docsDescription": "Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.",
+ "executiveText": "Requires explicit organizational approval before Microsoft support staff can access company data for service operations. This provides an additional layer of data protection and ensures the organization maintains control over who can access sensitive business information, even during technical support scenarios.",
"addedComponent": [],
"label": "Enable Customer Lockbox",
"impact": "Low Impact",
@@ -177,6 +272,7 @@
"cat": "Global Standards",
"tag": [],
"helpText": "Enables the Pronouns feature for the tenant. This allows users to set their pronouns in their profile.",
+ "executiveText": "Allows employees to display their preferred pronouns in their Microsoft 365 profiles, supporting inclusive workplace practices and helping colleagues communicate respectfully. This feature enhances diversity and inclusion initiatives while fostering a more welcoming work environment.",
"addedComponent": [],
"label": "Enable Pronouns",
"impact": "Low Impact",
@@ -185,12 +281,27 @@
"powershellEquivalent": "Update-MgBetaAdminPeoplePronoun -IsEnabledInOrganization:$true",
"recommendedBy": []
},
+ {
+ "name": "standards.EnableNamePronunciation",
+ "cat": "Global Standards",
+ "tag": [],
+ "helpText": "Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile.",
+ "docsDescription": "Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile.",
+ "executiveText": "Enables employees to add pronunciation guides for their names in Microsoft 365 profiles, improving communication and respect in diverse workplaces. This feature helps colleagues pronounce names correctly, enhancing professional relationships and inclusive culture.",
+ "addedComponent": [],
+ "label": "Enable Name Pronunciation",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-06",
+ "recommendedBy": ["CIPP"]
+ },
{
"name": "standards.AnonReportDisable",
"cat": "Global Standards",
"tag": [],
"helpText": "Shows usernames instead of pseudo anonymised names in reports. This standard is required for reporting to work correctly.",
"docsDescription": "Microsoft announced some APIs and reports no longer return names, to comply with compliance and legal requirements in specific countries. This proves an issue for a lot of MSPs because those reports are often helpful for engineers. This standard applies a setting that shows usernames in those API calls / reports.",
+ "executiveText": "Configures Microsoft 365 reports to display actual usernames instead of anonymized identifiers, enabling IT administrators to effectively troubleshoot issues and generate meaningful usage reports. This improves operational efficiency and system management capabilities.",
"addedComponent": [],
"label": "Enable Usernames instead of pseudo anonymised names in reports",
"impact": "Low Impact",
@@ -202,9 +313,17 @@
{
"name": "standards.DisableGuestDirectory",
"cat": "Global Standards",
- "tag": [],
+ "tag": [
+ "CIS M365 5.0 (5.1.6.2)",
+ "CISA (MS.AAD.5.1v1)",
+ "EIDSCA.AP14",
+ "EIDSCA.ST08",
+ "EIDSCA.ST09",
+ "NIST CSF 2.0 (PR.AA-05)"
+ ],
"helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.",
"docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)",
+ "executiveText": "Restricts external guest users from viewing the company's employee directory and organizational structure, protecting sensitive information about staff and internal groups. This security measure prevents unauthorized access to corporate contact information while still allowing necessary collaboration.",
"addedComponent": [],
"label": "Restrict guest user access to directory objects",
"impact": "Low Impact",
@@ -216,9 +335,10 @@
{
"name": "standards.DisableBasicAuthSMTP",
"cat": "Global Standards",
- "tag": [],
- "helpText": "Disables SMTP AUTH for the organization and all users. This is the default for new tenants.",
- "docsDescription": "Disables SMTP basic authentication for the tenant and all users with it explicitly enabled.",
+ "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"],
+ "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)",
+ "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).",
+ "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.",
"addedComponent": [],
"label": "Disable SMTP Basic Authentication",
"impact": "Medium Impact",
@@ -230,8 +350,9 @@
{
"name": "standards.ActivityBasedTimeout",
"cat": "Global Standards",
- "tag": ["CIS", "spo_idle_session_timeout"],
+ "tag": ["CIS M365 5.0 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"],
"helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps",
+ "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.",
"addedComponent": [
{
"type": "autoComplete",
@@ -273,9 +394,10 @@
{
"name": "standards.AuthMethodsSettings",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03"],
"helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.",
"docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.",
+ "executiveText": "Configures security settings that allow users to report suspicious login attempts and manages how the system handles authentication credentials. This enhances overall security by enabling early detection of potential security threats and optimizing authentication processes.",
"addedComponent": [
{
"type": "autoComplete",
@@ -329,17 +451,76 @@
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy",
"recommendedBy": []
},
+ {
+ "name": "standards.AuthMethodsPolicyMigration",
+ "cat": "Entra (AAD) Standards",
+ "tag": [],
+ "helpText": "Completes the migration of authentication methods policy to the new format",
+ "docsDescription": "Sets the authentication methods policy migration state to complete. This is required when migrating from legacy authentication policies to the new unified authentication methods policy.",
+ "executiveText": "Completes the transition from legacy authentication policies to Microsoft's modern unified authentication methods policy, ensuring the organization benefits from the latest security features and management capabilities. This migration enables enhanced security controls and simplified policy management.",
+ "addedComponent": [],
+ "label": "Complete Authentication Methods Policy Migration",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-07-07",
+ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy",
+ "recommendedBy": ["CIPP"]
+ },
{
"name": "standards.AppDeploy",
"cat": "Entra (AAD) Standards",
"tag": [],
"helpText": "Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application.",
"docsDescription": "Uses the CIPP functionality that deploys applications across an entire tenant base as a standard.",
+ "executiveText": "Automatically deploys approved business applications across all company locations and users, ensuring consistent access to essential tools and maintaining standardized software configurations. This streamlines application management and reduces IT deployment overhead.",
"addedComponent": [
+ {
+ "type": "select",
+ "multiple": false,
+ "creatable": false,
+ "label": "App Approval Mode",
+ "name": "standards.AppDeploy.mode",
+ "options": [
+ {
+ "label": "Template",
+ "value": "template"
+ },
+ {
+ "label": "Copy Permissions",
+ "value": "copy"
+ }
+ ]
+ },
+ {
+ "type": "autoComplete",
+ "multiple": true,
+ "creatable": false,
+ "label": "Select Applications",
+ "name": "standards.AppDeploy.templateIds",
+ "api": {
+ "url": "/api/ListAppApprovalTemplates",
+ "labelField": "TemplateName",
+ "valueField": "TemplateId",
+ "queryKey": "StdAppApprovalTemplateList",
+ "addedField": {
+ "AppId": "AppId"
+ }
+ },
+ "condition": {
+ "field": "standards.AppDeploy.mode",
+ "compareType": "is",
+ "compareValue": "template"
+ }
+ },
{
"type": "textField",
"name": "standards.AppDeploy.appids",
- "label": "Application IDs, comma separated"
+ "label": "Application IDs, comma separated",
+ "condition": {
+ "field": "standards.AppDeploy.mode",
+ "compareType": "isNot",
+ "compareValue": "template"
+ }
}
],
"label": "Deploy Application",
@@ -355,6 +536,7 @@
"tag": [],
"helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.",
"docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.",
+ "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.",
"addedComponent": [],
"label": "Enable LAPS on the tenant",
"impact": "Low Impact",
@@ -366,9 +548,19 @@
{
"name": "standards.PWdisplayAppInformationRequiredState",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS M365 5.0 (2.3.1)",
+ "EIDSCA.AM03",
+ "EIDSCA.AM04",
+ "EIDSCA.AM06",
+ "EIDSCA.AM07",
+ "EIDSCA.AM09",
+ "EIDSCA.AM10",
+ "NIST CSF 2.0 (PR.AA-03)"
+ ],
"helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.",
"docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request",
+ "executiveText": "Enhances authentication security by requiring users to match numbers and showing detailed information about login requests, including application names and location data. This helps employees verify legitimate login attempts and prevents unauthorized access through more secure authentication methods.",
"addedComponent": [],
"label": "Enable Passwordless with Location information and Number Matching",
"impact": "Low Impact",
@@ -380,9 +572,10 @@
{
"name": "standards.allowOTPTokens",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["EIDSCA.AM02"],
"helpText": "Allows you to use MS authenticator OTP token generator",
"docsDescription": "Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients.",
+ "executiveText": "Enables one-time password generation through Microsoft Authenticator app, providing an additional secure authentication method for employees. This is particularly useful for secure VPN access and other systems requiring multi-factor authentication.",
"addedComponent": [],
"label": "Enable OTP via Authenticator",
"impact": "Low Impact",
@@ -394,9 +587,10 @@
{
"name": "standards.PWcompanionAppAllowedState",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["EIDSCA.AM01"],
"helpText": "Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication.",
"docsDescription": "Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app.",
+ "executiveText": "Enables a simplified authentication experience by allowing users to authenticate directly through Outlook without requiring a separate authenticator app. This improves user convenience while maintaining security standards for passwordless authentication.",
"addedComponent": [
{
"type": "autoComplete",
@@ -412,6 +606,10 @@
{
"label": "Disabled",
"value": "disabled"
+ },
+ {
+ "label": "Microsoft managed",
+ "value": "default"
}
]
}
@@ -426,9 +624,18 @@
{
"name": "standards.EnableFIDO2",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": [
+ "EIDSCA.AF01",
+ "EIDSCA.AF02",
+ "EIDSCA.AF03",
+ "EIDSCA.AF04",
+ "EIDSCA.AF05",
+ "EIDSCA.AF06",
+ "NIST CSF 2.0 (PR.AA-03)"
+ ],
"helpText": "Enables the FIDO2 authenticationMethod for the tenant",
"docsDescription": "Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication.",
+ "executiveText": "Enables support for hardware security keys (like YubiKey) that provide the highest level of authentication security. These physical devices prevent phishing attacks and credential theft, offering superior protection for high-value accounts and sensitive business operations.",
"addedComponent": [],
"label": "Enable FIDO2 capabilities",
"impact": "Low Impact",
@@ -443,6 +650,7 @@
"tag": [],
"helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.",
"docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.",
+ "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.",
"addedComponent": [],
"label": "Enable Hardware OAuth tokens",
"impact": "Low Impact",
@@ -454,9 +662,10 @@
{
"name": "standards.allowOAuthTokens",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["EIDSCA.AT01", "EIDSCA.AT02"],
"helpText": "Allows you to use any software OAuth token generator",
"docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.",
+ "executiveText": "Allows employees to use third-party authentication apps and password managers to generate secure login codes, providing flexibility in authentication methods while maintaining security standards. This accommodates diverse user preferences and existing security tools.",
"addedComponent": [],
"label": "Enable OTP Software OAuth tokens",
"impact": "Low Impact",
@@ -465,12 +674,28 @@
"powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration",
"recommendedBy": []
},
+ {
+ "name": "standards.FormsPhishingProtection",
+ "cat": "Global Standards",
+ "tag": ["CIS M365 5.0 (1.3.5)", "Security", "PhishingProtection"],
+ "helpText": "Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns.",
+ "docsDescription": "Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization.",
+ "executiveText": "Automatically scans Microsoft Forms created by employees for malicious content and phishing attempts, preventing the creation and distribution of harmful forms within the organization. This protects against both internal threats and compromised accounts that might be used to distribute malicious content.",
+ "addedComponent": [],
+ "label": "Enable internal phishing protection for Forms",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-06",
+ "powershellEquivalent": "Graph API",
+ "recommendedBy": ["CIS", "CIPP"]
+ },
{
"name": "standards.TAP",
"cat": "Entra (AAD) Standards",
"tag": [],
"helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.",
"docsDescription": "Enables Temporary Password generation for the tenant.",
+ "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.",
"addedComponent": [
{
"type": "autoComplete",
@@ -500,9 +725,10 @@
{
"name": "standards.PasswordExpireDisabled",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS", "PWAgePolicyNew"],
+ "tag": ["CIS M365 5.0 (1.3.1)", "PWAgePolicyNew"],
"helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.",
"docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.",
+ "executiveText": "Eliminates mandatory password expiration requirements, allowing employees to keep strong passwords indefinitely rather than forcing frequent changes that often lead to weaker passwords. This modern security approach reduces help desk calls and improves overall password security when combined with multi-factor authentication.",
"addedComponent": [],
"label": "Do not expire passwords",
"impact": "Low Impact",
@@ -511,11 +737,34 @@
"powershellEquivalent": "Update-MgDomain",
"recommendedBy": ["CIS", "CIPP"]
},
+ {
+ "name": "standards.CustomBannedPasswordList",
+ "cat": "Entra (AAD) Standards",
+ "tag": ["CIS M365 5.0 (5.2.3.2)"],
+ "helpText": "**Requires Entra ID P1.** Updates and enables the Entra ID custom banned password list with the supplied words. Enter words separated by commas or semicolons. Each word must be 4-16 characters long. Maximum 1,000 words allowed.",
+ "docsDescription": "Updates and enables the Entra ID custom banned password list with the supplied words. This supplements the global banned password list maintained by Microsoft. The custom list is limited to 1,000 key base terms of 4-16 characters each. Entra ID will [block variations and common substitutions](https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection#configure-custom-banned-passwords) of these words in user passwords. [How are passwords evaluated?](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad#score-calculation)",
+ "addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.CustomBannedPasswordList.BannedWords",
+ "label": "Banned Words",
+ "placeholder": "Banned words separated by commas or semicolons",
+ "required": true
+ }
+ ],
+ "label": "Set Entra ID Custom Banned Password List",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-06-28",
+ "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting",
+ "recommendedBy": ["CIS"]
+ },
{
"name": "standards.ExternalMFATrusted",
"cat": "Entra (AAD) Standards",
"tag": [],
"helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.",
+ "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.",
"addedComponent": [
{
"type": "autoComplete",
@@ -545,9 +794,10 @@
{
"name": "standards.DisableTenantCreation",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)"],
"helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.",
"docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.",
+ "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.",
"addedComponent": [],
"label": "Disable M365 Tenant creation by users",
"impact": "Low Impact",
@@ -559,9 +809,20 @@
{
"name": "standards.EnableAppConsentRequests",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS M365 5.0 (1.5.2)",
+ "CISA (MS.AAD.9.1v1)",
+ "EIDSCA.CP04",
+ "EIDSCA.CR01",
+ "EIDSCA.CR02",
+ "EIDSCA.CR03",
+ "EIDSCA.CR04",
+ "Essential 8 (1507)",
+ "NIST CSF 2.0 (PR.AA-05)"
+ ],
"helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings",
"docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards",
+ "executiveText": "Establishes a formal approval process where employees can request access to business applications that require administrative review. This balances security with productivity by allowing controlled access to necessary tools while preventing unauthorized application installations.",
"addedComponent": [
{
"type": "AdminRolesMultiSelect",
@@ -582,6 +843,7 @@
"tag": [],
"helpText": "Sets the state of the registration campaign for the tenant",
"docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.",
+ "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.",
"addedComponent": [
{
"type": "autoComplete",
@@ -617,9 +879,10 @@
{
"name": "standards.DisableM365GroupUsers",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["CISA (MS.AAD.21.1v1)"],
"helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc",
"docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc",
+ "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.",
"addedComponent": [],
"label": "Disable M365 Group creation by users",
"impact": "Low Impact",
@@ -631,9 +894,16 @@
{
"name": "standards.DisableAppCreation",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS M365 5.0 (1.2.2)",
+ "CISA (MS.AAD.4.1v1)",
+ "EIDSCA.AP10",
+ "Essential 8 (1175)",
+ "NIST CSF 2.0 (PR.AA-05)"
+ ],
"helpText": "Disables the ability for users to create App registrations in the tenant.",
"docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.",
+ "executiveText": "Prevents regular employees from creating application registrations that could be used to maintain unauthorized access to company systems. This security measure ensures that only authorized IT personnel can create applications, reducing the risk of persistent security breaches through malicious applications.",
"addedComponent": [],
"label": "Disable App creation by users",
"impact": "Low Impact",
@@ -643,10 +913,44 @@
"recommendedBy": ["CIS", "CIPP"]
},
{
- "name": "standards.DisableSecurityGroupUsers",
+ "name": "standards.BitLockerKeysForOwnedDevice",
"cat": "Entra (AAD) Standards",
"tag": [],
+ "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.",
+ "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.",
+ "executiveText": "Gives administrators centralized control over BitLocker recovery secretsβrestrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "label": "Select state",
+ "name": "standards.BitLockerKeysForOwnedDevice.state",
+ "options": [
+ {
+ "label": "Restrict",
+ "value": "restrict"
+ },
+ {
+ "label": "Allow",
+ "value": "allow"
+ }
+ ]
+ }
+ ],
+ "label": "Control BitLocker key recovery for owned devices",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-10-12",
+ "powershellEquivalent": "Update-MgBetaPolicyAuthorizationPolicy",
+ "recommendedBy": []
+ },
+ {
+ "name": "standards.DisableSecurityGroupUsers",
+ "cat": "Entra (AAD) Standards",
+ "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)"],
"helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams",
+ "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.",
"addedComponent": [],
"label": "Disable Security Group creation by users",
"impact": "Medium Impact",
@@ -660,6 +964,7 @@
"cat": "Entra (AAD) Standards",
"tag": [],
"helpText": "This standard currently does not function and can be safely disabled",
+ "executiveText": "This standard is currently non-functional and should be disabled. It was previously designed to remove outdated multi-factor authentication configurations in favor of modern security policies.",
"addedComponent": [],
"label": "Remove Legacy MFA if SD or CA is active",
"impact": "Medium Impact",
@@ -672,7 +977,8 @@
"name": "standards.DisableSelfServiceLicenses",
"cat": "Entra (AAD) Standards",
"tag": [],
- "helpText": "This standard disables all self service licenses and enables all exclusions",
+ "helpText": "Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions",
+ "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.",
"addedComponent": [
{
"type": "textField",
@@ -692,9 +998,18 @@
"name": "standards.DisableGuests",
"cat": "Entra (AAD) Standards",
"tag": [],
- "helpText": "Blocks login for guest users that have not logged in for 90 days",
- "addedComponent": [],
- "label": "Disable Guest accounts that have not logged on for 90 days",
+ "helpText": "Blocks login for guest users that have not logged in for a number of days",
+ "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.",
+ "addedComponent": [
+ {
+ "type": "number",
+ "name": "standards.DisableGuests.days",
+ "required": true,
+ "defaultValue": 90,
+ "label": "Days of inactivity"
+ }
+ ],
+ "label": "Disable Guest accounts that have not logged on for a number of days",
"impact": "Medium Impact",
"impactColour": "warning",
"addedDate": "2022-10-20",
@@ -704,9 +1019,17 @@
{
"name": "standards.OauthConsent",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": [
+ "CIS M365 5.0 (1.5.1)",
+ "CISA (MS.AAD.4.2v1)",
+ "EIDSCA.AP08",
+ "EIDSCA.AP09",
+ "Essential 8 (1175)",
+ "NIST CSF 2.0 (PR.AA-05)"
+ ],
"helpText": "Disables users from being able to consent to applications, except for those specified in the field below",
"docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.",
+ "executiveText": "Requires administrative approval before employees can grant applications access to company data, preventing unauthorized data sharing and potential security breaches. This protects against malicious applications while allowing approved business tools to function normally.",
"addedComponent": [
{
"type": "textField",
@@ -728,6 +1051,7 @@
"tag": ["IntegratedApps"],
"helpText": "Sets the default oauth consent level so users can consent to applications that have low risks.",
"docsDescription": "Allows users to consent to applications with low assigned risk.",
+ "executiveText": "Allows employees to approve low-risk applications without administrative intervention, balancing security with productivity. This provides a middle ground between complete restriction and open access, enabling business agility while maintaining protection against high-risk applications.",
"label": "Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure)",
"impact": "Medium Impact",
"impactColour": "warning",
@@ -738,8 +1062,9 @@
{
"name": "standards.GuestInvite",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07"],
"helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.",
+ "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.",
"addedComponent": [
{
"type": "autoComplete",
@@ -778,9 +1103,10 @@
{
"name": "standards.StaleEntraDevices",
"cat": "Entra (AAD) Standards",
- "tag": ["CIS"],
+ "tag": ["Essential 8 (1501)", "NIST CSF 2.0 (ID.AM-08)", "NIST CSF 2.0 (PR.PS-03)"],
"helpText": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days.",
"docsDescription": "Remediate is currently not available. Cleans up Entra devices that have not connected/signed in for the specified number of days. First disables and later deletes the devices. More info can be found in the [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices)",
+ "executiveText": "Automatically identifies and removes inactive devices that haven't connected to company systems for a specified period, reducing security risks from abandoned or lost devices. This maintains a clean device inventory and prevents potential unauthorized access through dormant device registrations.",
"addedComponent": [
{
"type": "number",
@@ -791,7 +1117,7 @@
"disabledFeatures": {
"report": false,
"warn": false,
- "remediate": true
+ "remediate": false
},
"label": "Cleanup stale Entra devices",
"impact": "High Impact",
@@ -805,6 +1131,7 @@
"cat": "Entra (AAD) Standards",
"tag": [],
"helpText": "Disables App consent and set to Allow user consent for apps",
+ "executiveText": "Reverses application consent restrictions, allowing employees to approve applications independently without administrative oversight. This increases productivity and user autonomy but reduces security controls over data access permissions.",
"addedComponent": [],
"label": "Undo App Consent Standard",
"impact": "High Impact",
@@ -816,9 +1143,10 @@
{
"name": "standards.SecurityDefaults",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["CISA (MS.AAD.11.1v1)"],
"helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.",
"docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.",
+ "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.",
"addedComponent": [],
"label": "Enable Security Defaults",
"impact": "High Impact",
@@ -830,9 +1158,10 @@
{
"name": "standards.DisableSMS",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)"],
"helpText": "This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in.",
"docsDescription": "Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in.",
+ "executiveText": "Disables SMS text messages as a multi-factor authentication method due to security vulnerabilities like SIM swapping attacks. This forces users to adopt more secure authentication methods like authenticator apps or hardware tokens, significantly improving account security.",
"addedComponent": [],
"label": "Disables SMS as an MFA method",
"impact": "High Impact",
@@ -844,9 +1173,10 @@
{
"name": "standards.DisableVoice",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)"],
"helpText": "This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in.",
"docsDescription": "Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in.",
+ "executiveText": "Disables voice call authentication due to security vulnerabilities and social engineering risks. This forces users to adopt more secure authentication methods like authenticator apps, improving overall account security by eliminating phone-based attack vectors.",
"addedComponent": [],
"label": "Disables Voice call as an MFA method",
"impact": "High Impact",
@@ -858,8 +1188,9 @@
{
"name": "standards.DisableEmail",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": ["CIS M365 5.0 (2.3.5)", "NIST CSF 2.0 (PR.AA-03)"],
"helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.",
+ "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.",
"addedComponent": [],
"label": "Disables Email as an MFA method",
"impact": "High Impact",
@@ -874,6 +1205,7 @@
"tag": [],
"helpText": "This blocks users from using Certificates as an MFA method.",
"docsDescription": "",
+ "executiveText": "Disables certificate-based authentication as a multi-factor authentication method, typically used when organizations want to standardize on other authentication methods or when certificate management becomes too complex for the security benefit provided.",
"addedComponent": [],
"label": "Disables Certificates as an MFA method",
"impact": "High Impact",
@@ -888,6 +1220,7 @@
"tag": [],
"helpText": "This blocks users from using QR Code Pin as an MFA method. If a user only has QR Code Pin as a MFA method, they will be unable to log in.",
"docsDescription": "Disables QR Code Pin as an MFA method for the tenant. If a user only has QR Code Pin as a MFA method, they will be unable to sign in.",
+ "executiveText": "Disables QR Code Pin authentication method due to security concerns, forcing users to adopt more secure authentication alternatives. This helps standardize authentication methods and reduces potential security vulnerabilities while ensuring employees use more robust multi-factor authentication options.",
"addedComponent": [],
"label": "Disables QR Code Pin as an MFA method",
"impact": "High Impact",
@@ -899,8 +1232,19 @@
{
"name": "standards.PerUserMFA",
"cat": "Entra (AAD) Standards",
- "tag": [],
+ "tag": [
+ "CIS M365 5.0 (1.2.1)",
+ "CIS M365 5.0 (1.1.1)",
+ "CIS M365 5.0 (1.1.2)",
+ "CISA (MS.AAD.1.1v1)",
+ "CISA (MS.AAD.1.2v1)",
+ "Essential 8 (1504)",
+ "Essential 8 (1173)",
+ "Essential 8 (1401)",
+ "NIST CSF 2.0 (PR.AA-03)"
+ ],
"helpText": "Enables per user MFA for all users.",
+ "executiveText": "Requires all employees to use multi-factor authentication for enhanced account security, significantly reducing the risk of unauthorized access from compromised passwords. This fundamental security measure protects against the majority of account-based attacks and is essential for maintaining strong cybersecurity posture.",
"addedComponent": [],
"label": "Enables per user MFA for all users.",
"impact": "High Impact",
@@ -924,7 +1268,7 @@
"label": "Preferred Language",
"api": {
"url": "/languageList.json",
- "labelField": "language",
+ "labelField": "tag",
"valueField": "tag"
}
}
@@ -939,7 +1283,7 @@
{
"name": "standards.OutBoundSpamAlert",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (2.1.6)"],
"helpText": "Set the Outbound Spam Alert e-mail address",
"docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.",
"addedComponent": [
@@ -1011,6 +1355,7 @@
"tag": [],
"helpText": "Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF.",
"docsDescription": "Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF. Cannot be overridden by the user. For more information, see [Microsoft's documentation.](https://learn.microsoft.com/en-us/exchange/mail-flow/content-conversion/tnef-conversion?view=exchserver-2019)",
+ "executiveText": "Prevents the creation of winmail.dat attachments that can cause compatibility issues when sending emails to external recipients using non-Outlook email clients. This improves email compatibility and reduces support issues with external partners and customers.",
"addedComponent": [],
"label": "Disable TNEF/winmail.dat",
"impact": "Low Impact",
@@ -1025,6 +1370,7 @@
"tag": [],
"helpText": "Sets the default Focused Inbox state for the tenant. This can be overridden by the user.",
"docsDescription": "Sets the default Focused Inbox state for the tenant. This can be overridden by the user in their Outlook settings. For more information, see [Microsoft's documentation.](https://support.microsoft.com/en-us/office/focused-inbox-for-outlook-f445ad7f-02f4-4294-a82e-71d8964e3978)",
+ "executiveText": "Configures the default setting for Outlook's Focused Inbox feature, which automatically sorts important emails into a focused view while placing less important emails in a separate section. This can improve employee productivity by reducing email clutter, though users can adjust this setting individually.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1056,6 +1402,7 @@
"tag": [],
"helpText": "Sets the Cloud Message Recall state for the tenant. This allows users to recall messages from the cloud.",
"docsDescription": "Sets the default state for Cloud Message Recall for the tenant. By default this is enabled. You can read more about the feature [here.](https://techcommunity.microsoft.com/t5/exchange-team-blog/cloud-based-message-recall-in-exchange-online/ba-p/3744714)",
+ "executiveText": "Enables employees to recall or retract emails they've sent, helping prevent embarrassing mistakes or accidental data sharing. This feature can reduce the impact of human errors in email communication and provides a safety net for sensitive information accidentally sent to wrong recipients.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1087,6 +1434,7 @@
"tag": [],
"helpText": "Enables auto-expanding archives for the tenant",
"docsDescription": "Enables auto-expanding archives for the tenant. Does not enable archives for users.",
+ "executiveText": "Enables automatic expansion of email archive storage when users approach their archive limits, ensuring continuous email retention without manual intervention. This prevents email storage issues and maintains compliance with data retention policies without requiring ongoing administrative management.",
"addedComponent": [],
"label": "Enable Auto-expanding archives",
"impact": "Low Impact",
@@ -1096,10 +1444,44 @@
"recommendedBy": []
},
{
- "name": "standards.EnableOnlineArchiving",
+ "name": "standards.TwoClickEmailProtection",
"cat": "Exchange Standards",
"tag": [],
+ "helpText": "Configures the two-click confirmation requirement for viewing encrypted/protected emails in OWA and new Outlook. When enabled, users must click \"View message\" before accessing protected content, providing an additional layer of privacy protection.",
+ "docsDescription": "Configures the TwoClickMailPreviewEnabled setting in Exchange Online organization configuration. This security feature requires users to click \"View message\" before accessing encrypted or protected emails in Outlook on the web (OWA) and new Outlook for Windows. This provides additional privacy protection by preventing protected content from automatically displaying, giving users time to ensure their screen is not visible to others before viewing sensitive content. The feature helps protect against shoulder surfing and accidental disclosure of confidential information.",
+ "executiveText": "Requires employees to click twice before viewing encrypted or sensitive emails, preventing accidental exposure of confidential information when screens might be visible to others. This privacy protection helps prevent shoulder surfing and ensures employees are intentional about viewing sensitive content.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "label": "Select value",
+ "name": "standards.TwoClickEmailProtection.state",
+ "options": [
+ {
+ "label": "Enabled",
+ "value": "enabled"
+ },
+ {
+ "label": "Disabled",
+ "value": "disabled"
+ }
+ ]
+ }
+ ],
+ "label": "Set two-click confirmation for encrypted emails in New Outlook",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-13",
+ "powershellEquivalent": "Set-OrganizationConfig -TwoClickMailPreviewEnabled $true | $false",
+ "recommendedBy": []
+ },
+ {
+ "name": "standards.EnableOnlineArchiving",
+ "cat": "Exchange Standards",
+ "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)"],
"helpText": "Enables the In-Place Online Archive for all UserMailboxes with a valid license.",
+ "executiveText": "Automatically enables online email archiving for all licensed employees, providing additional storage for older emails while maintaining easy access. This helps manage mailbox sizes, improves email performance, and supports compliance with data retention requirements.",
"addedComponent": [],
"label": "Enable Online Archive for all users",
"impact": "Low Impact",
@@ -1113,6 +1495,7 @@
"cat": "Exchange Standards",
"tag": [],
"helpText": "Enables litigation hold for all UserMailboxes with a valid license.",
+ "executiveText": "Preserves all email content for legal and compliance purposes by preventing permanent deletion of emails, even when users attempt to delete them. This is essential for organizations subject to legal discovery requirements or regulatory compliance mandates.",
"addedComponent": [
{
"type": "textField",
@@ -1131,9 +1514,10 @@
{
"name": "standards.SpoofWarn",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (6.2.3)"],
"helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA",
"docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)",
+ "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1170,8 +1554,9 @@
{
"name": "standards.EnableMailTips",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_mailtipsenabled"],
+ "tag": ["CIS M365 5.0 (6.5.2)", "exo_mailtipsenabled"],
"helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements",
+ "executiveText": "Enables helpful notifications in Outlook that warn users about potential email issues, such as sending to large groups, external recipients, or invalid addresses. This reduces email mistakes and improves communication efficiency by providing real-time guidance to employees.",
"addedComponent": [
{
"type": "number",
@@ -1193,6 +1578,7 @@
"cat": "Exchange Standards",
"tag": [],
"helpText": "Sets the default state for automatically turning meetings into Teams meetings for the tenant. This can be overridden by the user in Outlook.",
+ "executiveText": "Automatically adds Microsoft Teams meeting links to calendar invitations by default, streamlining the process of creating virtual meetings. This improves collaboration efficiency and ensures consistent meeting experiences across the organization, though users can override this setting when needed.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1225,6 +1611,7 @@
"tag": [],
"helpText": "Disables the daily viva reports for all users. This standard requires the CIPP-SAM application to have the Company Administrator (Global Admin) role in the tenant. Enable this using CIPP > Advanced > Super Admin > SAM App Roles. Activate the roles with a CPV refresh.",
"docsDescription": "",
+ "executiveText": "Disables daily Microsoft Viva Insights reports that are automatically sent to employees, reducing email volume and allowing organizations to control when and how productivity insights are shared. This can help prevent information overload while maintaining the ability to access insights when needed.",
"addedComponent": [],
"label": "Disable daily Insight/Viva reports",
"impact": "Low Impact",
@@ -1236,8 +1623,9 @@
{
"name": "standards.RotateDKIM",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (2.1.9)"],
"helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit",
+ "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.",
"addedComponent": [],
"label": "Rotate DKIM keys that are 1024 bit to 2048 bit",
"impact": "Low Impact",
@@ -1249,8 +1637,9 @@
{
"name": "standards.AddDKIM",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (2.1.9)"],
"helpText": "Enables DKIM for all domains that currently support it",
+ "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.",
"addedComponent": [],
"label": "Enables DKIM for all domains that currently support it",
"impact": "Low Impact",
@@ -1259,12 +1648,52 @@
"powershellEquivalent": "New-DkimSigningConfig and Set-DkimSigningConfig",
"recommendedBy": ["CIS", "CIPP"]
},
+ {
+ "name": "standards.AddDMARCToMOERA",
+ "cat": "Global Standards",
+ "tag": ["CIS M365 5.0 (2.1.10)", "Security", "PhishingProtection"],
+ "helpText": "Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%",
+ "docsDescription": "Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%",
+ "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": true,
+ "required": false,
+ "placeholder": "v=DMARC1; p=reject; (recommended)",
+ "label": "Value",
+ "name": "standards.AddDMARCToMOERA.RecordValue",
+ "options": [
+ {
+ "label": "v=DMARC1; p=reject; (recommended)",
+ "value": "v=DMARC1; p=reject;"
+ }
+ ]
+ }
+ ],
+ "label": "Enables DMARC on MOERA (onmicrosoft.com) domains",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-16",
+ "powershellEquivalent": "Portal only",
+ "recommendedBy": ["CIS", "Microsoft"]
+ },
{
"name": "standards.EnableMailboxAuditing",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_mailboxaudit"],
+ "tag": [
+ "CIS M365 5.0 (6.1.1)",
+ "CIS M365 5.0 (6.1.2)",
+ "CIS M365 5.0 (6.1.3)",
+ "exo_mailboxaudit",
+ "Essential 8 (1509)",
+ "Essential 8 (1683)",
+ "NIST CSF 2.0 (DE.CM-09)"
+ ],
"helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.",
"docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.",
+ "executiveText": "Enables comprehensive logging of all email access and modifications across all employee mailboxes, providing detailed audit trails for security investigations and compliance requirements. This helps detect unauthorized access, data breaches, and supports regulatory compliance efforts.",
"addedComponent": [],
"label": "Enable Mailbox auditing",
"impact": "Low Impact",
@@ -1278,6 +1707,7 @@
"cat": "Exchange Standards",
"tag": [],
"helpText": "Sets the Send and Receive limits for new users. Valid values are 1MB to 150MB",
+ "executiveText": "Establishes standard email attachment size limits for all new employees, balancing functionality with system performance and security. This prevents email system overload from large attachments while ensuring employees can share necessary files through appropriate channels.",
"addedComponent": [
{
"type": "number",
@@ -1305,6 +1735,7 @@
"tag": [],
"helpText": "Sets the default sharing level for the default calendar, for all users",
"docsDescription": "Sets the default sharing level for the default calendar for all users in the tenant. You can read about the different sharing levels [here.](https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxfolderpermission?view=exchange-ps#-accessrights)",
+ "executiveText": "Configures how much calendar information employees share by default with colleagues, balancing collaboration needs with privacy. This setting determines whether others can see meeting details, free/busy times, or just availability, helping optimize scheduling while protecting sensitive meeting information.",
"disabledFeatures": {
"report": true,
"warn": true,
@@ -1374,9 +1805,10 @@
{
"name": "standards.EXOOutboundSpamLimits",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (2.1.6)"],
"helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ",
"docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.",
+ "executiveText": "Sets limits on how many emails employees can send per hour and per day to prevent spam and protect the organization's email reputation. When limits are exceeded, the system can alert administrators or temporarily block the user, helping detect compromised accounts or prevent abuse.",
"addedComponent": [
{
"type": "number",
@@ -1403,8 +1835,14 @@
"name": "standards.EXOOutboundSpamLimits.ActionWhenThresholdReached",
"label": "Action When Threshold Reached",
"options": [
- { "label": "Alert", "value": "Alert" },
- { "label": "Block User", "value": "BlockUser" },
+ {
+ "label": "Alert",
+ "value": "Alert"
+ },
+ {
+ "label": "Block User",
+ "value": "BlockUser"
+ },
{
"label": "Block user from sending mail for the rest of the day",
"value": "BlockUserForToday"
@@ -1422,9 +1860,10 @@
{
"name": "standards.DisableExternalCalendarSharing",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_individualsharing"],
+ "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing"],
"helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.",
"docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.",
+ "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.",
"addedComponent": [],
"label": "Disable external calendar sharing",
"impact": "Low Impact",
@@ -1436,9 +1875,10 @@
{
"name": "standards.AutoAddProxy",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": [],
"helpText": "Automatically adds all available domains as a proxy address.",
"docsDescription": "Automatically finds all available domain names in the tenant, and tries to add proxy addresses based on the user's UPN to each of these.",
+ "executiveText": "Automatically creates email addresses for employees across all company domains, ensuring they can receive emails sent to any of the organization's domain names. This improves email delivery reliability and maintains consistent communication channels across different business units or brands.",
"addedComponent": [],
"label": "Automatically deploy proxy addresses",
"impact": "Medium Impact",
@@ -1455,9 +1895,10 @@
{
"name": "standards.DisableAdditionalStorageProviders",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_storageproviderrestricted"],
+ "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted"],
"helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.",
"docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.",
+ "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.",
"addedComponent": [],
"label": "Disable additional storage providers in OWA",
"impact": "Low Impact",
@@ -1469,9 +1910,10 @@
{
"name": "standards.AntiSpamSafeList",
"cat": "Defender Standards",
- "tag": [],
+ "tag": ["CIS M365 5.0 (2.1.13)"],
"helpText": "Sets the anti-spam connection filter policy option 'safe list' in Defender.",
"docsDescription": "Sets [Microsoft's built-in 'safe list'](https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps#-enablesafelist) in the anti-spam connection filter policy, rather than setting a custom safe/block list of IPs.",
+ "executiveText": "Enables Microsoft's pre-approved list of trusted email servers to improve email delivery from legitimate sources while maintaining spam protection. This reduces false positives where legitimate emails might be blocked while still protecting against spam and malicious emails.",
"addedComponent": [
{
"type": "switch",
@@ -1491,6 +1933,7 @@
"cat": "Exchange Standards",
"tag": [],
"helpText": "Sets the shorten meetings settings on a tenant level. This will shorten meetings by the selected amount of minutes. Valid values are 0 to 29. Short meetings are under 60 minutes, long meetings are over 60 minutes.",
+ "executiveText": "Automatically shortens calendar meetings by a specified number of minutes to provide buffer time between meetings, reducing back-to-back scheduling stress and allowing employees time to transition between meetings. This improves work-life balance and meeting effectiveness.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1538,6 +1981,7 @@
"tag": [],
"helpText": "Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external.",
"docsDescription": "",
+ "executiveText": "Controls whether employees can use Microsoft Bookings to create online appointment scheduling pages for internal and external clients. This feature can improve customer service and streamline appointment management, but may need to be controlled for security or business process reasons.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1563,12 +2007,51 @@
"powershellEquivalent": "Set-OrganizationConfig -BookingsEnabled",
"recommendedBy": []
},
+ {
+ "name": "standards.EXODirectSend",
+ "cat": "Exchange Standards",
+ "tag": [],
+ "helpText": "Sets the state of Direct Send in Exchange Online. Direct Send allows applications to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication.",
+ "docsDescription": "Controls whether applications can use Direct Send to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication. A detailed explanation from Microsoft can be found [here.](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)",
+ "executiveText": "Controls whether business applications and devices (like printers or scanners) can send emails through the company's email system without authentication. While this enables convenient features like scan-to-email, it may pose security risks and should be carefully managed.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "label": "Select value",
+ "name": "standards.EXODirectSend.state",
+ "options": [
+ {
+ "label": "Enabled",
+ "value": "enabled"
+ },
+ {
+ "label": "Disabled",
+ "value": "disabled"
+ }
+ ]
+ }
+ ],
+ "label": "Set Direct Send state",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-05-28",
+ "powershellEquivalent": "Set-OrganizationConfig -RejectDirectSend $true/$false",
+ "recommendedBy": []
+ },
{
"name": "standards.DisableOutlookAddins",
"cat": "Exchange Standards",
- "tag": ["CIS", "exo_outlookaddins"],
+ "tag": [
+ "CIS M365 5.0 (6.3.1)",
+ "exo_outlookaddins",
+ "NIST CSF 2.0 (PR.AA-05)",
+ "NIST CSF 2.0 (PR.PS-05)"
+ ],
"helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.",
"docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.",
+ "executiveText": "Prevents employees from installing third-party add-ins in Outlook without administrative approval, reducing security risks from potentially malicious extensions. This ensures only vetted and approved tools can access company email data while maintaining centralized control over email functionality.",
"addedComponent": [],
"label": "Disable users from installing add-ins in Outlook",
"impact": "Medium Impact",
@@ -1582,6 +2065,7 @@
"cat": "Exchange Standards",
"tag": [],
"helpText": "Loops through all users and removes the Safe Senders list. This is to prevent SPF bypass attacks, as the Safe Senders list is not checked by SPF.",
+ "executiveText": "Removes user-defined safe sender lists to prevent security bypasses where malicious emails could avoid spam filtering. This ensures all emails go through proper security screening, even if users have previously marked senders as 'safe', improving overall email security.",
"addedComponent": [],
"disabledFeatures": {
"report": true,
@@ -1601,6 +2085,7 @@
"tag": [],
"helpText": "Sets emails sent as and on behalf of shared mailboxes to also be stored in the shared mailbox sent items folder",
"docsDescription": "This makes sure that e-mails sent from shared mailboxes or delegate mailboxes, end up in the mailbox of the shared/delegate mailbox instead of the sender, allowing you to keep replies in the same mailbox as the original e-mail.",
+ "executiveText": "Ensures emails sent from shared mailboxes (like info@company.com) are stored in the shared mailbox rather than the individual sender's mailbox. This maintains complete email threads in one location, improving collaboration and ensuring all team members can see the full conversation history.",
"addedComponent": [
{
"type": "switch",
@@ -1621,6 +2106,7 @@
"tag": [],
"helpText": "Enables the ability for users to send from their alias addresses.",
"docsDescription": "Allows users to change the 'from' address to any set in their Azure AD Profile.",
+ "executiveText": "Allows employees to send emails from their alternative email addresses (aliases) rather than just their primary address. This is useful for employees who manage multiple roles or departments, enabling them to send emails from the most appropriate address for the context.",
"addedComponent": [],
"label": "Allow users to send from their alias addresses",
"impact": "Medium Impact",
@@ -1635,6 +2121,7 @@
"tag": [],
"helpText": "Set the state of the spam submission button in Outlook",
"docsDescription": "Set the state of the built-in Report button in Outlook. This gives the users the ability to report emails as spam or phish.",
+ "executiveText": "Enables employees to easily report suspicious emails directly from Outlook, helping improve the organization's spam and phishing detection systems. This crowdsourced approach to security allows users to contribute to threat detection while providing valuable feedback to enhance email security filters.",
"addedComponent": [
{
"type": "autoComplete",
@@ -1669,11 +2156,12 @@
{
"name": "standards.DisableSharedMailbox",
"cat": "Exchange Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)"],
"helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.",
"docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.",
+ "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.",
"addedComponent": [],
- "label": "Disable Shared Mailbox AAD accounts",
+ "label": "Disable Shared Mailbox Entra accounts",
"impact": "Medium Impact",
"impactColour": "warning",
"addedDate": "2021-11-16",
@@ -1681,11 +2169,33 @@
"recommendedBy": ["CIS", "CIPP"]
},
{
- "name": "standards.EXODisableAutoForwarding",
+ "name": "standards.DisableResourceMailbox",
"cat": "Exchange Standards",
- "tag": ["CIS", "mdo_autoforwardingmode", "mdo_blockmailforward"],
- "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.",
+ "tag": ["NIST CSF 2.0 (PR.AA-01)"],
+ "helpText": "Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.",
+ "docsDescription": "Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.",
+ "executiveText": "Prevents direct login to resource mailbox accounts (like conference rooms or equipment), ensuring they can only be managed through proper administrative channels. This security measure eliminates potential unauthorized access to resource scheduling systems while maintaining proper booking functionality.",
+ "addedComponent": [],
+ "label": "Disable Unlicensed Resource Mailbox Entra accounts",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-06-01",
+ "powershellEquivalent": "Get-Mailbox & Update-MgUser",
+ "recommendedBy": ["Microsoft", "CIPP"]
+ },
+ {
+ "name": "standards.EXODisableAutoForwarding",
+ "cat": "Exchange Standards",
+ "tag": [
+ "CIS M365 5.0 (6.2.1)",
+ "mdo_autoforwardingmode",
+ "mdo_blockmailforward",
+ "CISA (MS.EXO.4.1v1)",
+ "NIST CSF 2.0 (PR.DS-02)"
+ ],
+ "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.",
"docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.",
+ "executiveText": "Prevents employees from automatically forwarding company emails to external addresses, protecting against data leaks and unauthorized information sharing. This security measure helps maintain control over sensitive business communications while preventing both accidental and intentional data exfiltration.",
"addedComponent": [],
"label": "Disable automatic forwarding to external recipients",
"impact": "High Impact",
@@ -1697,9 +2207,10 @@
{
"name": "standards.RetentionPolicyTag",
"cat": "Exchange Standards",
- "tag": [],
+ "tag": ["CIS M365 5.0 (6.4.1)"],
"helpText": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.",
"docsDescription": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.",
+ "executiveText": "Automatically and permanently removes deleted emails after a specified number of days, helping manage storage costs and ensuring compliance with data retention policies. This prevents accumulation of unnecessary deleted items while maintaining a reasonable recovery window for accidentally deleted emails.",
"addedComponent": [
{
"type": "number",
@@ -1721,6 +2232,7 @@
"tag": [],
"helpText": "Sets a e-mail address to alert when a User requests to release a quarantined message.",
"docsDescription": "Sets a e-mail address to alert when a User requests to release a quarantined message. This is useful for monitoring and ensuring that the correct messages are released.",
+ "executiveText": "Notifies IT administrators when employees request to release emails that were quarantined for security reasons, enabling oversight of potentially dangerous messages. This helps ensure that legitimate emails are released while maintaining security controls over suspicious content.",
"addedComponent": [
{
"type": "textField",
@@ -1741,6 +2253,7 @@
"tag": [],
"helpText": "Sets a e-mail address to alert when a User deletes more than 20 SharePoint files within 60 minutes. NB: Requires a Office 365 E5 subscription, Office 365 E3 with Threat Intelligence or Office 365 EquivioAnalytics add-on.",
"docsDescription": "Sets a e-mail address to alert when a User deletes more than 20 SharePoint files within 60 minutes. This is useful for monitoring and ensuring that the correct SharePoint files are deleted. NB: Requires a Office 365 E5 subscription, Office 365 E3 with Threat Intelligence or Office 365 EquivioAnalytics add-on.",
+ "executiveText": "Alerts administrators when employees delete large numbers of SharePoint files in a short time period, helping detect potential data destruction attacks, ransomware, or accidental mass deletions. This early warning system enables rapid response to protect critical business documents and data.",
"addedComponent": [
{
"type": "number",
@@ -1770,12 +2283,54 @@
"powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert",
"recommendedBy": []
},
+ {
+ "name": "standards.SafeLinksTemplatePolicy",
+ "label": "SafeLinks Policy Template",
+ "cat": "Templates",
+ "multiple": false,
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "impact": "Medium Impact",
+ "addedDate": "2025-04-29",
+ "helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.",
+ "executiveText": "Deploys standardized URL protection policies that automatically scan and verify links in emails and documents before users click them. This template-based approach ensures consistent protection against malicious websites and phishing attacks across the organization.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": true,
+ "creatable": false,
+ "name": "standards.SafeLinksTemplatePolicy.TemplateIds",
+ "label": "Select SafeLinks Policy Templates",
+ "api": {
+ "url": "/api/ListSafeLinksPolicyTemplates",
+ "labelField": "TemplateName",
+ "valueField": "GUID",
+ "queryKey": "ListSafeLinksPolicyTemplates"
+ }
+ }
+ ]
+ },
{
"name": "standards.SafeLinksPolicy",
"cat": "Defender Standards",
- "tag": ["CIS", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps"],
+ "tag": [
+ "CIS M365 5.0 (2.1.1)",
+ "mdo_safelinksforemail",
+ "mdo_safelinksforOfficeApps",
+ "NIST CSF 2.0 (DE.CM-09)"
+ ],
"helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders",
"addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.SafeLinksPolicy.name",
+ "label": "Policy Name",
+ "required": true,
+ "defaultValue": "CIPP Default SafeLinks Policy"
+ },
{
"type": "switch",
"label": "AllowClickThrough",
@@ -1811,17 +2366,25 @@
"name": "standards.AntiPhishPolicy",
"cat": "Defender Standards",
"tag": [
- "CIS",
"mdo_safeattachments",
"mdo_highconfidencespamaction",
"mdo_highconfidencephishaction",
"mdo_phisspamacation",
"mdo_spam_notifications_only_for_admins",
"mdo_antiphishingpolicies",
- "mdo_phishthresholdlevel"
+ "mdo_phishthresholdlevel",
+ "CIS M365 5.0 (2.1.7)",
+ "NIST CSF 2.0 (DE.CM-09)"
],
"helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.",
"addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.AntiPhishPolicy.name",
+ "label": "Policy Name",
+ "required": true,
+ "defaultValue": "CIPP Default Anti-Phishing Policy"
+ },
{
"type": "number",
"label": "Phishing email threshold. (Default 1)",
@@ -1871,6 +2434,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for Spoof",
"name": "standards.AntiPhishPolicy.SpoofQuarantineTag",
"options": [
@@ -1911,6 +2475,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for user impersonation",
"name": "standards.AntiPhishPolicy.TargetedUserQuarantineTag",
"options": [
@@ -1951,6 +2516,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Quarantine policy for domain impersonation",
"name": "standards.AntiPhishPolicy.TargetedDomainQuarantineTag",
"options": [
@@ -1991,6 +2557,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "Apply quarantine policy",
"name": "standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag",
"options": [
@@ -2019,9 +2586,22 @@
{
"name": "standards.SafeAttachmentPolicy",
"cat": "Defender Standards",
- "tag": ["CIS", "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy"],
+ "tag": [
+ "CIS M365 5.0 (2.1.4)",
+ "mdo_safedocuments",
+ "mdo_commonattachmentsfilter",
+ "mdo_safeattachmentpolicy",
+ "NIST CSF 2.0 (DE.CM-09)"
+ ],
"helpText": "This creates a Safe Attachment policy",
"addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.SafeAttachmentPolicy.name",
+ "label": "Policy Name",
+ "required": true,
+ "defaultValue": "CIPP Default Safe Attachment Policy"
+ },
{
"type": "select",
"multiple": false,
@@ -2045,6 +2625,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "QuarantineTag",
"name": "standards.SafeAttachmentPolicy.QuarantineTag",
"options": [
@@ -2071,7 +2652,12 @@
"type": "textField",
"name": "standards.SafeAttachmentPolicy.RedirectAddress",
"label": "Redirect Address",
- "required": false
+ "required": false,
+ "condition": {
+ "field": "standards.SafeAttachmentPolicy.Redirect",
+ "compareType": "is",
+ "compareValue": true
+ }
}
],
"label": "Default Safe Attachment Policy",
@@ -2084,7 +2670,7 @@
{
"name": "standards.AtpPolicyForO365",
"cat": "Defender Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"],
"helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.",
"addedComponent": [
{
@@ -2131,6 +2717,13 @@
"required": false,
"label": "Phishing Simulation Urls",
"name": "standards.PhishingSimulations.PhishingSimUrls"
+ },
+ {
+ "type": "switch",
+ "label": "Remove extra urls",
+ "name": "standards.PhishingSimulations.RemoveExtraUrls",
+ "defaultValue": false,
+ "required": false
}
],
"label": "Phishing Simulation Configuration",
@@ -2143,9 +2736,23 @@
{
"name": "standards.MalwareFilterPolicy",
"cat": "Defender Standards",
- "tag": ["CIS", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware"],
+ "tag": [
+ "CIS M365 5.0 (2.1.2)",
+ "CIS M365 5.0 (2.1.3)",
+ "mdo_zapspam",
+ "mdo_zapphish",
+ "mdo_zapmalware",
+ "NIST CSF 2.0 (DE.CM-09)"
+ ],
"helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.",
"addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.MalwareFilterPolicy.name",
+ "label": "Policy Name",
+ "required": true,
+ "defaultValue": "CIPP Default Malware Policy"
+ },
{
"type": "select",
"multiple": false,
@@ -2171,6 +2778,7 @@
{
"type": "select",
"multiple": false,
+ "creatable": true,
"label": "QuarantineTag",
"name": "standards.MalwareFilterPolicy.QuarantineTag",
"options": [
@@ -2198,7 +2806,12 @@
"type": "textField",
"name": "standards.MalwareFilterPolicy.InternalSenderAdminAddress",
"required": false,
- "label": "Internal Sender Admin Address"
+ "label": "Internal Sender Admin Address",
+ "condition": {
+ "field": "standards.MalwareFilterPolicy.EnableInternalSenderAdminNotifications",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "switch",
@@ -2210,7 +2823,12 @@
"type": "textField",
"name": "standards.MalwareFilterPolicy.ExternalSenderAdminAddress",
"required": false,
- "label": "External Sender Admin Address"
+ "label": "External Sender Admin Address",
+ "condition": {
+ "field": "standards.MalwareFilterPolicy.EnableExternalSenderAdminNotifications",
+ "compareType": "is",
+ "compareValue": true
+ }
}
],
"label": "Default Malware Filter Policy",
@@ -2226,6 +2844,13 @@
"tag": [],
"helpText": "This adds allowed domains to the Spoof Intelligence Allow/Block List.",
"addedComponent": [
+ {
+ "type": "switch",
+ "label": "Remove extra domains from the allow list",
+ "name": "standards.PhishSimSpoofIntelligence.RemoveExtraDomains",
+ "defaultValue": false,
+ "required": false
+ },
{
"type": "autoComplete",
"multiple": true,
@@ -2247,7 +2872,15 @@
"cat": "Defender Standards",
"tag": [],
"helpText": "This standard creates a Spam filter policy similar to the default strict policy.",
+ "docsDescription": "This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled",
"addedComponent": [
+ {
+ "type": "textField",
+ "name": "standards.SpamFilterPolicy.name",
+ "label": "Policy Name",
+ "required": true,
+ "defaultValue": "CIPP Default Spam Filter Policy"
+ },
{
"type": "number",
"label": "Bulk email threshold (Default 7)",
@@ -2276,7 +2909,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Spam Quarantine Tag",
"name": "standards.SpamFilterPolicy.SpamQuarantineTag",
"options": [
@@ -2316,7 +2949,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "High Confidence Spam Quarantine Tag",
"name": "standards.SpamFilterPolicy.HighConfidenceSpamQuarantineTag",
"options": [
@@ -2356,7 +2989,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Bulk Quarantine Tag",
"name": "standards.SpamFilterPolicy.BulkQuarantineTag",
"options": [
@@ -2396,7 +3029,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "Phish Quarantine Tag",
"name": "standards.SpamFilterPolicy.PhishQuarantineTag",
"options": [
@@ -2418,7 +3051,7 @@
"type": "autoComplete",
"required": true,
"multiple": false,
- "creatable": false,
+ "creatable": true,
"label": "High Confidence Phish Quarantine Tag",
"name": "standards.SpamFilterPolicy.HighConfidencePhishQuarantineTag",
"options": [
@@ -2496,7 +3129,12 @@
"creatable": true,
"required": false,
"name": "standards.SpamFilterPolicy.LanguageBlockList",
- "label": "Languages to block (uppercase ISO 639-1 two-letter)"
+ "label": "Languages to block (uppercase ISO 639-1 two-letter)",
+ "condition": {
+ "field": "standards.SpamFilterPolicy.EnableLanguageBlockList",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "switch",
@@ -2510,7 +3148,12 @@
"creatable": true,
"required": false,
"name": "standards.SpamFilterPolicy.RegionBlockList",
- "label": "Regions to block (uppercase ISO 3166-1 two-letter)"
+ "label": "Regions to block (uppercase ISO 3166-1 two-letter)",
+ "condition": {
+ "field": "standards.SpamFilterPolicy.EnableRegionBlockList",
+ "compareType": "is",
+ "compareValue": true
+ }
},
{
"type": "autoComplete",
@@ -2528,16 +3171,104 @@
"powershellEquivalent": "New-HostedContentFilterPolicy or Set-HostedContentFilterPolicy",
"recommendedBy": []
},
+ {
+ "name": "standards.QuarantineTemplate",
+ "cat": "Defender Standards",
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "tag": [],
+ "helpText": "This standard creates a Custom Quarantine Policies that can be used in Anti-Spam and all MDO365 policies. Quarantine Policies can be used to specify recipients permissions, enable end-user spam notifications, and specify the release action preference",
+ "executiveText": "Creates standardized quarantine policies that define how employees can interact with quarantined emails, including permissions to release, delete, or preview suspicious messages. This ensures consistent security handling across the organization while providing appropriate user access to manage quarantined content.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": true,
+ "name": "displayName",
+ "label": "Quarantine Display Name",
+ "required": true
+ },
+ {
+ "type": "switch",
+ "label": "Enable end-user spam notifications",
+ "name": "ESNEnabled",
+ "defaultValue": true,
+ "required": false
+ },
+ {
+ "type": "select",
+ "multiple": false,
+ "label": "Select release action preference",
+ "name": "ReleaseAction",
+ "options": [
+ {
+ "label": "Allow recipients to request a message to be released from quarantine",
+ "value": "PermissionToRequestRelease"
+ },
+ {
+ "label": "Allow recipients to release a message from quarantine",
+ "value": "PermissionToRelease"
+ }
+ ]
+ },
+ {
+ "type": "switch",
+ "label": "Include Messages From Blocked Sender Address",
+ "name": "IncludeMessagesFromBlockedSenderAddress",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to delete message",
+ "name": "PermissionToDelete",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to preview message",
+ "name": "PermissionToPreview",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to block Sender Address",
+ "name": "PermissionToBlockSender",
+ "defaultValue": false,
+ "required": false
+ },
+ {
+ "type": "switch",
+ "label": "Allow recipients to whitelist Sender Address",
+ "name": "PermissionToAllowSender",
+ "defaultValue": false,
+ "required": false
+ }
+ ],
+ "label": "Custom Quarantine Policy",
+ "multiple": true,
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-05-16",
+ "powershellEquivalent": "Set-QuarantinePolicy or New-QuarantinePolicy",
+ "recommendedBy": []
+ },
{
"name": "standards.intuneDeviceRetirementDays",
"cat": "Intune Standards",
"tag": [],
- "helpText": "A value between 0 and 270 is supported. A value of 0 disables retirement, retired devices are removed from Intune after the specified number of days.",
+ "helpText": "A value between 31 and 365 is supported. retired devices are removed from Intune after the specified number of days.",
+ "executiveText": "Automatically removes inactive devices from management after a specified period, helping maintain a clean device inventory and reducing security risks from abandoned or lost devices. This policy ensures that only actively used corporate devices remain in the management system.",
"addedComponent": [
{
"type": "number",
"name": "standards.intuneDeviceRetirementDays.days",
- "label": "Maximum days (0 equals disabled)"
+ "label": "Maximum days"
}
],
"label": "Set inactive device retirement days",
@@ -2552,6 +3283,7 @@
"cat": "Intune Standards",
"tag": [],
"helpText": "Sets the branding profile for the Intune Company Portal app. This is a tenant wide setting and overrules any settings set on the app level.",
+ "executiveText": "Customizes the Intune Company Portal app with company branding, contact information, and support details, providing employees with a consistent corporate experience when managing their devices. This improves user experience and ensures employees know how to get IT support when needed.",
"addedComponent": [
{
"type": "textField",
@@ -2625,6 +3357,7 @@
"cat": "Intune Standards",
"tag": [],
"helpText": "Sets the mark devices with no compliance policy assigned as compliance/non compliant and Compliance status validity period.",
+ "executiveText": "Configures how the system treats devices that don't have specific compliance policies and sets how often devices must check in to maintain their compliance status. This ensures proper security oversight of all corporate devices and maintains current compliance information.",
"addedComponent": [
{
"type": "autoComplete",
@@ -2663,15 +3396,25 @@
"tag": [],
"helpText": "Configures the MDM user scope. This also sets the terms of use, discovery and compliance URL to default URLs.",
"docsDescription": "Configures the MDM user scope. This also sets the terms of use URL, discovery URL and compliance URL to default values.",
+ "executiveText": "Defines which users can enroll their devices in mobile device management, controlling access to corporate resources and applications. This setting determines the scope of device management coverage and ensures appropriate users have access to necessary business tools.",
"addedComponent": [
{
"name": "appliesTo",
"label": "MDM User Scope?",
"type": "radio",
"options": [
- { "label": "All", "value": "all" },
- { "label": "None", "value": "none" },
- { "label": "Custom Group", "value": "selected" }
+ {
+ "label": "All",
+ "value": "all"
+ },
+ {
+ "label": "None",
+ "value": "none"
+ },
+ {
+ "label": "Custom Group",
+ "value": "selected"
+ }
]
},
{
@@ -2691,8 +3434,9 @@
{
"name": "standards.DefaultPlatformRestrictions",
"cat": "Intune Standards",
- "tag": [],
+ "tag": ["CISA (MS.AAD.19.1v1)"],
"helpText": "Sets the default platform restrictions for enrolling devices into Intune. Note: Do not block personally owned if platform is blocked.",
+ "executiveText": "Controls which types of devices (iOS, Android, Windows, macOS) and ownership models (corporate vs. personal) can be enrolled in the company's device management system. This helps maintain security standards while supporting necessary business device types and usage scenarios.",
"addedComponent": [
{
"type": "switch",
@@ -2763,96 +3507,256 @@
"recommendedBy": []
},
{
- "name": "standards.intuneDeviceReg",
- "cat": "Intune Standards",
- "tag": [],
- "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users",
- "addedComponent": [
- {
- "type": "number",
- "name": "standards.intuneDeviceReg.max",
- "label": "Maximum devices (Enter 2147483647 for unlimited.)",
- "required": true
- }
- ],
- "label": "Set Maximum Number of Devices per user",
- "impact": "Medium Impact",
- "impactColour": "warning",
- "addedDate": "2023-03-27",
- "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy",
- "recommendedBy": []
- },
- {
- "name": "standards.intuneRequireMFA",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration",
"cat": "Intune Standards",
"tag": [],
- "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.",
- "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra",
- "impact": "Medium Impact",
- "impactColour": "warning",
- "addedDate": "2023-10-23",
- "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy",
- "recommendedBy": []
- },
- {
- "name": "standards.DeletedUserRentention",
- "cat": "SharePoint Standards",
- "tag": [],
- "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.",
- "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.",
+ "helpText": "Sets the Windows Hello for Business configuration during device enrollment.",
+ "executiveText": "Enables or disables Windows Hello for Business during device enrollment, enhancing security through biometric or PIN-based authentication methods. This ensures that devices meet corporate security standards while providing a user-friendly sign-in experience.",
"addedComponent": [
{
"type": "autoComplete",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.state",
+ "label": "Configure Windows Hello for Business",
"multiple": false,
- "name": "standards.DeletedUserRentention.Days",
- "label": "Retention time (Default 30 days)",
"options": [
{
- "label": "30 days",
- "value": "30"
+ "label": "Not configured",
+ "value": "notConfigured"
},
{
- "label": "90 days",
- "value": "90"
+ "label": "Enabled",
+ "value": "enabled"
},
{
- "label": "1 year",
- "value": "365"
- },
+ "label": "Disabled",
+ "value": "disabled"
+ }
+ ]
+ },
+ {
+ "type": "switch",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.securityDeviceRequired",
+ "label": "Use a Trusted Platform Module (TPM)",
+ "default": true
+ },
+ {
+ "type": "number",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinMinimumLength",
+ "label": "Minimum PIN length (4-127)",
+ "default": 4
+ },
+ {
+ "type": "number",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinMaximumLength",
+ "label": "Maximum PIN length (4-127)",
+ "default": 127
+ },
+ {
+ "type": "autoComplete",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinLowercaseCharactersUsage",
+ "label": "Lowercase letters in PIN",
+ "multiple": false,
+ "options": [
{
- "label": "2 years",
- "value": "730"
+ "label": "Not allowed",
+ "value": "disallowed"
},
{
- "label": "3 years",
- "value": "1095"
+ "label": "Allowed",
+ "value": "allowed"
},
{
- "label": "4 years",
- "value": "1460"
- },
+ "label": "Required",
+ "value": "required"
+ }
+ ]
+ },
+ {
+ "type": "autoComplete",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinUppercaseCharactersUsage",
+ "label": "Uppercase letters in PIN",
+ "multiple": false,
+ "options": [
{
- "label": "5 years",
- "value": "1825"
+ "label": "Not allowed",
+ "value": "disallowed"
},
{
- "label": "6 years",
- "value": "2190"
+ "label": "Allowed",
+ "value": "allowed"
},
{
- "label": "7 years",
- "value": "2555"
- },
+ "label": "Required",
+ "value": "required"
+ }
+ ]
+ },
+ {
+ "type": "autoComplete",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinSpecialCharactersUsage",
+ "label": "Special characters in PIN",
+ "multiple": false,
+ "options": [
{
- "label": "8 years",
- "value": "2920"
+ "label": "Not allowed",
+ "value": "disallowed"
},
{
- "label": "9 years",
- "value": "3285"
+ "label": "Allowed",
+ "value": "allowed"
},
{
- "label": "10 years",
+ "label": "Required",
+ "value": "required"
+ }
+ ]
+ },
+ {
+ "type": "number",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinExpirationInDays",
+ "label": "PIN expiration (days) - 0 to disable",
+ "default": 0
+ },
+ {
+ "type": "number",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.pinPreviousBlockCount",
+ "label": "PIN history - 0 to disable",
+ "default": 0
+ },
+ {
+ "type": "switch",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.unlockWithBiometricsEnabled",
+ "label": "Allow biometric authentication",
+ "default": true
+ },
+ {
+ "type": "autoComplete",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.enhancedBiometricsState",
+ "label": "Use enhanced anti-spoofing when available",
+ "multiple": false,
+ "options": [
+ {
+ "label": "Not configured",
+ "value": "notConfigured"
+ },
+ {
+ "label": "Enabled",
+ "value": "enabled"
+ },
+ {
+ "label": "Disabled",
+ "value": "disabled"
+ }
+ ]
+ },
+ {
+ "type": "switch",
+ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.remotePassportEnabled",
+ "label": "Allow phone sign-in",
+ "default": true
+ }
+ ],
+ "label": "Windows Hello for Business enrollment configuration",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-09-25",
+ "powershellEquivalent": "Graph API",
+ "recommendedBy": []
+ },
+ {
+ "name": "standards.intuneDeviceReg",
+ "cat": "Intune Standards",
+ "tag": ["CISA (MS.AAD.17.1v1)"],
+ "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users",
+ "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.",
+ "addedComponent": [
+ {
+ "type": "number",
+ "name": "standards.intuneDeviceReg.max",
+ "label": "Maximum devices (Enter 2147483647 for unlimited.)",
+ "required": true
+ }
+ ],
+ "label": "Set Maximum Number of Devices per user",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2023-03-27",
+ "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy",
+ "recommendedBy": []
+ },
+ {
+ "name": "standards.intuneRequireMFA",
+ "cat": "Intune Standards",
+ "tag": [],
+ "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.",
+ "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.",
+ "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2023-10-23",
+ "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy",
+ "recommendedBy": []
+ },
+ {
+ "name": "standards.DeletedUserRentention",
+ "cat": "SharePoint Standards",
+ "tag": [],
+ "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.",
+ "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.",
+ "executiveText": "Preserves departed employees' OneDrive files for a specified period, allowing time to recover important business documents before permanent deletion. This helps prevent data loss while managing storage costs and maintaining compliance with data retention policies.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "name": "standards.DeletedUserRentention.Days",
+ "label": "Retention time (Default 30 days)",
+ "options": [
+ {
+ "label": "30 days",
+ "value": "30"
+ },
+ {
+ "label": "90 days",
+ "value": "90"
+ },
+ {
+ "label": "1 year",
+ "value": "365"
+ },
+ {
+ "label": "2 years",
+ "value": "730"
+ },
+ {
+ "label": "3 years",
+ "value": "1095"
+ },
+ {
+ "label": "4 years",
+ "value": "1460"
+ },
+ {
+ "label": "5 years",
+ "value": "1825"
+ },
+ {
+ "label": "6 years",
+ "value": "2190"
+ },
+ {
+ "label": "7 years",
+ "value": "2555"
+ },
+ {
+ "label": "8 years",
+ "value": "2920"
+ },
+ {
+ "label": "9 years",
+ "value": "3285"
+ },
+ {
+ "label": "10 years",
"value": "3650"
}
]
@@ -2865,11 +3769,39 @@
"powershellEquivalent": "Update-MgBetaAdminSharePointSetting",
"recommendedBy": []
},
+ {
+ "name": "standards.SPFileRequests",
+ "cat": "SharePoint Standards",
+ "tag": [],
+ "helpText": "Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.",
+ "docsDescription": "File Requests allow users to create secure upload-only share links where uploads are hidden from other people using the link. This creates a secure and private way for people to upload files to a folder. This feature is not enabled by default on new tenants and requires PowerShell configuration. This standard enables or disables this feature and optionally configures link expiration settings for both SharePoint and OneDrive.",
+ "executiveText": "Enables secure file upload functionality that allows external users to submit files directly to company folders without seeing other submissions or folder contents. This provides a professional and secure way to collect documents from clients, vendors, and partners while maintaining data privacy and security.",
+ "addedComponent": [
+ {
+ "type": "switch",
+ "name": "standards.SPFileRequests.state",
+ "label": "Enable File Requests"
+ },
+ {
+ "type": "number",
+ "name": "standards.SPFileRequests.expirationDays",
+ "label": "Link Expiration 1-730 Days (Optional)",
+ "required": false
+ }
+ ],
+ "label": "Set SharePoint and OneDrive File Requests",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-07-30",
+ "powershellEquivalent": "Set-SPOTenant -CoreRequestFilesLinkEnabled $true -OneDriveRequestFilesLinkEnabled $true -CoreRequestFilesLinkExpirationInDays 30 -OneDriveRequestFilesLinkExpirationInDays 30",
+ "recommendedBy": ["CIPP"]
+ },
{
"name": "standards.TenantDefaultTimezone",
"cat": "SharePoint Standards",
"tag": [],
"helpText": "Sets the default timezone for the tenant. This will be used for all new users and sites.",
+ "executiveText": "Standardizes the timezone setting across all SharePoint sites and new user accounts, ensuring consistent scheduling and time-based operations throughout the organization. This improves collaboration efficiency and reduces confusion in global or multi-timezone organizations.",
"addedComponent": [
{
"type": "TimezoneSelect",
@@ -2887,8 +3819,9 @@
{
"name": "standards.SPAzureB2B",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.2.2)"],
"helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled",
+ "executiveText": "Enables secure collaboration with external partners through SharePoint and OneDrive by integrating with Azure B2B guest access. This allows controlled sharing with external organizations while maintaining security oversight and proper access management.",
"addedComponent": [],
"label": "Enable SharePoint and OneDrive integration with Azure AD B2B",
"impact": "Low Impact",
@@ -2900,8 +3833,9 @@
{
"name": "standards.SPDisallowInfectedFiles",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"],
"helpText": "Ensure Office 365 SharePoint infected files are disallowed for download",
+ "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.",
"addedComponent": [],
"label": "Disallow downloading infected files from SharePoint",
"impact": "Low Impact",
@@ -2915,6 +3849,7 @@
"cat": "SharePoint Standards",
"tag": [],
"helpText": "Disables the creation of new SharePoint 2010 and 2013 classic workflows and removes the 'Return to classic SharePoint' link on modern SharePoint list and library pages.",
+ "executiveText": "Disables outdated SharePoint workflow features and classic interface options, encouraging use of modern, more secure and efficient collaboration tools. This helps maintain security standards while guiding users toward current, supported functionality.",
"addedComponent": [],
"label": "Disable Legacy Workflows",
"impact": "Low Impact",
@@ -2926,8 +3861,9 @@
{
"name": "standards.SPDirectSharing",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
- "helpText": "Ensure default link sharing is set to Direct in SharePoint and OneDrive",
+ "tag": [],
+ "helpText": "This standard has been deprecated in favor of the Default Sharing Link standard. ",
+ "executiveText": "Configures SharePoint and OneDrive to share files directly with specific people rather than creating anonymous links, improving security by ensuring only intended recipients can access shared documents. This reduces the risk of accidental data exposure through link sharing.",
"addedComponent": [],
"label": "Default sharing to Direct users",
"impact": "Medium Impact",
@@ -2939,8 +3875,9 @@
{
"name": "standards.SPExternalUserExpiration",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)"],
"helpText": "Ensure guest access to a site or OneDrive will expire automatically",
+ "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.",
"addedComponent": [
{
"type": "number",
@@ -2958,8 +3895,9 @@
{
"name": "standards.SPEmailAttestation",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)"],
"helpText": "Ensure re-authentication with verification code is restricted",
+ "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.",
"addedComponent": [
{
"type": "number",
@@ -2974,11 +3912,46 @@
"powershellEquivalent": "Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15",
"recommendedBy": ["CIS", "CIPP"]
},
+ {
+ "name": "standards.DefaultSharingLink",
+ "cat": "SharePoint Standards",
+ "tag": ["CIS M365 5.0 (7.2.7)", "CIS M365 5.0 (7.2.11)", "CISA (MS.SPO.1.4v1)"],
+ "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.",
+ "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.",
+ "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "required": true,
+ "label": "Default Sharing Link Type",
+ "name": "standards.DefaultSharingLink.SharingLinkType",
+ "options": [
+ {
+ "label": "Direct - Only the people the user specifies",
+ "value": "Direct"
+ },
+ {
+ "label": "Internal - Only people in your organization",
+ "value": "Internal"
+ }
+ ]
+ }
+ ],
+ "label": "Set Default Sharing Link Settings",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-13",
+ "powershellEquivalent": "Set-SPOTenant -DefaultSharingLinkType [Direct|Internal] -DefaultLinkPermission View",
+ "recommendedBy": ["CIS", "CIPP"]
+ },
{
"name": "standards.DisableAddShortcutsToOneDrive",
"cat": "SharePoint Standards",
"tag": [],
"helpText": "If disabled, the button Add shortcut to OneDrive will be removed and users in the tenant will no longer be able to add new shortcuts to their OneDrive. Existing shortcuts will remain functional",
+ "executiveText": "Controls whether employees can create shortcuts to SharePoint libraries in their OneDrive, managing how users organize and access shared content. This setting helps maintain organized file structures and can prevent confusion from excessive shortcuts while preserving existing workflows.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3010,6 +3983,7 @@
"cat": "SharePoint Standards",
"tag": [],
"helpText": "If disabled, users in the tenant will no longer be able to use the Sync button to sync SharePoint content on all sites. However, existing synced content will remain functional on the user's computer.",
+ "executiveText": "Controls whether employees can synchronize SharePoint files to their local devices, balancing productivity benefits with data security concerns. This setting helps manage data distribution while maintaining access to cloud-based collaboration when sync is disabled.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3039,9 +4013,16 @@
{
"name": "standards.DisableSharePointLegacyAuth",
"cat": "SharePoint Standards",
- "tag": ["CIS", "spo_legacy_auth"],
+ "tag": [
+ "CIS M365 5.0 (6.5.1)",
+ "CIS M365 5.0 (7.2.1)",
+ "spo_legacy_auth",
+ "CISA (MS.AAD.3.1v1)",
+ "NIST CSF 2.0 (PR.IR-01)"
+ ],
"helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.",
"docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.",
+ "executiveText": "Disables outdated authentication methods for SharePoint access, forcing applications and users to use modern, more secure authentication protocols. This significantly improves security by eliminating vulnerable authentication pathways while requiring updates to older applications.",
"addedComponent": [],
"label": "Disable legacy basic authentication for SharePoint",
"impact": "Medium Impact",
@@ -3053,8 +4034,9 @@
{
"name": "standards.sharingCapability",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)"],
"helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level",
+ "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3091,9 +4073,10 @@
{
"name": "standards.DisableReshare",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"],
"helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access",
"docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level",
+ "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.",
"addedComponent": [],
"label": "Disable Re-sharing by External Users",
"impact": "High Impact",
@@ -3108,6 +4091,7 @@
"tag": [],
"helpText": "Disables users from creating new SharePoint sites",
"docsDescription": "Disables standard users from creating SharePoint sites, also disables the ability to fully create teams",
+ "executiveText": "Restricts the creation of new SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces and ensuring proper governance. This maintains organized information architecture while requiring approval for new collaborative environments.",
"addedComponent": [],
"label": "Disable site creation by standard users",
"impact": "High Impact",
@@ -3121,6 +4105,7 @@
"cat": "SharePoint Standards",
"tag": [],
"helpText": "Sets the file extensions that are excluded from syncing with OneDrive. These files will be blocked from upload. '*.' is automatically added to the extension and can be omitted.",
+ "executiveText": "Blocks specific file types from being uploaded or synchronized to OneDrive, helping prevent security risks from potentially dangerous file formats. This security measure protects against malware distribution while allowing legitimate business file types to be shared safely.",
"addedComponent": [
{
"type": "textField",
@@ -3140,6 +4125,7 @@
"cat": "SharePoint Standards",
"tag": [],
"helpText": "Disables the ability for Mac devices to sync with OneDrive.",
+ "executiveText": "Prevents Mac computers from synchronizing files with OneDrive, typically implemented for security or compliance reasons in Windows-centric environments. This restriction helps maintain standardized device management while potentially limiting collaboration for Mac users.",
"addedComponent": [],
"label": "Do not allow Mac devices to sync using OneDrive",
"impact": "High Impact",
@@ -3151,21 +4137,43 @@
{
"name": "standards.unmanagedSync",
"cat": "SharePoint Standards",
- "tag": [],
- "helpText": "The unmanaged Sync standard has been temporarily disabled and does nothing.",
- "addedComponent": [],
- "label": "Only allow users to sync OneDrive from AAD joined devices",
+ "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"],
+ "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.",
+ "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)",
+ "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "name": "standards.unmanagedSync.state",
+ "label": "State",
+ "options": [
+ {
+ "label": "Allow limited, web-only access",
+ "value": "1"
+ },
+ {
+ "label": "Block access",
+ "value": "2"
+ }
+ ],
+ "required": false
+ }
+ ],
+ "label": "Restrict access to SharePoint and OneDrive from unmanaged devices",
"impact": "High Impact",
"impactColour": "danger",
- "addedDate": "2022-06-15",
- "powershellEquivalent": "Update-MgAdminSharePointSetting",
- "recommendedBy": []
+ "addedDate": "2025-06-13",
+ "powershellEquivalent": "Set-SPOTenant -ConditionalAccessPolicy AllowFullAccess | AllowLimitedAccess | BlockAccess",
+ "recommendedBy": ["CIS"]
},
{
"name": "standards.sharingDomainRestriction",
"cat": "SharePoint Standards",
- "tag": ["CIS"],
+ "tag": ["CIS M365 5.0 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"],
"helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.",
+ "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3204,8 +4212,16 @@
{
"name": "standards.TeamsGlobalMeetingPolicy",
"cat": "Teams Standards",
- "tag": [],
+ "tag": [
+ "CIS M365 5.0 (8.5.1)",
+ "CIS M365 5.0 (8.5.2)",
+ "CIS M365 5.0 (8.5.3)",
+ "CIS M365 5.0 (8.5.4)",
+ "CIS M365 5.0 (8.5.5)",
+ "CIS M365 5.0 (8.5.6)"
+ ],
"helpText": "Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl",
+ "executiveText": "Establishes security-focused default settings for Teams meetings, controlling who can join meetings, present content, and participate in chats. These policies balance collaboration needs with security requirements, ensuring meetings remain productive while protecting against unauthorized access and disruption.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3274,11 +4290,71 @@
"recommendedBy": ["CIS"]
},
{
- "name": "standards.TeamsEmailIntegration",
+ "name": "standards.TeamsChatProtection",
+ "cat": "Teams Standards",
+ "tag": [],
+ "helpText": "Configures Teams chat protection settings including weaponizable file protection and malicious URL protection.",
+ "docsDescription": "Configures Teams messaging safety features to protect users from weaponizable files and malicious URLs in chats and channels. Weaponizable File Protection automatically blocks messages containing potentially dangerous file types (like .exe, .dll, .bat, etc.). Malicious URL Protection scans URLs in messages and displays warnings when potentially harmful links are detected. These protections work across internal and external collaboration scenarios.",
+ "executiveText": "Enables automated security protections in Microsoft Teams to block dangerous files and warn users about malicious links in chat messages. This helps protect employees from file-based attacks and phishing attempts. These safeguards work seamlessly in the background, providing essential protection without disrupting normal business communication.",
+ "addedComponent": [
+ {
+ "type": "switch",
+ "name": "standards.TeamsChatProtection.FileTypeCheck",
+ "label": "Enable Weaponizable File Protection",
+ "defaultValue": true
+ },
+ {
+ "type": "switch",
+ "name": "standards.TeamsChatProtection.UrlReputationCheck",
+ "label": "Enable Malicious URL Protection",
+ "defaultValue": true
+ }
+ ],
+ "label": "Set Teams Chat Protection Settings",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-10-02",
+ "powershellEquivalent": "Set-CsTeamsMessagingConfiguration -FileTypeCheck 'Enabled' -UrlReputationCheck 'Enabled' -ReportIncorrectSecurityDetections 'Enabled'",
+ "recommendedBy": ["CIPP"]
+ },
+ {
+ "name": "standards.TeamsExternalChatWithAnyone",
"cat": "Teams Standards",
"tag": [],
+ "helpText": "Controls whether users can start Teams chats with any email address, inviting external recipients as guests via email.",
+ "docsDescription": "Manages the Teams messaging policy setting UseB2BInvitesToAddExternalUsers. When enabled, users can start chats with any email address and recipients receive an invitation to join the chat as guests. Disabling the setting prevents these external email chats from being created, keeping conversations limited to internal users and approved guests.",
+ "executiveText": "Allows organizations to decide if employees can launch Microsoft Teams chats with anyone on the internet using just an email address. Disabling the feature keeps conversations inside trusted boundaries and helps prevent accidental data exposure through unexpected external invitations.",
+ "addedComponent": [
+ {
+ "type": "radio",
+ "name": "standards.TeamsExternalChatWithAnyone.UseB2BInvitesToAddExternalUsers",
+ "label": "Allow chatting with anyone via email",
+ "options": [
+ {
+ "label": "Enabled",
+ "value": "true"
+ },
+ {
+ "label": "Disabled",
+ "value": "false"
+ }
+ ],
+ "defaultValue": "Disabled"
+ }
+ ],
+ "label": "Set Teams chat with anyone setting",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-11-03",
+ "powershellEquivalent": "Set-CsTeamsMessagingPolicy -Identity Global -UseB2BInvitesToAddExternalUsers $false/$true",
+ "recommendedBy": ["CIPP"]
+ },
+ {
+ "name": "standards.TeamsEmailIntegration",
+ "cat": "Teams Standards",
"helpText": "Should users be allowed to send emails directly to a channel email addresses?",
"docsDescription": "Teams channel email addresses are an optional feature that allows users to email the Teams channel directly.",
+ "executiveText": "Controls whether Teams channels can receive emails directly, enabling integration between email and team collaboration. This feature can improve workflow efficiency by allowing external communications to flow into team discussions, though it may need management for security or organizational reasons.",
"addedComponent": [
{
"type": "switch",
@@ -3291,13 +4367,69 @@
"impactColour": "info",
"addedDate": "2024-07-30",
"powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false",
- "recommendedBy": ["CIS"]
+ "recommendedBy": ["CIS"],
+ "tag": ["CIS M365 5.0 (8.1.2)"]
},
{
- "name": "standards.TeamsExternalFileSharing",
+ "name": "standards.TeamsGuestAccess",
+ "cat": "Teams Standards",
+ "tag": [],
+ "helpText": "Allow guest users access to teams.",
+ "docsDescription": "Allow guest users access to teams. Guest users are users who are not part of your organization but have been invited to collaborate with your organization in Teams. This setting allows you to control whether guest users can access Teams.",
+ "executiveText": "Determines whether external partners, vendors, and collaborators can be invited to participate in Teams conversations and meetings. This fundamental setting enables external collaboration while requiring careful management to balance openness with security and information protection.",
+ "addedComponent": [
+ {
+ "type": "switch",
+ "name": "standards.TeamsGuestAccess.AllowGuestUser",
+ "label": "Allow guest users"
+ }
+ ],
+ "label": "Allow guest users in Teams",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-03",
+ "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowGuestUser $true",
+ "recommendedBy": []
+ },
+ {
+ "name": "standards.TeamsMeetingVerification",
"cat": "Teams Standards",
"tag": [],
+ "helpText": "Configures CAPTCHA verification for external users joining Teams meetings. This helps prevent unauthorized AI notetakers and bots from joining meetings.",
+ "docsDescription": "Configures CAPTCHA verification for external users joining Teams meetings. This security feature requires external participants to complete a CAPTCHA challenge before joining, which helps prevent unauthorized AI notetakers, bots, and other automated systems from accessing meetings.",
+ "executiveText": "Requires external meeting participants to complete verification challenges before joining Teams meetings, preventing automated bots and unauthorized AI systems from accessing confidential discussions. This security measure protects against meeting infiltration while maintaining legitimate external collaboration.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "creatable": false,
+ "label": "CAPTCHA Verification Setting",
+ "name": "standards.TeamsMeetingVerification.CaptchaVerificationForMeetingJoin",
+ "options": [
+ {
+ "label": "Not Required",
+ "value": "NotRequired"
+ },
+ {
+ "label": "Anonymous Users and Untrusted Organizations",
+ "value": "AnonymousUsersAndUntrustedOrganizations"
+ }
+ ]
+ }
+ ],
+ "label": "Teams Meeting Verification (CAPTCHA)",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-06-14",
+ "powershellEquivalent": "Set-CsTeamsMeetingPolicy -CaptchaVerificationForMeetingJoin",
+ "recommendedBy": ["CIPP"]
+ },
+ {
+ "name": "standards.TeamsExternalFileSharing",
+ "cat": "Teams Standards",
+ "tag": ["CIS M365 5.0 (8.4.1)"],
"helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.",
+ "executiveText": "Controls which external cloud storage services (like Google Drive, Dropbox, Box) employees can access through Teams, ensuring file sharing occurs only through approved and secure platforms. This helps maintain data governance while supporting necessary business integrations.",
"addedComponent": [
{
"type": "switch",
@@ -3338,6 +4470,7 @@
"tag": [],
"helpText": "Controls whether users with this policy can set the voice profile capture and enrollment through the Recognition tab in their Teams client settings.",
"docsDescription": "Controls whether users with this policy can set the voice profile capture and enrollment through the Recognition tab in their Teams client settings.",
+ "executiveText": "Determines whether employees can enroll their voice and face profiles for recognition features in Teams, enabling personalized experiences like voice identification. This setting balances convenience features with privacy considerations and organizational policies regarding biometric data collection.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3371,6 +4504,7 @@
"tag": [],
"helpText": "Sets the properties of the Global external access policy.",
"docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.",
+ "executiveText": "Defines the organization's policy for communicating with external users through Teams, including other organizations, Skype users, and unmanaged accounts. This fundamental setting determines the scope of external collaboration while maintaining security boundaries for business communications.",
"addedComponent": [
{
"type": "switch",
@@ -3396,6 +4530,7 @@
"tag": [],
"helpText": "Sets the properties of the Global federation configuration.",
"docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.",
+ "executiveText": "Configures how the organization federates with external organizations for Teams communication, controlling whether employees can communicate with specific external domains or all external organizations. This setting enables secure inter-organizational collaboration while maintaining control over external communications.",
"addedComponent": [
{
"type": "switch",
@@ -3448,6 +4583,7 @@
"tag": [],
"helpText": "Sets the default number of days after which Teams meeting recordings automatically expire. Valid values are -1 (Never Expire) or between 1 and 99999. The default value is 120 days.",
"docsDescription": "Allows administrators to configure a default expiration period (in days) for Teams meeting recordings. Recordings older than this period will be automatically moved to the recycle bin. This setting helps manage storage consumption and enforce data retention policies.",
+ "executiveText": "Automatically removes old Teams meeting recordings after a specified period to manage storage costs and comply with data retention policies. This helps organizations balance the need to preserve important meeting content with storage efficiency and regulatory compliance requirements.",
"addedComponent": [
{
"type": "number",
@@ -3469,6 +4605,7 @@
"tag": [],
"helpText": "Sets the properties of the Global messaging policy.",
"docsDescription": "Sets the properties of the Global messaging policy. Messaging policies control which chat and channel messaging features are available to users in Teams.",
+ "executiveText": "Defines what messaging capabilities employees have in Teams, including the ability to edit or delete messages, create custom emojis, and report inappropriate content. These policies help maintain professional communication standards while enabling necessary collaboration features.",
"addedComponent": [
{
"type": "switch",
@@ -3559,6 +4696,7 @@
},
"helpText": "Deploy the Autopilot Status Page, which shows progress during device setup through Autopilot.",
"docsDescription": "This standard allows configuration of the Autopilot Status Page, providing users with a visual representation of the progress during device setup. It includes options like timeout, logging, and retry settings.",
+ "executiveText": "Provides employees with a visual progress indicator during automated device setup, improving the user experience when receiving new computers. This reduces IT support calls and helps ensure successful device deployment by guiding users through the setup process.",
"addedComponent": [
{
"type": "number",
@@ -3592,14 +4730,14 @@
},
{
"type": "switch",
- "name": "standards.AutopilotStatusPage.BlockDevice",
- "label": "Block device usage during setup",
+ "name": "standards.AutopilotStatusPage.InstallWindowsUpdates",
+ "label": "Install Windows Updates during setup",
"defaultValue": true
},
{
"type": "switch",
- "name": "standards.AutopilotStatusPage.AllowRetry",
- "label": "Allow retry",
+ "name": "standards.AutopilotStatusPage.BlockDevice",
+ "label": "Block device usage during setup",
"defaultValue": true
},
{
@@ -3646,7 +4784,8 @@
{
"type": "textField",
"name": "standards.AutopilotProfile.DeviceNameTemplate",
- "label": "Unique Device Name Template"
+ "label": "Unique Device Name Template",
+ "required": false
},
{
"type": "autoComplete",
@@ -3657,7 +4796,7 @@
"label": "Languages",
"api": {
"url": "/languageList.json",
- "labelField": "language",
+ "labelField": "languageTag",
"valueField": "tag"
}
},
@@ -3735,18 +4874,37 @@
"impact": "High Impact",
"addedDate": "2023-12-30",
"helpText": "Deploy and manage Intune templates across devices.",
+ "executiveText": "Deploys standardized device management configurations across all corporate devices, ensuring consistent security policies, application settings, and compliance requirements. This template-based approach streamlines device management while maintaining uniform security standards across the organization.",
"addedComponent": [
{
"type": "autoComplete",
"multiple": false,
"creatable": false,
+ "required": false,
"name": "TemplateList",
"label": "Select Intune Template",
"api": {
+ "queryKey": "ListIntuneTemplates-autcomplete",
"url": "/api/ListIntuneTemplates",
"labelField": "Displayname",
- "valueField": "GUID",
- "queryKey": "languages"
+ "valueField": "GUID"
+ }
+ },
+ {
+ "type": "autoComplete",
+ "multiple": false,
+ "required": false,
+ "creatable": false,
+ "name": "TemplateList-Tags",
+ "label": "Or select a package of Intune Templates",
+ "api": {
+ "queryKey": "ListIntuneTemplates-tag-autcomplete",
+ "url": "/api/ListIntuneTemplates?mode=Tag",
+ "labelField": "label",
+ "valueField": "value",
+ "addedField": {
+ "templates": "templates"
+ }
}
},
{
@@ -3754,11 +4912,26 @@
"label": "Who should this template be assigned to?",
"type": "radio",
"options": [
- { "label": "Do not assign", "value": "On" },
- { "label": "Assign to all users", "value": "allLicensedUsers" },
- { "label": "Assign to all devices", "value": "AllDevices" },
- { "label": "Assign to all users and devices", "value": "AllDevicesAndUsers" },
- { "label": "Assign to Custom Group", "value": "customGroup" }
+ {
+ "label": "Do not assign",
+ "value": "On"
+ },
+ {
+ "label": "Assign to all users",
+ "value": "allLicensedUsers"
+ },
+ {
+ "label": "Assign to all devices",
+ "value": "AllDevices"
+ },
+ {
+ "label": "Assign to all users and devices",
+ "value": "AllDevicesAndUsers"
+ },
+ {
+ "label": "Assign to Custom Group",
+ "value": "customGroup"
+ }
]
},
{
@@ -3772,7 +4945,31 @@
"label": "Exclude Groups",
"type": "textField",
"required": false,
- "helpText": "Enter the group name to exclude from the assignment. Wildcards are allowed."
+ "helpText": "Enter the group name(s) to exclude from the assignment. Wildcards are allowed. Multiple group names are comma-seperated."
+ },
+ {
+ "type": "textField",
+ "required": false,
+ "name": "assignmentFilter",
+ "label": "Assignment Filter Name (Optional)",
+ "helpText": "Enter the assignment filter name to apply to this policy assignment. Wildcards are allowed."
+ },
+ {
+ "name": "assignmentFilterType",
+ "label": "Assignment Filter Mode (Optional)",
+ "type": "radio",
+ "required": false,
+ "helpText": "Choose whether to include or exclude devices matching the filter. Only applies if you specified a filter name above. Defaults to Include if not specified.",
+ "options": [
+ {
+ "label": "Include - Assign to devices matching the filter",
+ "value": "include"
+ },
+ {
+ "label": "Exclude - Assign to devices NOT matching the filter",
+ "value": "exclude"
+ }
+ ]
}
]
},
@@ -3788,6 +4985,7 @@
"impact": "Medium Impact",
"addedDate": "2023-12-30",
"helpText": "Deploy transport rules to manage email flow.",
+ "executiveText": "Deploys standardized email flow rules that automatically manage how emails are processed, filtered, and routed within the organization. These templates ensure consistent email security policies, compliance requirements, and business rules are applied across all email communications.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3815,6 +5013,7 @@
"impact": "High Impact",
"addedDate": "2023-12-30",
"helpText": "Manage conditional access policies for better security.",
+ "executiveText": "Deploys standardized conditional access policies that automatically enforce security requirements based on user location, device compliance, and risk factors. These templates ensure consistent security controls across the organization while enabling secure access to business resources.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3833,17 +5032,35 @@
"label": "What state should we deploy this template in?",
"type": "radio",
"options": [
- { "value": "donotchange", "label": "Do not change state" },
- { "value": "Enabled", "label": "Set to enabled" },
- { "value": "Disabled", "label": "Set to disabled" },
- { "value": "enabledForReportingButNotEnforced", "label": "Set to report only" }
+ {
+ "value": "donotchange",
+ "label": "Do not change state"
+ },
+ {
+ "value": "Enabled",
+ "label": "Set to enabled"
+ },
+ {
+ "value": "Disabled",
+ "label": "Set to disabled"
+ },
+ {
+ "value": "enabledForReportingButNotEnforced",
+ "label": "Set to report only"
+ }
]
+ },
+ {
+ "type": "switch",
+ "name": "DisableSD",
+ "label": "Disable Security Defaults when deploying policy"
}
]
},
{
"name": "standards.ExchangeConnectorTemplate",
"label": "Exchange Connector Template",
+ "cat": "Templates",
"disabledFeatures": {
"report": true,
"warn": true,
@@ -3852,6 +5069,7 @@
"impact": "Medium Impact",
"addedDate": "2023-12-30",
"helpText": "Deploy and manage Exchange connectors.",
+ "executiveText": "Configures standardized Exchange connectors that control how email flows between your organization and external systems. These templates ensure secure and reliable email delivery while maintaining proper routing and security policies for business communications.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3879,6 +5097,7 @@
"impact": "Medium Impact",
"addedDate": "2023-12-30",
"helpText": "Deploy and manage group templates.",
+ "executiveText": "Creates standardized groups with predefined settings, permissions, and membership rules. These templates ensure consistent group configurations across the organization, streamlining collaboration and access management while maintaining security standards.",
"addedComponent": [
{
"type": "autoComplete",
@@ -3887,10 +5106,250 @@
"api": {
"url": "/api/ListGroupTemplates",
"labelField": "Displayname",
+ "altLabelField": "displayName",
"valueField": "GUID",
"queryKey": "ListGroupTemplates"
}
}
]
+ },
+ {
+ "name": "standards.AssignmentFilterTemplate",
+ "label": "Assignment Filter Template",
+ "multi": true,
+ "cat": "Templates",
+ "disabledFeatures": {
+ "report": true,
+ "warn": true,
+ "remediate": false
+ },
+ "impact": "Medium Impact",
+ "addedDate": "2025-10-04",
+ "helpText": "Deploy and manage assignment filter templates.",
+ "executiveText": "Creates standardized assignment filters with predefined settings. These templates ensure consistent assignment filter configurations across the organization, streamlining assignment management.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "name": "assignmentFilterTemplate",
+ "label": "Select Assignment Filter Template",
+ "api": {
+ "url": "/api/ListAssignmentFilterTemplates",
+ "labelField": "Displayname",
+ "altLabelField": "displayName",
+ "valueField": "GUID",
+ "queryKey": "ListAssignmentFilterTemplates"
+ }
+ }
+ ]
+ },
+ {
+ "name": "standards.MailboxRecipientLimits",
+ "cat": "Exchange Standards",
+ "tag": [],
+ "helpText": "Sets the maximum number of recipients that can be specified in the To, Cc, and Bcc fields of a message for all mailboxes in the tenant.",
+ "docsDescription": "This standard configures the recipient limits for all mailboxes in the tenant. The recipient limit determines the maximum number of recipients that can be specified in the To, Cc, and Bcc fields of a message. This helps prevent spam and manage email flow.",
+ "executiveText": "Controls how many recipients employees can include in a single email, helping prevent spam distribution and managing email server load. This security measure protects against both accidental mass mailings and potential abuse while ensuring legitimate business communications can still reach necessary recipients.",
+ "addedComponent": [
+ {
+ "type": "number",
+ "name": "standards.MailboxRecipientLimits.RecipientLimit",
+ "label": "Recipient Limit",
+ "defaultValue": 500
+ }
+ ],
+ "label": "Set Mailbox Recipient Limits",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-05-28",
+ "powershellEquivalent": "Set-Mailbox -RecipientLimits",
+ "recommendedBy": ["CIPP"]
+ },
+ {
+ "name": "standards.DisableExchangeOnlinePowerShell",
+ "cat": "Exchange Standards",
+ "tag": ["CIS M365 5.0 (6.1.1)", "Security", "NIST CSF 2.0 (PR.AA-05)"],
+ "helpText": "Disables Exchange Online PowerShell access for non-admin users by setting the RemotePowerShellEnabled property to false for each user. This helps prevent attackers from using PowerShell to run malicious commands, access file systems, registry, and distribute ransomware throughout networks. Users with admin roles are automatically excluded.",
+ "docsDescription": "Disables Exchange Online PowerShell access for non-admin users by setting the RemotePowerShellEnabled property to false for each user. This security measure follows a least privileged access approach, preventing potential attackers from using PowerShell to execute malicious commands, access sensitive systems, or distribute malware. Users with management roles containing 'Admin' are automatically excluded to ensure administrators retain PowerShell access to perform necessary management tasks.",
+ "executiveText": "Restricts PowerShell access to Exchange Online for regular employees while maintaining access for administrators, significantly reducing security risks from compromised accounts. This prevents attackers from using PowerShell to execute malicious commands or distribute ransomware while preserving necessary administrative capabilities.",
+ "label": "Disable Exchange Online PowerShell for non-admin users",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-06-19",
+ "powershellEquivalent": "Set-User -Identity $user -RemotePowerShellEnabled $false",
+ "recommendedBy": ["CIS", "CIPP"]
+ },
+ {
+ "name": "standards.OWAAttachmentRestrictions",
+ "cat": "Exchange Standards",
+ "tag": ["CIS M365 5.0 (6.1.2)", "Security", "NIST CSF 2.0 (PR.AA-05)"],
+ "helpText": "Restricts how users on unmanaged devices can interact with email attachments in Outlook on the web and new Outlook for Windows. Prevents downloading attachments or blocks viewing them entirely.",
+ "docsDescription": "This standard configures the OWA mailbox policy to restrict access to email attachments on unmanaged devices. Users can be prevented from downloading attachments (but can view/edit via Office Online) or blocked from seeing attachments entirely. This helps prevent data exfiltration through email attachments on devices not managed by the organization.",
+ "executiveText": "Restricts access to email attachments on personal or unmanaged devices while allowing full functionality on corporate-managed devices. This security measure prevents data theft through email attachments while maintaining productivity for employees using approved company devices.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "name": "standards.OWAAttachmentRestrictions.ConditionalAccessPolicy",
+ "label": "Attachment Restriction Policy",
+ "options": [
+ {
+ "label": "Read Only (View/Edit via Office Online, no download)",
+ "value": "ReadOnly"
+ },
+ {
+ "label": "Read Only Plus Attachments Blocked (Cannot see attachments)",
+ "value": "ReadOnlyPlusAttachmentsBlocked"
+ }
+ ],
+ "defaultValue": "ReadOnlyPlusAttachmentsBlocked"
+ }
+ ],
+ "label": "Restrict Email Attachments on Unmanaged Devices",
+ "impact": "Medium Impact",
+ "impactColour": "warning",
+ "addedDate": "2025-08-22",
+ "powershellEquivalent": "Set-OwaMailboxPolicy -Identity \"OwaMailboxPolicy-Default\" -ConditionalAccessPolicy ReadOnlyPlusAttachmentsBlocked",
+ "recommendedBy": ["Microsoft Zero Trust", "CIPP"]
+ },
+ {
+ "name": "standards.LegacyEmailReportAddins",
+ "cat": "Exchange Standards",
+ "tag": [],
+ "helpText": "Removes legacy Report Phishing and Report Message Outlook add-ins.",
+ "executiveText": "The legacy Report Phishing and Report Message Outlook add-ins are security issues with the add-in which makes them unsafe for the organization.",
+ "label": "Remove legacy Outlook Report add-ins",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-08-26",
+ "powershellEquivalent": "None",
+ "recommendedBy": ["Microsoft"]
+ },
+ {
+ "name": "standards.DeployCheckChromeExtension",
+ "cat": "Intune Standards",
+ "tag": [],
+ "helpText": "Deploys the Check Chrome extension via Intune OMA-URI custom policies for both Chrome and Edge browsers with configurable settings. Chrome ID: benimdeioplgkhanklclahllklceahbe, Edge ID: knepjpocdagponkonnbggpcnhnaikajg",
+ "docsDescription": "Creates Intune OMA-URI custom policies that automatically install and configure the Check Chrome extension on managed devices for both Google Chrome and Microsoft Edge browsers. This ensures the extension is deployed consistently across all corporate devices with customizable settings.",
+ "executiveText": "Automatically deploys the Check browser extension across all company devices with configurable security and branding settings, ensuring consistent security monitoring and compliance capabilities. This extension provides enhanced security features and monitoring tools that help protect against threats while maintaining user productivity.",
+ "addedComponent": [
+ {
+ "type": "switch",
+ "name": "standards.DeployCheckChromeExtension.enableValidPageBadge",
+ "label": "Enable valid page badge",
+ "defaultValue": true
+ },
+ {
+ "type": "switch",
+ "name": "standards.DeployCheckChromeExtension.enablePageBlocking",
+ "label": "Enable page blocking",
+ "defaultValue": true
+ },
+ {
+ "type": "switch",
+ "name": "standards.DeployCheckChromeExtension.enableCippReporting",
+ "label": "Enable CIPP reporting",
+ "defaultValue": true
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.cippServerUrl",
+ "label": "CIPP Server URL",
+ "placeholder": "https://YOUR-CIPP-SERVER-URL",
+ "required": false
+ },
+
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.customRulesUrl",
+ "label": "Custom Rules URL",
+ "placeholder": "https://YOUR-CIPP-SERVER-URL/rules.json",
+ "required": false
+ },
+ {
+ "type": "number",
+ "name": "standards.DeployCheckChromeExtension.updateInterval",
+ "label": "Update interval (hours)",
+ "defaultValue": 12
+ },
+ {
+ "type": "switch",
+ "name": "standards.DeployCheckChromeExtension.enableDebugLogging",
+ "label": "Enable debug logging",
+ "defaultValue": false
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.companyName",
+ "label": "Company Name",
+ "placeholder": "YOUR-COMPANY",
+ "required": false
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.productName",
+ "label": "Product Name",
+ "placeholder": "YOUR-PRODUCT-NAME",
+ "required": false
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.supportEmail",
+ "label": "Support Email",
+ "placeholder": "support@yourcompany.com",
+ "required": false
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.primaryColor",
+ "label": "Primary Color",
+ "placeholder": "#0044CC",
+ "required": false
+ },
+ {
+ "type": "textField",
+ "name": "standards.DeployCheckChromeExtension.logoUrl",
+ "label": "Logo URL",
+ "placeholder": "https://yourcompany.com/logo.png",
+ "required": false
+ },
+ {
+ "name": "AssignTo",
+ "label": "Who should this policy be assigned to?",
+ "type": "radio",
+ "options": [
+ {
+ "label": "Do not assign",
+ "value": "On"
+ },
+ {
+ "label": "Assign to all users",
+ "value": "allLicensedUsers"
+ },
+ {
+ "label": "Assign to all devices",
+ "value": "AllDevices"
+ },
+ {
+ "label": "Assign to all users and devices",
+ "value": "AllDevicesAndUsers"
+ },
+ {
+ "label": "Assign to Custom Group",
+ "value": "customGroup"
+ }
+ ]
+ },
+ {
+ "type": "textField",
+ "required": false,
+ "name": "customGroup",
+ "label": "Enter the custom group name if you selected 'Assign to Custom Group'. Wildcards are allowed."
+ }
+ ],
+ "label": "Deploy Check Chrome Extension",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2025-09-18",
+ "powershellEquivalent": "New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies'",
+ "recommendedBy": ["CIPP"]
}
]
diff --git a/src/data/timezoneList.json b/src/data/timezoneList.json
index 57238fe7eedc..d2501fca5bca 100644
--- a/src/data/timezoneList.json
+++ b/src/data/timezoneList.json
@@ -1,335 +1,446 @@
[
{
- "timezone": "(UTC-12:00) International Date Line West"
+ "timezone": "(UTC-12:00) International Date Line West",
+ "standardTime": "Dateline Standard Time"
},
{
- "timezone": "(UTC-11:00) Coordinated Universal Time-11"
+ "timezone": "(UTC-11:00) Coordinated Universal Time-11",
+ "standardTime": "UTC-11"
},
{
- "timezone": "(UTC-10:00) Hawaii"
+ "timezone": "(UTC-10:00) Hawaii",
+ "standardTime": "Hawaiian Standard Time"
},
{
- "timezone": "(UTC-09:00) Alaska"
+ "timezone": "(UTC-09:00) Alaska",
+ "standardTime": "Alaskan Standard Time"
},
{
- "timezone": "(UTC-08:00) Baja California"
+ "timezone": "(UTC-08:00) Baja California",
+ "standardTime": "Pacific Standard Time (Mexico)"
},
{
- "timezone": "(UTC-08:00) Pacific Time (US and Canada)"
+ "timezone": "(UTC-08:00) Pacific Time (US and Canada)",
+ "standardTime": "Pacific Standard Time"
},
{
- "timezone": "(UTC-07:00) Arizona"
+ "timezone": "(UTC-07:00) Arizona",
+ "standardTime": "US Mountain Standard Time"
},
{
- "timezone": "(UTC-07:00) Chihuahua, La Paz, Mazatlan"
+ "timezone": "(UTC-07:00) Chihuahua, La Paz, Mazatlan",
+ "standardTime": "Mountain Standard Time (Mexico)"
},
{
- "timezone": "(UTC-07:00) Mountain Time (US and Canada)"
+ "timezone": "(UTC-07:00) Mountain Time (US and Canada)",
+ "standardTime": "Mountain Standard Time"
},
{
- "timezone": "(UTC-06:00) Central America"
+ "timezone": "(UTC-06:00) Central America",
+ "standardTime": "Central America Standard Time"
},
{
- "timezone": "(UTC-06:00) Central Time (US and Canada)"
+ "timezone": "(UTC-06:00) Central Time (US and Canada)",
+ "standardTime": "Central Standard Time"
},
{
- "timezone": "(UTC-06:00) Guadalajara, Mexico City, Monterrey"
+ "timezone": "(UTC-06:00) Guadalajara, Mexico City, Monterrey",
+ "standardTime": "Central Standard Time (Mexico)"
},
{
- "timezone": "(UTC-06:00) Saskatchewan"
+ "timezone": "(UTC-06:00) Saskatchewan",
+ "standardTime": "Canada Central Standard Time"
},
{
- "timezone": "(UTC-05:00) Bogota, Lima, Quito"
+ "timezone": "(UTC-05:00) Bogota, Lima, Quito",
+ "standardTime": "SA Pacific Standard Time"
},
{
- "timezone": "(UTC-05:00) Eastern Time (US and Canada)"
+ "timezone": "(UTC-05:00) Eastern Time (US and Canada)",
+ "standardTime": "Eastern Standard Time"
},
{
- "timezone": "(UTC-05:00) Indiana (East)"
+ "timezone": "(UTC-05:00) Indiana (East)",
+ "standardTime": "US Eastern Standard Time"
},
{
- "timezone": "(UTC-04:30) Caracas"
+ "timezone": "(UTC-04:30) Caracas",
+ "standardTime": "Venezuela Standard Time"
},
{
- "timezone": "(UTC-04:00) Asuncion"
+ "timezone": "(UTC-04:00) Asuncion",
+ "standardTime": "Paraguay Standard Time"
},
{
- "timezone": "(UTC-04:00) Atlantic Time (Canada)"
+ "timezone": "(UTC-04:00) Atlantic Time (Canada)",
+ "standardTime": "Atlantic Standard Time"
},
{
- "timezone": "(UTC-04:00) Cuiaba"
+ "timezone": "(UTC-04:00) Cuiaba",
+ "standardTime": "Central Brazilian Standard Time"
},
{
- "timezone": "(UTC-04:00) Georgetown, La Paz, Manaus, San Juan"
+ "timezone": "(UTC-04:00) Georgetown, La Paz, Manaus, San Juan",
+ "standardTime": "SA Western Standard Time"
},
{
- "timezone": "(UTC-04:00) Santiago"
+ "timezone": "(UTC-04:00) Santiago",
+ "standardTime": "Pacific SA Standard Time"
},
{
- "timezone": "(UTC-03:30) Newfoundland"
+ "timezone": "(UTC-03:30) Newfoundland",
+ "standardTime": "Newfoundland Standard Time"
},
{
- "timezone": "(UTC-03:00) Brasilia"
+ "timezone": "(UTC-03:00) Brasilia",
+ "standardTime": "E. South America Standard Time"
},
{
- "timezone": "(UTC-03:00) Buenos Aires"
+ "timezone": "(UTC-03:00) Buenos Aires",
+ "standardTime": "Argentina Standard Time"
},
{
- "timezone": "(UTC-03:00) Cayenne, Fortaleza"
+ "timezone": "(UTC-03:00) Cayenne, Fortaleza",
+ "standardTime": "SA Eastern Standard Time"
},
{
- "timezone": "(UTC-03:00) Greenland"
+ "timezone": "(UTC-03:00) Greenland",
+ "standardTime": "Greenland Standard Time"
},
{
- "timezone": "(UTC-03:00) Montevideo"
+ "timezone": "(UTC-03:00) Montevideo",
+ "standardTime": "Montevideo Standard Time"
},
{
- "timezone": "(UTC-03:00) Salvador"
+ "timezone": "(UTC-03:00) Salvador",
+ "standardTime": "Bahia Standard Time"
},
{
- "timezone": "(UTC-02:00) Coordinated Universal Time-02"
+ "timezone": "(UTC-02:00) Coordinated Universal Time-02",
+ "standardTime": "UTC-02"
},
{
- "timezone": "(UTC-02:00) Mid-Atlantic"
+ "timezone": "(UTC-02:00) Mid-Atlantic",
+ "standardTime": "Mid-Atlantic Standard Time"
},
{
- "timezone": "(UTC-01:00) Azores"
+ "timezone": "(UTC-01:00) Azores",
+ "standardTime": "Azores Standard Time"
},
{
- "timezone": "(UTC-01:00) Cabo Verde"
+ "timezone": "(UTC-01:00) Cabo Verde",
+ "standardTime": "Cape Verde Standard Time"
},
{
- "timezone": "(UTC) Casablanca"
+ "timezone": "(UTC) Casablanca",
+ "standardTime": "Morocco Standard Time"
},
{
- "timezone": "(UTC) Coordinated Universal Time"
+ "timezone": "(UTC) Coordinated Universal Time",
+ "standardTime": "UTC"
},
{
- "timezone": "(UTC) Dublin, Edinburgh, Lisbon, London"
+ "timezone": "(UTC) Dublin, Edinburgh, Lisbon, London",
+ "standardTime": "GMT Standard Time"
},
{
- "timezone": "(UTC) Monrovia, Reykjavik"
+ "timezone": "(UTC) Monrovia, Reykjavik",
+ "standardTime": "Greenwich Standard Time"
},
{
- "timezone": "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"
+ "timezone": "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
+ "standardTime": "W. Europe Standard Time"
},
{
- "timezone": "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague"
+ "timezone": "(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague",
+ "standardTime": "Central Europe Standard Time"
},
{
- "timezone": "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris"
+ "timezone": "(UTC+01:00) Brussels, Copenhagen, Madrid, Paris",
+ "standardTime": "Romance Standard Time"
},
{
- "timezone": "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb"
+ "timezone": "(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb",
+ "standardTime": "Central European Standard Time"
},
{
- "timezone": "(UTC+01:00) West Central Africa"
+ "timezone": "(UTC+01:00) West Central Africa",
+ "standardTime": "W. Central Africa Standard Time"
},
{
- "timezone": "(UTC+01:00) Windhoek"
+ "timezone": "(UTC+01:00) Windhoek",
+ "standardTime": "Namibia Standard Time"
},
{
- "timezone": "(UTC+02:00) Amman"
+ "timezone": "(UTC+02:00) Amman",
+ "standardTime": "Jordan Standard Time"
},
{
- "timezone": "(UTC+02:00) Athens, Bucharest"
+ "timezone": "(UTC+02:00) Athens, Bucharest",
+ "standardTime": "GTB Standard Time"
},
{
- "timezone": "(UTC+02:00) Beirut"
+ "timezone": "(UTC+02:00) Beirut",
+ "standardTime": "Middle East Standard Time"
},
{
- "timezone": "(UTC+02:00) Cairo"
+ "timezone": "(UTC+02:00) Cairo",
+ "standardTime": "Egypt Standard Time"
},
{
- "timezone": "(UTC+02:00) Damascus"
+ "timezone": "(UTC+02:00) Damascus",
+ "standardTime": "Syria Standard Time"
},
{
- "timezone": "(UTC+02:00) Harare, Pretoria"
+ "timezone": "(UTC+02:00) Harare, Pretoria",
+ "standardTime": "South Africa Standard Time"
},
{
- "timezone": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"
+ "timezone": "(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
+ "standardTime": "FLE Standard Time"
},
{
- "timezone": "(UTC+02:00) Jerusalem"
+ "timezone": "(UTC+02:00) Jerusalem",
+ "standardTime": "Israel Standard Time"
},
{
- "timezone": "(UTC+02:00) Minsk (old)"
+ "timezone": "(UTC+02:00) Minsk (old)",
+ "standardTime": "Belarus Standard Time"
},
{
- "timezone": "(UTC+02:00) E. Europe"
+ "timezone": "(UTC+02:00) E. Europe",
+ "standardTime": "E. Europe Standard Time"
},
{
- "timezone": "(UTC+02:00) Kaliningrad"
+ "timezone": "(UTC+02:00) Kaliningrad",
+ "standardTime": "Kaliningrad Standard Time"
},
{
- "timezone": "(UTC+03:00) Baghdad"
+ "timezone": "(UTC+03:00) Baghdad",
+ "standardTime": "Arabic Standard Time"
},
{
- "timezone": "(UTC+03:00) Istanbul"
+ "timezone": "(UTC+03:00) Istanbul",
+ "standardTime": "Turkey Standard Time"
},
{
- "timezone": "(UTC+03:00) Kuwait, Riyadh"
+ "timezone": "(UTC+03:00) Kuwait, Riyadh",
+ "standardTime": "Arab Standard Time"
},
{
- "timezone": "(UTC+03:00) Minsk"
+ "timezone": "(UTC+03:00) Minsk",
+ "standardTime": "Belarus Standard Time"
},
{
- "timezone": "(UTC+03:00) Moscow, St. Petersburg, Volgograd"
+ "timezone": "(UTC+03:00) Moscow, St. Petersburg, Volgograd",
+ "standardTime": "Russian Standard Time"
},
{
- "timezone": "(UTC+03:00) Nairobi"
+ "timezone": "(UTC+03:00) Nairobi",
+ "standardTime": "E. Africa Standard Time"
},
{
- "timezone": "(UTC+03:30) Tehran"
+ "timezone": "(UTC+03:30) Tehran",
+ "standardTime": "Iran Standard Time"
},
{
- "timezone": "(UTC+04:00) Abu Dhabi, Muscat"
+ "timezone": "(UTC+04:00) Abu Dhabi, Muscat",
+ "standardTime": "Arabian Standard Time"
},
{
- "timezone": "(UTC+04:00) Astrakhan, Ulyanovsk"
+ "timezone": "(UTC+04:00) Astrakhan, Ulyanovsk",
+ "standardTime": "Astrakhan Standard Time"
},
{
- "timezone": "(UTC+04:00) Baku"
+ "timezone": "(UTC+04:00) Baku",
+ "standardTime": "Azerbaijan Standard Time"
},
{
- "timezone": "(UTC+04:00) Izhevsk, Samara"
+ "timezone": "(UTC+04:00) Izhevsk, Samara",
+ "standardTime": "Russia Time Zone 3"
},
{
- "timezone": "(UTC+04:00) Port Louis"
+ "timezone": "(UTC+04:00) Port Louis",
+ "standardTime": "Mauritius Standard Time"
},
{
- "timezone": "(UTC+04:00) Tbilisi"
+ "timezone": "(UTC+04:00) Tbilisi",
+ "standardTime": "Georgian Standard Time"
},
{
- "timezone": "(UTC+04:00) Yerevan"
+ "timezone": "(UTC+04:00) Yerevan",
+ "standardTime": "Caucasus Standard Time"
},
{
- "timezone": "(UTC+04:30) Kabul"
+ "timezone": "(UTC+04:30) Kabul",
+ "standardTime": "Afghanistan Standard Time"
},
{
- "timezone": "(UTC+05:00) Ekaterinburg"
+ "timezone": "(UTC+05:00) Ekaterinburg",
+ "standardTime": "Ekaterinburg Standard Time"
},
{
- "timezone": "(UTC+05:00) Islamabad, Karachi"
+ "timezone": "(UTC+05:00) Islamabad, Karachi",
+ "standardTime": "Pakistan Standard Time"
},
{
- "timezone": "(UTC+05:00) Tashkent"
+ "timezone": "(UTC+05:00) Tashkent",
+ "standardTime": "West Asia Standard Time"
},
{
- "timezone": "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi"
+ "timezone": "(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi",
+ "standardTime": "India Standard Time"
},
{
- "timezone": "(UTC+05:30) Sri Jayawardenepura"
+ "timezone": "(UTC+05:30) Sri Jayawardenepura",
+ "standardTime": "Sri Lanka Standard Time"
},
{
- "timezone": "(UTC+05:45) Kathmandu"
+ "timezone": "(UTC+05:45) Kathmandu",
+ "standardTime": "Nepal Standard Time"
},
{
- "timezone": "(UTC+06:00) Astana"
+ "timezone": "(UTC+06:00) Astana",
+ "standardTime": "Qyzylorda Standard Time"
},
{
- "timezone": "(UTC+06:00) Dhaka"
+ "timezone": "(UTC+06:00) Dhaka",
+ "standardTime": "Bangladesh Standard Time"
},
{
- "timezone": "(UTC+06:00) Omsk"
+ "timezone": "(UTC+06:00) Omsk",
+ "standardTime": "Omsk Standard Time"
},
{
- "timezone": "(UTC+06:30) Yangon (Rangoon)"
+ "timezone": "(UTC+06:30) Yangon (Rangoon)",
+ "standardTime": "Myanmar Standard Time"
},
{
- "timezone": "(UTC+07:00) Bangkok, Hanoi, Jakarta"
+ "timezone": "(UTC+07:00) Bangkok, Hanoi, Jakarta",
+ "standardTime": "SE Asia Standard Time"
},
{
- "timezone": "(UTC+07:00) Barnaul, Gorno-Altaysk"
+ "timezone": "(UTC+07:00) Barnaul, Gorno-Altaysk",
+ "standardTime": "Altai Standard Time"
},
{
- "timezone": "(UTC+07:00) Krasnoyarsk"
+ "timezone": "(UTC+07:00) Krasnoyarsk",
+ "standardTime": "North Asia Standard Time"
},
{
- "timezone": "(UTC+07:00) Novosibirsk"
+ "timezone": "(UTC+07:00) Novosibirsk",
+ "standardTime": "N. Central Asia Standard Time"
},
{
- "timezone": "(UTC+07:00) Tomsk"
+ "timezone": "(UTC+07:00) Tomsk",
+ "standardTime": "Tomsk Standard Time"
},
{
- "timezone": "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi"
+ "timezone": "(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi",
+ "standardTime": "China Standard Time"
},
{
- "timezone": "(UTC+08:00) Irkutsk"
+ "timezone": "(UTC+08:00) Irkutsk",
+ "standardTime": "North Asia East Standard Time"
},
{
- "timezone": "(UTC+08:00) Kuala Lumpur, Singapore"
+ "timezone": "(UTC+08:00) Kuala Lumpur, Singapore",
+ "standardTime": "Singapore Standard Time"
},
{
- "timezone": "(UTC+08:00) Perth"
+ "timezone": "(UTC+08:00) Perth",
+ "standardTime": "W. Australia Standard Time"
},
{
- "timezone": "(UTC+08:00) Taipei"
+ "timezone": "(UTC+08:00) Taipei",
+ "standardTime": "Taipei Standard Time"
},
{
- "timezone": "(UTC+08:00) Ulaanbaatar"
+ "timezone": "(UTC+08:00) Ulaanbaatar",
+ "standardTime": "Ulaanbaatar Standard Time"
},
{
- "timezone": "(UTC+09:00) Osaka, Sapporo, Tokyo"
+ "timezone": "(UTC+09:00) Osaka, Sapporo, Tokyo",
+ "standardTime": "Tokyo Standard Time"
},
{
- "timezone": "(UTC+09:00) Seoul"
+ "timezone": "(UTC+09:00) Seoul",
+ "standardTime": "Korea Standard Time"
},
{
- "timezone": "(UTC+09:00) Yakutsk"
+ "timezone": "(UTC+09:00) Yakutsk",
+ "standardTime": "Yakutsk Standard Time"
},
{
- "timezone": "(UTC+09:30) Adelaide"
+ "timezone": "(UTC+09:30) Adelaide",
+ "standardTime": "Cen. Australia Standard Time"
},
{
- "timezone": "(UTC+09:30) Darwin"
+ "timezone": "(UTC+09:30) Darwin",
+ "standardTime": "AUS Central Standard Time"
},
{
- "timezone": "(UTC+10:00) Brisbane"
+ "timezone": "(UTC+10:00) Brisbane",
+ "standardTime": "E. Australia Standard Time"
},
{
- "timezone": "(UTC+10:00) Canberra, Melbourne, Sydney"
+ "timezone": "(UTC+10:00) Canberra, Melbourne, Sydney",
+ "standardTime": "AUS Eastern Standard Time"
},
{
- "timezone": "(UTC+10:00) Guam, Port Moresby"
+ "timezone": "(UTC+10:00) Guam, Port Moresby",
+ "standardTime": "West Pacific Standard Time"
},
{
- "timezone": "(UTC+10:00) Hobart"
+ "timezone": "(UTC+10:00) Hobart",
+ "standardTime": "Tasmania Standard Time"
},
{
- "timezone": "(UTC+10:00) Magadan"
+ "timezone": "(UTC+10:00) Magadan",
+ "standardTime": "Magadan Standard Time"
},
{
- "timezone": "(UTC+10:00) Vladivostok"
+ "timezone": "(UTC+10:00) Vladivostok",
+ "standardTime": "Vladivostok Standard Time"
},
{
- "timezone": "(UTC+11:00) Chokurdakh"
+ "timezone": "(UTC+11:00) Chokurdakh",
+ "standardTime": "Russia Time Zone 10"
},
{
- "timezone": "(UTC+11:00) Sakhalin"
+ "timezone": "(UTC+11:00) Sakhalin",
+ "standardTime": "Sakhalin Standard Time"
},
{
- "timezone": "(UTC+11:00) Solomon Is., New Caledonia"
+ "timezone": "(UTC+11:00) Solomon Is., New Caledonia",
+ "standardTime": "Central Pacific Standard Time"
},
{
- "timezone": "(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky"
+ "timezone": "(UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky",
+ "standardTime": "Russia Time Zone 11"
},
{
- "timezone": "(UTC+12:00) Auckland, Wellington"
+ "timezone": "(UTC+12:00) Auckland, Wellington",
+ "standardTime": "New Zealand Standard Time"
},
{
- "timezone": "(UTC+12:00) Coordinated Universal Time+12"
+ "timezone": "(UTC+12:00) Coordinated Universal Time+12",
+ "standardTime": "UTC+12"
},
{
- "timezone": "(UTC+12:00) Fiji"
+ "timezone": "(UTC+12:00) Fiji",
+ "standardTime": "Fiji Standard Time"
},
{
- "timezone": "(UTC+12:00) Petropavlovsk-Kamchatsky - Old"
+ "timezone": "(UTC+12:00) Petropavlovsk-Kamchatsky - Old",
+ "standardTime": "Kamchatka Standard Time"
},
{
- "timezone": "(UTC+13:00) Nuku'alofa"
+ "timezone": "(UTC+13:00) Nuku'alofa",
+ "standardTime": "Tonga Standard Time"
},
{
- "timezone": "(UTC+13:00) Samoa"
+ "timezone": "(UTC+13:00) Samoa",
+ "standardTime": "Samoa Standard Time"
}
]
diff --git a/src/hooks/use-guid-resolver.js b/src/hooks/use-guid-resolver.js
new file mode 100644
index 000000000000..1325722fc872
--- /dev/null
+++ b/src/hooks/use-guid-resolver.js
@@ -0,0 +1,523 @@
+import { useState, useCallback, useRef, useEffect } from "react";
+import { ApiPostCall } from "/src/api/ApiCall";
+import { useSettings } from "/src/hooks/use-settings";
+
+// Function to check if a string is a GUID
+const isGuid = (str) => {
+ if (typeof str !== "string") return false;
+ const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ return guidRegex.test(str);
+};
+
+// Function to extract GUIDs from strings (including embedded GUIDs)
+const extractGuidsFromString = (str) => {
+ if (typeof str !== "string") return [];
+ const guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
+ return str.match(guidRegex) || [];
+};
+
+// Function to extract object IDs from partner tenant UPNs (user_@.onmicrosoft.com)
+// Also handles format: TenantName.onmicrosoft.com\tenant: , object:
+const extractObjectIdFromPartnerUPN = (str) => {
+ if (typeof str !== "string") return [];
+ const matches = [];
+
+ // Format 1: user_@.onmicrosoft.com
+ const partnerUpnRegex = /user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)/gi;
+ let match;
+
+ while ((match = partnerUpnRegex.exec(str)) !== null) {
+ // Convert the 32-character hex string to GUID format
+ const hexId = match[1];
+ const tenantDomain = match[2];
+ if (hexId.length === 32) {
+ const guid = [
+ hexId.slice(0, 8),
+ hexId.slice(8, 12),
+ hexId.slice(12, 16),
+ hexId.slice(16, 20),
+ hexId.slice(20, 32),
+ ].join("-");
+ matches.push({ guid, tenantDomain });
+ }
+ }
+
+ // Format 2: TenantName.onmicrosoft.com\tenant: , object:
+ // For exchange format, use the partner tenant guid for resolution
+ const partnerTenantObjectRegex =
+ /([^\\]+\.onmicrosoft\.com)\\tenant:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}),\s*object:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi;
+
+ while ((match = partnerTenantObjectRegex.exec(str)) !== null) {
+ const customerTenantDomain = match[1]; // This is the customer tenant domain
+ const partnerTenantGuid = match[2]; // This is the partner tenant guid - use this for resolution
+ const objectGuid = match[3]; // This is the object to resolve
+
+ // Use the partner tenant GUID for resolution
+ matches.push({ guid: objectGuid, tenantDomain: partnerTenantGuid });
+ }
+
+ return matches;
+};
+
+// Function to recursively scan an object for GUIDs
+const findGuids = (obj, guidsSet = new Set(), partnerGuidsMap = new Map()) => {
+ if (!obj) return { guidsSet, partnerGuidsMap };
+
+ if (typeof obj === "string") {
+ // First, extract object IDs from partner tenant UPNs to track which GUIDs belong to partners
+ const partnerObjectIds = extractObjectIdFromPartnerUPN(obj);
+ const partnerGuids = new Set();
+
+ partnerObjectIds.forEach(({ guid, tenantDomain }) => {
+ if (!partnerGuidsMap.has(tenantDomain)) {
+ partnerGuidsMap.set(tenantDomain, new Set());
+ }
+ partnerGuidsMap.get(tenantDomain).add(guid);
+ partnerGuids.add(guid); // Track this GUID as belonging to a partner
+ });
+
+ // Check if the entire string is a GUID
+ if (isGuid(obj)) {
+ // Only add to main guidsSet if it's not a partner GUID
+ if (!partnerGuids.has(obj)) {
+ guidsSet.add(obj);
+ }
+ } else {
+ // Extract GUIDs embedded within longer strings
+ const embeddedGuids = extractGuidsFromString(obj);
+ embeddedGuids.forEach((guid) => {
+ // Only add to main guidsSet if it's not a partner GUID
+ if (!partnerGuids.has(guid)) {
+ guidsSet.add(guid);
+ }
+ });
+ }
+ } else if (Array.isArray(obj)) {
+ obj.forEach((item) => {
+ const result = findGuids(item, guidsSet, partnerGuidsMap);
+ guidsSet = result.guidsSet;
+ partnerGuidsMap = result.partnerGuidsMap;
+ });
+ } else if (typeof obj === "object") {
+ Object.values(obj).forEach((value) => {
+ const result = findGuids(value, guidsSet, partnerGuidsMap);
+ guidsSet = result.guidsSet;
+ partnerGuidsMap = result.partnerGuidsMap;
+ });
+ }
+
+ return { guidsSet, partnerGuidsMap };
+};
+
+// Helper function to replace GUIDs and special UPNs in a string with resolved names
+const replaceGuidsAndUpnsInString = (str, guidMapping, upnMapping, isLoadingGuids) => {
+ if (typeof str !== "string") return { result: str, hasResolvedNames: false };
+
+ let result = str;
+ let hasResolvedNames = false;
+
+ // Replace standard GUIDs
+ const guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
+ const guidsInString = str.match(guidRegex) || [];
+
+ guidsInString.forEach((guid) => {
+ if (guidMapping[guid]) {
+ result = result.replace(new RegExp(guid, "gi"), guidMapping[guid]);
+ hasResolvedNames = true;
+ }
+ });
+
+ // Replace partner UPNs (user_@partnertenant.onmicrosoft.com)
+ const partnerUpnRegex = /user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)/gi;
+ let match;
+
+ // We need to clone the string to reset the regex lastIndex
+ const strForMatching = String(str);
+
+ while ((match = partnerUpnRegex.exec(strForMatching)) !== null) {
+ const fullMatch = match[0]; // The complete UPN
+ const hexId = match[1];
+
+ if (hexId.length === 32) {
+ const guid = [
+ hexId.slice(0, 8),
+ hexId.slice(8, 12),
+ hexId.slice(12, 16),
+ hexId.slice(16, 20),
+ hexId.slice(20, 32),
+ ].join("-");
+
+ // For partner UPN format, use the actual UPN if available, otherwise fall back to display name
+ if (upnMapping[guid]) {
+ result = result.replace(
+ new RegExp(fullMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
+ upnMapping[guid]
+ );
+ hasResolvedNames = true;
+ } else if (guidMapping[guid]) {
+ result = result.replace(
+ new RegExp(fullMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
+ guidMapping[guid]
+ );
+ hasResolvedNames = true;
+ }
+ }
+ }
+
+ return { result, hasResolvedNames };
+};
+
+export const useGuidResolver = (manualTenant = null) => {
+ const tenantFilter = useSettings().currentTenant;
+ const activeTenant = manualTenant || tenantFilter;
+
+ // GUID resolution state
+ const [guidMapping, setGuidMapping] = useState({});
+ const [upnMapping, setUpnMapping] = useState({}); // New mapping specifically for UPNs
+ const [isLoadingGuids, setIsLoadingGuids] = useState(false);
+
+ // Use refs for values that shouldn't trigger re-renders but need to persist
+ const notFoundGuidsRef = useRef(new Set());
+ const pendingGuidsRef = useRef([]);
+ const pendingPartnerGuidsRef = useRef(new Map()); // Map of tenantDomain -> Set of GUIDs
+ const lastRequestTimeRef = useRef(0);
+ const lastPartnerRequestTimeRef = useRef(0); // Separate timing for partner tenant calls
+ const rateLimitBackoffRef = useRef(2000); // Default backoff time in milliseconds
+ const rateLimitTimeoutRef = useRef(null); // For tracking retry timeouts
+
+ // Helper function to retry API call with the correct backoff
+ const retryApiCallWithBackoff = useCallback((apiCall, url, data, retryDelay = null) => {
+ // Clear any existing timeout
+ if (rateLimitTimeoutRef.current) {
+ clearTimeout(rateLimitTimeoutRef.current);
+ }
+
+ // Use specified delay or current backoff time
+ const delay = retryDelay || rateLimitBackoffRef.current;
+
+ // Set timeout to retry
+ rateLimitTimeoutRef.current = setTimeout(() => {
+ apiCall.mutate({ url, data });
+ rateLimitTimeoutRef.current = null;
+ }, delay);
+
+ // Increase backoff for future retries (up to a reasonable limit)
+ rateLimitBackoffRef.current = Math.min(rateLimitBackoffRef.current * 1.5, 10000);
+ }, []);
+
+ // Setup API call for directory objects resolution
+ const directoryObjectsMutation = ApiPostCall({
+ relatedQueryKeys: ["directoryObjects"],
+ onResult: (data) => {
+ // Handle rate limit error
+ if (data && data.statusCode === 429) {
+ console.log("Rate limit hit on directory objects lookup, retrying...");
+
+ // Extract retry time from message if available
+ let retryAfterSeconds = 2;
+ if (data.message && typeof data.message === "string") {
+ const match = data.message.match(/Try again in (\d+) seconds/i);
+ if (match && match[1]) {
+ retryAfterSeconds = parseInt(match[1], 10) || 2;
+ }
+ }
+
+ // Retry with the specified delay (convert to milliseconds)
+ retryApiCallWithBackoff(
+ directoryObjectsMutation,
+ "/api/ListDirectoryObjects",
+ {
+ tenantFilter: activeTenant,
+ ids: pendingGuidsRef.current,
+ $select: "id,displayName,userPrincipalName,mail",
+ },
+ retryAfterSeconds * 1000
+ );
+ return;
+ }
+
+ // Reset backoff time on successful request
+ rateLimitBackoffRef.current = 2000;
+
+ if (data && Array.isArray(data.value)) {
+ const newDisplayMapping = {};
+ const newUpnMapping = {};
+
+ // Process the returned results
+ data.value.forEach((item) => {
+ if (item.id) {
+ // For display purposes, prefer displayName > userPrincipalName > mail
+ if (item.displayName || item.userPrincipalName || item.mail) {
+ newDisplayMapping[item.id] = item.displayName || item.userPrincipalName || item.mail;
+ }
+
+ // For UPN replacement, specifically store the UPN when available
+ if (item.userPrincipalName) {
+ newUpnMapping[item.id] = item.userPrincipalName;
+ }
+ }
+ });
+
+ // Find GUIDs that were sent but not returned in the response
+ const processedGuids = new Set(pendingGuidsRef.current);
+ const returnedGuids = new Set(data.value.map((item) => item.id));
+ const notReturned = [...processedGuids].filter((guid) => !returnedGuids.has(guid));
+
+ // Add unresolved GUIDs to partner tenant fallback lookup
+ if (notReturned.length > 0) {
+ console.log(
+ `${notReturned.length} GUIDs not resolved by primary tenant, trying partner tenant lookup`
+ );
+
+ // Add to partner lookup with the current tenant as fallback
+ if (!pendingPartnerGuidsRef.current.has(activeTenant)) {
+ pendingPartnerGuidsRef.current.set(activeTenant, new Set());
+ }
+ notReturned.forEach((guid) => {
+ pendingPartnerGuidsRef.current.get(activeTenant).add(guid);
+ });
+
+ // Trigger partner lookup immediately for fallback
+ const now = Date.now();
+ if (!rateLimitTimeoutRef.current && now - lastPartnerRequestTimeRef.current >= 2000) {
+ lastPartnerRequestTimeRef.current = now;
+
+ // Use partner tenant API for unresolved GUIDs
+ console.log(
+ `Sending partner fallback request for ${notReturned.length} GUIDs in tenant ${activeTenant}`
+ );
+ partnerDirectoryObjectsMutation.mutate({
+ url: "/api/ListDirectoryObjects",
+ data: {
+ tenantFilter: activeTenant,
+ ids: notReturned,
+ $select: "id,displayName,userPrincipalName,mail",
+ partnerLookup: true, // Flag to indicate this is a partner lookup
+ },
+ });
+ }
+ }
+
+ setGuidMapping((prevMapping) => ({ ...prevMapping, ...newDisplayMapping }));
+ setUpnMapping((prevMapping) => ({ ...prevMapping, ...newUpnMapping }));
+ pendingGuidsRef.current = [];
+
+ // Only set loading to false if we don't have pending partner lookups
+ if (notReturned.length === 0) {
+ setIsLoadingGuids(false);
+ }
+ }
+ },
+ });
+
+ // Setup API call for partner tenant directory objects resolution
+ const partnerDirectoryObjectsMutation = ApiPostCall({
+ relatedQueryKeys: ["partnerDirectoryObjects"],
+ onResult: (data) => {
+ // Handle rate limit error
+ if (data && data.statusCode === 429) {
+ console.log("Rate limit hit on partner directory objects lookup, retrying...");
+
+ // Extract retry time from message if available
+ let retryAfterSeconds = 2;
+ if (data.message && typeof data.message === "string") {
+ const match = data.message.match(/Try again in (\d+) seconds/i);
+ if (match && match[1]) {
+ retryAfterSeconds = parseInt(match[1], 10) || 2;
+ }
+ }
+
+ // We need to preserve the current tenant domain for retry
+ const currentTenantEntries = [...pendingPartnerGuidsRef.current.entries()];
+
+ if (currentTenantEntries.length > 0) {
+ const [tenantDomain, guidsSet] = currentTenantEntries[0];
+ const guidsToRetry = Array.from(guidsSet);
+
+ // Retry with the specified delay (convert to milliseconds)
+ retryApiCallWithBackoff(
+ partnerDirectoryObjectsMutation,
+ "/api/ListDirectoryObjects",
+ {
+ tenantFilter: tenantDomain,
+ ids: guidsToRetry,
+ $select: "id,displayName,userPrincipalName,mail",
+ },
+ retryAfterSeconds * 1000
+ );
+ }
+ return;
+ }
+
+ // Reset backoff time on successful request
+ rateLimitBackoffRef.current = 2000;
+
+ if (data && Array.isArray(data.value)) {
+ const newDisplayMapping = {};
+ const newUpnMapping = {};
+
+ // Process the returned results
+ data.value.forEach((item) => {
+ if (item.id) {
+ // For display purposes, prefer userPrincipalName > mail > DisplayName
+ if (item.userPrincipalName || item.mail || item.displayName) {
+ newDisplayMapping[item.id] = item.userPrincipalName || item.mail || item.displayName;
+ }
+
+ // For UPN replacement, specifically store the UPN when available
+ if (item.userPrincipalName) {
+ newUpnMapping[item.id] = item.userPrincipalName;
+ }
+ }
+ });
+
+ // Find GUIDs that were sent but not returned in the partner lookup
+ const allPendingPartnerGuids = new Set();
+ pendingPartnerGuidsRef.current.forEach((guidsSet) => {
+ guidsSet.forEach((guid) => allPendingPartnerGuids.add(guid));
+ });
+
+ const returnedGuids = new Set(data.value.map((item) => item.id));
+ const stillNotFound = [...allPendingPartnerGuids].filter(
+ (guid) => !returnedGuids.has(guid)
+ );
+
+ // Add truly unresolved GUIDs to notFoundGuids
+ if (stillNotFound.length > 0) {
+ stillNotFound.forEach((guid) => notFoundGuidsRef.current.add(guid));
+ }
+
+ setGuidMapping((prevMapping) => ({ ...prevMapping, ...newDisplayMapping }));
+ setUpnMapping((prevMapping) => ({ ...prevMapping, ...newUpnMapping }));
+
+ // Clear processed partner GUIDs
+ pendingPartnerGuidsRef.current = new Map();
+ setIsLoadingGuids(false);
+ }
+ },
+ }); // Function to handle resolving GUIDs
+ const resolveGuids = useCallback(
+ (objectToScan) => {
+ const { guidsSet, partnerGuidsMap } = findGuids(objectToScan);
+
+ // Handle regular GUIDs (current tenant) - these should NOT include partner tenant GUIDs
+ if (guidsSet.size > 0) {
+ const guidsArray = Array.from(guidsSet);
+ const notResolvedGuids = guidsArray.filter(
+ (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid)
+ );
+
+ if (notResolvedGuids.length > 0) {
+ // Merge new GUIDs with existing pending GUIDs without duplicates
+ const allPendingGuids = [...new Set([...pendingGuidsRef.current, ...notResolvedGuids])];
+ pendingGuidsRef.current = allPendingGuids;
+ setIsLoadingGuids(true);
+
+ // Make API call for primary tenant GUIDs
+ const now = Date.now();
+ if (!rateLimitTimeoutRef.current && now - lastRequestTimeRef.current >= 2000) {
+ lastRequestTimeRef.current = now;
+
+ // Only send a maximum of 1000 GUIDs per request
+ const batchSize = 1000;
+ const guidsToSend = allPendingGuids.slice(0, batchSize);
+
+ if (guidsToSend.length > 0) {
+ console.log(
+ `Sending primary tenant request for ${guidsToSend.length} GUIDs in tenant ${activeTenant}`
+ );
+ directoryObjectsMutation.mutate({
+ url: "/api/ListDirectoryObjects",
+ data: {
+ tenantFilter: activeTenant,
+ ids: guidsToSend,
+ $select: "id,displayName,userPrincipalName,mail",
+ },
+ });
+ } else {
+ setIsLoadingGuids(false);
+ }
+ }
+ }
+ }
+
+ // Handle partner tenant GUIDs separately
+ if (partnerGuidsMap.size > 0) {
+ partnerGuidsMap.forEach((guids, tenantDomain) => {
+ const guidsArray = Array.from(guids);
+ const notResolvedGuids = guidsArray.filter(
+ (guid) => !guidMapping[guid] && !notFoundGuidsRef.current.has(guid)
+ );
+
+ if (notResolvedGuids.length > 0) {
+ // Store pending partner GUIDs
+ if (!pendingPartnerGuidsRef.current.has(tenantDomain)) {
+ pendingPartnerGuidsRef.current.set(tenantDomain, new Set());
+ }
+ notResolvedGuids.forEach((guid) =>
+ pendingPartnerGuidsRef.current.get(tenantDomain).add(guid)
+ );
+
+ setIsLoadingGuids(true);
+
+ // Make API call for partner tenant - with separate timing from primary tenant
+ const now = Date.now();
+ if (!rateLimitTimeoutRef.current && now - lastPartnerRequestTimeRef.current >= 2000) {
+ lastPartnerRequestTimeRef.current = now;
+
+ // Only send a maximum of 1000 GUIDs per request
+ const batchSize = 1000;
+ const guidsToSend = notResolvedGuids.slice(0, batchSize);
+
+ if (guidsToSend.length > 0) {
+ console.log(
+ `Sending partner tenant request for ${guidsToSend.length} GUIDs in tenant ${tenantDomain}`
+ );
+ partnerDirectoryObjectsMutation.mutate({
+ url: "/api/ListDirectoryObjects",
+ data: {
+ tenantFilter: tenantDomain,
+ ids: guidsToSend,
+ $select: "id,displayName,userPrincipalName,mail",
+ },
+ });
+ }
+ }
+ }
+ });
+ }
+
+ // If no GUIDs to process, ensure loading state is false
+ if (guidsSet.size === 0 && partnerGuidsMap.size === 0) {
+ setIsLoadingGuids(false);
+ }
+ },
+ [guidMapping, activeTenant, directoryObjectsMutation, partnerDirectoryObjectsMutation]
+ );
+
+ // Create a memoized version of the string replacement function
+ const replaceGuidsAndUpnsInStringMemoized = useCallback(
+ (str) => replaceGuidsAndUpnsInString(str, guidMapping, upnMapping, isLoadingGuids),
+ [guidMapping, upnMapping, isLoadingGuids]
+ );
+
+ // Cleanup function to clear any pending timeouts when the component unmounts
+ useEffect(() => {
+ return () => {
+ if (rateLimitTimeoutRef.current) {
+ clearTimeout(rateLimitTimeoutRef.current);
+ rateLimitTimeoutRef.current = null;
+ }
+ };
+ }, []);
+
+ return {
+ guidMapping,
+ upnMapping,
+ isLoadingGuids,
+ resolveGuids,
+ isGuid,
+ extractObjectIdFromPartnerUPN,
+ replaceGuidsAndUpnsInString: replaceGuidsAndUpnsInStringMemoized,
+ };
+};
diff --git a/src/hooks/use-page-view.js b/src/hooks/use-page-view.js
index c4365337c92a..fe5923c47679 100644
--- a/src/hooks/use-page-view.js
+++ b/src/hooks/use-page-view.js
@@ -1,3 +1 @@
-import { useEffect } from "react";
-
export const usePageView = () => {};
diff --git a/src/hooks/use-permissions.js b/src/hooks/use-permissions.js
new file mode 100644
index 000000000000..6b7973c164f8
--- /dev/null
+++ b/src/hooks/use-permissions.js
@@ -0,0 +1,96 @@
+import { useCallback } from "react";
+import { ApiGetCall } from "/src/api/ApiCall";
+import { hasAccess, hasPermission, hasRole } from "/src/utils/permissions";
+
+/**
+ * Hook for checking user permissions and roles
+ * Integrates with the existing CIPP authentication system
+ */
+export const usePermissions = () => {
+ const currentRole = ApiGetCall({
+ url: "/api/me",
+ queryKey: "authmecipp",
+ });
+
+ const userRoles = currentRole.data?.clientPrincipal?.userRoles || [];
+ const userPermissions = currentRole.data?.permissions || [];
+ const isLoading = currentRole.isLoading;
+ const isAuthenticated = currentRole.isSuccess && userRoles.length > 0;
+
+ /**
+ * Check if user has specific permissions
+ * @param {string[]} requiredPermissions - Array of required permissions (supports wildcards)
+ * @returns {boolean} - True if user has at least one of the required permissions
+ */
+ const checkPermissions = useCallback(
+ (requiredPermissions) => {
+ if (!isAuthenticated) return false;
+ return hasPermission(userPermissions, requiredPermissions);
+ },
+ [userPermissions, isAuthenticated]
+ );
+
+ /**
+ * Check if user has specific roles
+ * @param {string[]} requiredRoles - Array of required roles
+ * @returns {boolean} - True if user has at least one of the required roles
+ */
+ const checkRoles = useCallback(
+ (requiredRoles) => {
+ if (!isAuthenticated) return false;
+ return hasRole(userRoles, requiredRoles);
+ },
+ [userRoles, isAuthenticated]
+ );
+
+ /**
+ * Check if user has access based on both permissions and roles
+ * @param {Object} config - Configuration object
+ * @param {string[]} config.requiredPermissions - Array of required permissions
+ * @param {string[]} config.requiredRoles - Array of required roles
+ * @returns {boolean} - True if user has access
+ */
+ const checkAccess = useCallback(
+ (config = {}) => {
+ if (!isAuthenticated) return false;
+
+ const { requiredPermissions = [], requiredRoles = [] } = config;
+
+ return hasAccess({
+ userPermissions,
+ userRoles,
+ requiredPermissions,
+ requiredRoles,
+ });
+ },
+ [userPermissions, userRoles, isAuthenticated]
+ );
+
+ return {
+ userPermissions,
+ userRoles,
+ isLoading,
+ isAuthenticated,
+ checkPermissions,
+ checkRoles,
+ checkAccess,
+ };
+};
+
+/**
+ * Hook specifically for checking permissions with a simpler API
+ * @param {string[]} requiredPermissions - Array of required permissions
+ * @param {string[]} requiredRoles - Array of required roles
+ * @returns {Object} - Object containing hasAccess boolean and loading state
+ */
+export const useHasPermission = (requiredPermissions = [], requiredRoles = []) => {
+ const { checkAccess, isLoading, isAuthenticated } = usePermissions();
+
+ const hasAccess = checkAccess({ requiredPermissions, requiredRoles });
+
+ return {
+ hasAccess,
+ isLoading,
+ isAuthenticated,
+ };
+};
diff --git a/src/hooks/use-securescore.js b/src/hooks/use-securescore.js
index 2394fa98ed0a..f96c2bd232b7 100644
--- a/src/hooks/use-securescore.js
+++ b/src/hooks/use-securescore.js
@@ -3,7 +3,7 @@ import { ApiGetCall } from "../api/ApiCall";
import { useSettings } from "./use-settings";
import standards from "/src/data/standards.json";
-export function useSecureScore() {
+export function useSecureScore({ waiting = true } = {}) {
const currentTenant = useSettings().currentTenant;
if (currentTenant === "AllTenants") {
return {
@@ -27,6 +27,7 @@ export function useSecureScore() {
$top: 999,
},
queryKey: `controlScore-${currentTenant}`,
+ waiting: waiting,
});
const secureScore = ApiGetCall({
@@ -39,6 +40,7 @@ export function useSecureScore() {
$top: 7,
},
queryKey: `secureScore-${currentTenant}`,
+ waiting: waiting,
});
useEffect(() => {
@@ -66,7 +68,7 @@ export function useSecureScore() {
complianceInformation: translation?.complianceInformation,
actionUrl: remediation
? //this needs to be updated to be a direct url to apply this standard.
- "/tenant/standards/list-applied-standards"
+ "/tenant/standards/list-standards"
: translation?.actionUrl,
remediation: remediation
? `1. Enable the CIPP Standard: ${remediation.label}`
@@ -89,12 +91,10 @@ export function useSecureScore() {
(secureScoreData.currentScore / secureScoreData.maxScore) * 100
),
percentageVsAllTenants: Math.round(
- (secureScoreData.averageComparativeScores?.[0]?.averageScore / secureScoreData.maxScore) *
- 100
+ secureScoreData.averageComparativeScores?.[0]?.averageScore
),
percentageVsSimilar: Math.round(
- (secureScoreData.averageComparativeScores?.[1]?.averageScore / secureScoreData.maxScore) *
- 100
+ secureScoreData.averageComparativeScores?.[1]?.averageScore
),
controlScores: updatedControlScores,
});
diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx
index 1f2b77713237..9594443127bc 100644
--- a/src/layouts/TabbedLayout.jsx
+++ b/src/layouts/TabbedLayout.jsx
@@ -1,5 +1,5 @@
import { usePathname, useRouter } from "next/navigation";
-import { Box, Container, Divider, Stack, Tab, Tabs, Typography } from "@mui/material";
+import { Box, Divider, Stack, Tab, Tabs } from "@mui/material";
export const TabbedLayout = (props) => {
const { tabOptions, children } = props;
diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js
index ab6b9a11155b..579780b6adc8 100644
--- a/src/layouts/account-popover.js
+++ b/src/layouts/account-popover.js
@@ -9,6 +9,7 @@ import SunIcon from "@heroicons/react/24/outline/SunIcon";
import {
Avatar,
Box,
+ CircularProgress,
List,
ListItem,
ListItemButton,
@@ -23,7 +24,9 @@ import {
import { usePopover } from "../hooks/use-popover";
import { paths } from "../paths";
import { ApiGetCall } from "../api/ApiCall";
-import { CogIcon } from "@heroicons/react/24/outline";
+import { CogIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
+import { useReleaseNotes } from "../contexts/release-notes-context";
+import { useQueryClient } from "@tanstack/react-query";
export const AccountPopover = (props) => {
const {
@@ -36,21 +39,36 @@ export const AccountPopover = (props) => {
const router = useRouter();
const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md"));
const popover = usePopover();
-
+ const queryClient = useQueryClient();
+ const { openReleaseNotes } = useReleaseNotes();
const orgData = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
+ });
+
+ const userDetails = orgData.data?.clientPrincipal?.userDetails;
+
+ // Cache user photo with user-specific key
+ const userPhoto = ApiGetCall({
+ url: "/api/ListUserPhoto",
+ data: { UserID: userDetails },
+ queryKey: `userPhoto-${userDetails}`,
+ waiting: !!userDetails,
+ staleTime: Infinity,
+ responseType: "blob",
+ convertToDataUrl: true,
});
const handleLogout = useCallback(async () => {
try {
popover.handleClose();
+ // delete query cache and persisted data
+ queryClient.clear();
router.push("/.auth/logout?post_logout_redirect_uri=" + encodeURIComponent(paths.index));
} catch (err) {
console.error(err);
+ console.log(orgData);
toast.error("Something went wrong");
}
}, [router, popover]);
@@ -60,15 +78,12 @@ export const AccountPopover = (props) => {
sx={{
height: 40,
width: 40,
+ fontSize: 20,
}}
variant="rounded"
- src={
- orgData.data?.clientPrincipal?.userDetails
- ? `/api/ListUserPhoto?UserID=${orgData.data?.clientPrincipal?.userDetails}`
- : ""
- }
+ src={userPhoto.data && !userPhoto.isError ? userPhoto.data : undefined}
>
- {orgData.data?.userDetails?.[0] || ""}
+ {userDetails?.[0]?.toUpperCase() || ""}
);
@@ -89,16 +104,22 @@ export const AccountPopover = (props) => {
<>
- {orgData.data?.Org?.Domain}
+ {orgData.data?.clientPrincipal?.userDetails?.split("@")?.[1]}
{orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"}
{orgData.data?.clientPrincipal?.userDetails && (
-
-
-
+ <>
+ {orgData?.isFetching ? (
+
+ ) : (
+
+
+
+ )}
+ >
)}
>
)}
@@ -143,6 +164,19 @@ export const AccountPopover = (props) => {
+ {
+ popover.handleClose();
+ openReleaseNotes();
+ }}
+ >
+
+
+
+
+
+
+
diff --git a/src/layouts/config.js b/src/layouts/config.js
index b1d9e72b9623..115de1795d46 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -1,12 +1,9 @@
import { BuildingOfficeIcon, HomeIcon, UsersIcon, WrenchIcon } from "@heroicons/react/24/outline";
import {
- Cloud,
CloudOutlined,
- DeviceHub,
HomeRepairService,
Laptop,
MailOutline,
- Shield,
ShieldOutlined,
} from "@mui/icons-material";
import { SvgIcon } from "@mui/material";
@@ -20,6 +17,7 @@ export const nativeMenuItems = [
),
+ permissions: ["CIPP.Core.*"],
},
{
title: "Identity Management",
@@ -29,40 +27,96 @@ export const nativeMenuItems = [
),
+ permissions: ["Identity.*"],
items: [
{
title: "Administration",
path: "/identity/administration",
+ permissions: ["Identity.User.*"],
items: [
- { title: "Users", path: "/identity/administration/users" },
- { title: "Risky Users", path: "/identity/administration/risky-users" },
- { title: "Groups", path: "/identity/administration/groups" },
+ {
+ title: "Users",
+ path: "/identity/administration/users",
+ permissions: ["Identity.User.*"],
+ },
+ {
+ title: "Risky Users",
+ path: "/identity/administration/risky-users",
+ permissions: ["Identity.User.*"],
+ },
+ {
+ title: "Groups",
+ path: "/identity/administration/groups",
+ permissions: ["Identity.Group.*"],
+ },
{
title: "Group Templates",
path: "/identity/administration/group-templates",
+ permissions: ["Identity.Group.*"],
+ },
+ {
+ title: "Devices",
+ path: "/identity/administration/devices",
+ permissions: ["Identity.Device.*"],
+ },
+ {
+ title: "Deleted Items",
+ path: "/identity/administration/deleted-items",
+ permissions: ["Identity.User.*"],
+ },
+ {
+ title: "Roles",
+ path: "/identity/administration/roles",
+ permissions: ["Identity.Role.*"],
+ },
+ {
+ title: "JIT Admin",
+ path: "/identity/administration/jit-admin",
+ permissions: ["Identity.Role.*"],
},
- { title: "Devices", path: "/identity/administration/devices" },
- { title: "Deleted Items", path: "/identity/administration/deleted-items" },
- { title: "Roles", path: "/identity/administration/roles" },
- { title: "JIT Admin", path: "/identity/administration/jit-admin" },
{
title: "Offboarding Wizard",
path: "/identity/administration/offboarding-wizard",
+ permissions: ["Identity.User.*"],
},
],
},
{
title: "Reports",
path: "/identity/reports",
+ permissions: [
+ "Identity.User.*",
+ "Identity.Group.*",
+ "Identity.Device.*",
+ "Identity.Role.*",
+ "Identity.AuditLog.*",
+ ],
items: [
- { title: "MFA Report", path: "/identity/reports/mfa-report" },
- { title: "Inactive Users", path: "/identity/reports/inactive-users-report" },
- { title: "Sign-in Report", path: "/identity/reports/signin-report" },
+ {
+ title: "MFA Report",
+ path: "/identity/reports/mfa-report",
+ permissions: ["Identity.User.*"],
+ },
+ {
+ title: "Inactive Users",
+ path: "/identity/reports/inactive-users-report",
+ permissions: ["Identity.User.*"],
+ },
+ {
+ title: "Sign-in Report",
+ path: "/identity/reports/signin-report",
+ permissions: ["Identity.User.*"],
+ },
{
title: "AAD Connect Report",
path: "/identity/reports/azure-ad-connect-report",
+ permissions: ["Identity.User.*"],
+ },
+ {
+ title: "Risk Detections",
+ path: "/identity/reports/risk-detections",
+ permissions: ["Identity.User.*"],
},
- { title: "Risk Detections", path: "/identity/reports/risk-detections" },
],
},
],
@@ -75,94 +129,132 @@ export const nativeMenuItems = [
),
+ permissions: ["Tenant.*", "Identity.AuditLog.*", "CIPP.Backup.*", "Scheduler.Billing.*"],
items: [
{
title: "Administration",
path: "/tenant/administration",
+ permissions: ["Tenant.Administration.*"],
items: [
- { title: "Tenants", path: "/tenant/administration/tenants" },
+ {
+ title: "Tenants",
+ path: "/tenant/administration/tenants",
+ permissions: ["Tenant.Administration.*"],
+ },
{
title: "Alert Configuration",
path: "/tenant/administration/alert-configuration",
+ permissions: ["Tenant.Alert.*"],
+ },
+ {
+ title: "Audit Logs",
+ path: "/tenant/administration/audit-logs",
+ permissions: ["Identity.AuditLog.*"],
+ },
+ {
+ title: "Applications",
+ path: "/tenant/administration/applications/enterprise-apps",
+ permissions: ["Tenant.Application.*"],
},
- { title: "Audit Logs", path: "/tenant/administration/audit-logs" },
{
- title: "Enterprise Applications",
- path: "/tenant/administration/enterprise-apps",
+ title: "Secure Score",
+ path: "/tenant/administration/securescore",
+ permissions: ["Tenant.Administration.*"],
},
- { title: "Secure Score", path: "/tenant/administration/securescore" },
{
title: "App Consent Requests",
path: "/tenant/administration/app-consent-requests",
+ permissions: ["Tenant.Application.*"],
},
{
title: "Authentication Methods",
path: "/tenant/administration/authentication-methods",
+ permissions: ["Tenant.Config.*"],
},
{
title: "Partner Relationships",
path: "/tenant/administration/partner-relationships",
+ permissions: ["Tenant.Relationship.*"],
},
],
},
{
title: "GDAP Management",
path: "/tenant/gdap-management/",
+ permissions: ["Tenant.Relationship.*"],
},
{
- title: "Configuration Backup",
- path: "/tenant/backup",
- items: [{ title: "Backups", path: "/tenant/backup/backup-wizard" }],
- },
- {
- title: "Standards",
+ title: "Standards & Drift",
path: "/tenant/standards",
+ permissions: [
+ "Tenant.Standards.*",
+ "Tenant.BestPracticeAnalyser.*",
+ "Tenant.DomainAnalyser.*",
+ ],
items: [
- { title: "Standards", path: "/tenant/standards/list-standards" },
+ {
+ title: "Standards Management",
+ path: "/tenant/standards/list-standards",
+ permissions: ["Tenant.Standards.*"],
+ },
{
title: "Best Practice Analyser",
path: "/tenant/standards/bpa-report",
+ permissions: ["Tenant.BestPracticeAnalyser.*"],
},
{
title: "Domains Analyser",
path: "/tenant/standards/domains-analyser",
+ permissions: ["Tenant.DomainAnalyser.*"],
},
],
},
{
title: "Conditional Access",
path: "/tenant/conditional",
+ permissions: ["Tenant.ConditionalAccess.*"],
items: [
- { title: "CA Policies", path: "/tenant/conditional/list-policies" },
+ {
+ title: "CA Policies",
+ path: "/tenant/conditional/list-policies",
+ permissions: ["Tenant.ConditionalAccess.*"],
+ },
{
title: "CA Vacation Mode",
path: "/tenant/conditional/deploy-vacation",
+ permissions: ["Tenant.ConditionalAccess.*"],
},
{
title: "CA Templates",
path: "/tenant/conditional/list-template",
+ permissions: ["Tenant.ConditionalAccess.*"],
},
{
title: "Named Locations",
path: "/tenant/conditional/list-named-locations",
+ permissions: ["Tenant.ConditionalAccess.*"],
},
],
},
{
title: "Reports",
path: "/tenant/reports",
+ permissions: ["Tenant.Administration.*", "Scheduler.Billing.*", "Tenant.Application.*"],
items: [
{
title: "Licence Report",
path: "/tenant/reports/list-licenses",
+ permissions: ["Tenant.Administration.*"],
},
{
title: "Sherweb Licence Report",
path: "/tenant/reports/list-csp-licenses",
+ permissions: ["Tenant.Directory.*"],
},
{
title: "Consented Applications",
path: "/tenant/reports/application-consent",
+ permissions: ["Tenant.Application.*"],
},
],
},
@@ -176,37 +268,88 @@ export const nativeMenuItems = [
),
+ permissions: [
+ "Security.Incident.*",
+ "Security.Alert.*",
+ "Tenant.DeviceCompliance.*",
+ "Security.SafeLinksPolicy.*",
+ ],
items: [
{
title: "Incidents & Alerts",
path: "/security/incidents",
+ permissions: ["Security.Incident.*"],
items: [
- { title: "Incidents", path: "/security/incidents/list-incidents" },
- { title: "Alerts", path: "/security/incidents/list-alerts" },
+ {
+ title: "Incidents",
+ path: "/security/incidents/list-incidents",
+ permissions: ["Security.Incident.*"],
+ },
+ {
+ title: "Alerts",
+ path: "/security/incidents/list-alerts",
+ permissions: ["Security.Alert.*"],
+ },
+ {
+ title: "MDO Alerts",
+ path: "/security/incidents/list-mdo-alerts",
+ permissions: ["Security.Alert.*"],
+ },
+ {
+ title: "Check Alerts",
+ path: "/security/incidents/list-check-alerts",
+ permissions: ["Security.Alert.*"],
+ },
],
},
{
title: "Defender",
path: "/security/defender",
+ permissions: ["Security.Alert.*"],
items: [
- { title: "Defender Status", path: "/security/defender/list-defender" },
+ {
+ title: "Defender Status",
+ path: "/security/defender/list-defender",
+ permissions: ["Security.Alert.*"],
+ },
{
title: "Defender Deployment",
path: "/security/defender/deployment",
+ permissions: ["Security.Alert.*"],
},
{
title: "Vulnerabilities",
path: "/security/defender/list-defender-tvm",
+ permissions: ["Security.Alert.*"],
},
],
},
{
title: "Reports",
path: "/security/reports",
+ permissions: ["Tenant.DeviceCompliance.*"],
items: [
{
title: "Device Compliance",
path: "/security/reports/list-device-compliance",
+ permissions: ["Tenant.DeviceCompliance.*"],
+ },
+ ],
+ },
+ {
+ title: "Safe Links",
+ path: "/security/safelinks",
+ permissions: ["Security.SafeLinksPolicy.*"],
+ items: [
+ {
+ title: "Safe Links Policies",
+ path: "/security/safelinks/safelinks",
+ permissions: ["Security.SafeLinksPolicy.*"],
+ },
+ {
+ title: "Safe Links Templates",
+ path: "/security/safelinks/safelinks-template",
+ permissions: ["Security.SafeLinksPolicy.*"],
},
],
},
@@ -220,45 +363,125 @@ export const nativeMenuItems = [
),
+ permissions: [
+ "Endpoint.Application.*",
+ "Endpoint.Autopilot.*",
+ "Endpoint.MEM.*",
+ "Endpoint.Device.*",
+ "Endpoint.Device.Read",
+ ],
items: [
{
title: "Applications",
path: "/endpoint/applications",
+ permissions: ["Endpoint.Application.*"],
items: [
- { title: "Applications", path: "/endpoint/applications/list" },
- { title: "Application Queue", path: "/endpoint/applications/queue" },
+ {
+ title: "Applications",
+ path: "/endpoint/applications/list",
+ permissions: ["Endpoint.Application.*"],
+ },
+ {
+ title: "Application Queue",
+ path: "/endpoint/applications/queue",
+ permissions: ["Endpoint.Application.*"],
+ },
],
},
{
title: "Autopilot",
path: "/endpoint/autopilot",
+ permissions: ["Endpoint.Autopilot.*"],
items: [
- { title: "Autopilot Devices", path: "/endpoint/autopilot/list-devices" },
- { title: "Add Autopilot Device", path: "/endpoint/autopilot/add-device" },
- { title: "Profiles", path: "/endpoint/autopilot/list-profiles" },
- { title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" },
- { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" },
+ {
+ title: "Autopilot Devices",
+ path: "/endpoint/autopilot/list-devices",
+ permissions: ["Endpoint.Autopilot.*"],
+ },
+ {
+ title: "Add Autopilot Device",
+ path: "/endpoint/autopilot/add-device",
+ permissions: ["Endpoint.Autopilot.*"],
+ },
+ {
+ title: "Profiles",
+ path: "/endpoint/autopilot/list-profiles",
+ permissions: ["Endpoint.Autopilot.*"],
+ },
+ {
+ title: "Status Pages",
+ path: "/endpoint/autopilot/list-status-pages",
+ permissions: ["Endpoint.Autopilot.*"],
+ },
],
},
{
title: "Device Management",
path: "/endpoint/MEM",
+ permissions: ["Endpoint.MEM.*"],
items: [
- { title: "Devices", path: "/endpoint/MEM/devices" },
- { title: "Configuration Policies", path: "/endpoint/MEM/list-policies" },
- { title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" },
- { title: "Protection Policies", path: "/endpoint/MEM/list-appprotection-policies" },
- { title: "Apply Policy", path: "/endpoint/MEM/add-policy" },
- { title: "Policy Templates", path: "/endpoint/MEM/list-templates" },
- { title: "Scripts", path: "/endpoint/MEM/list-scripts" },
+ {
+ title: "Devices",
+ path: "/endpoint/MEM/devices",
+ permissions: ["Endpoint.Device.*"],
+ },
+ {
+ title: "Configuration Policies",
+ path: "/endpoint/MEM/list-policies",
+ permissions: ["Endpoint.MEM.*"],
+ },
+ {
+ title: "Compliance Policies",
+ path: "/endpoint/MEM/list-compliance-policies",
+ permissions: ["Endpoint.MEM.*"],
+ },
+ {
+ title: "Protection Policies",
+ path: "/endpoint/MEM/list-appprotection-policies",
+ permissions: ["Endpoint.MEM.*"],
+ },
+ {
+ title: "Policy Templates",
+ path: "/endpoint/MEM/list-templates",
+ permissions: ["Endpoint.MEM.*"],
+ },
+ {
+ title: "Assignment Filters",
+ path: "/endpoint/MEM/assignment-filters",
+ permissions: ["Endpoint.MEM.*"],
+ },
+ {
+ title: "Assignment Filter Templates",
+ path: "/endpoint/MEM/assignment-filter-templates",
+ permissions: ["Endpoint.MEM.*"],
+ },
+ {
+ title: "Scripts",
+ path: "/endpoint/MEM/list-scripts",
+ permissions: ["Endpoint.MEM.*"],
+ },
],
},
{
title: "Reports",
path: "/endpoint/reports",
+ permissions: ["Endpoint.Device.*", "Endpoint.Autopilot.*"],
items: [
- { title: "Analytics Device Score", path: "/endpoint/reports/analyticsdevicescore" },
- { title: "Work from anywhere", path: "/endpoint/reports/workfromanywhere" },
+ {
+ title: "Analytics Device Score",
+ path: "/endpoint/reports/analyticsdevicescore",
+ permissions: ["Endpoint.Device.*"],
+ },
+ {
+ title: "Work from anywhere",
+ path: "/endpoint/reports/workfromanywhere",
+ permissions: ["Endpoint.Device.*"],
+ },
+ {
+ title: "Autopilot Deployments",
+ path: "/endpoint/reports/autopilot-deployment",
+ permissions: ["Endpoint.Autopilot.*"],
+ },
],
},
],
@@ -271,22 +494,44 @@ export const nativeMenuItems = [
),
+ permissions: [
+ "Sharepoint.Site.*",
+ "Sharepoint.Admin.*",
+ "Teams.Group.*",
+ "Teams.Activity.*",
+ "Teams.Voice.*",
+ ],
items: [
{
title: "OneDrive",
path: "/teams-share/onedrive",
+ permissions: ["Sharepoint.Site.*"],
},
{
title: "SharePoint",
path: "/teams-share/sharepoint",
+ permissions: ["Sharepoint.Admin.*"],
},
{
title: "Teams",
path: "/teams-share/teams",
+ permissions: ["Teams.Group.*"],
items: [
- { title: "Teams", path: "/teams-share/teams/list-team" },
- { title: "Teams Activity", path: "/teams-share/teams/teams-activity" },
- { title: "Business Voice", path: "/teams-share/teams/business-voice" },
+ {
+ title: "Teams",
+ path: "/teams-share/teams/list-team",
+ permissions: ["Teams.Group.*"],
+ },
+ {
+ title: "Teams Activity",
+ path: "/teams-share/teams/teams-activity",
+ permissions: ["Teams.Activity.*"],
+ },
+ {
+ title: "Business Voice",
+ path: "/teams-share/teams/business-voice",
+ permissions: ["Teams.Voice.*"],
+ },
],
},
],
@@ -299,91 +544,202 @@ export const nativeMenuItems = [
),
+ permissions: [
+ "Exchange.Mailbox.*",
+ "Exchange.Contact.*",
+ "Exchange.SpamFilter.*",
+ "Exchange.TransportRule.*",
+ "Exchange.Connector.*",
+ "Exchange.ConnectionFilter.*",
+ "Exchange.Equipment.*",
+ "Exchange.Room.*",
+ "Exchange.SafeLinks.*",
+ "Exchange.Group.*",
+ "Exchange.RetentionPolicies.*",
+ ],
items: [
{
title: "Administration",
path: "/email/administration",
+ permissions: ["Exchange.Mailbox.*"],
items: [
- { title: "Mailboxes", path: "/email/administration/mailboxes" },
- { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" },
- { title: "Mailbox Rules", path: "/email/administration/mailbox-rules" },
- { title: "Contacts", path: "/email/administration/contacts" },
- { title: "Quarantine", path: "/email/administration/quarantine" },
+ {
+ title: "Mailboxes",
+ path: "/email/administration/mailboxes",
+ permissions: ["Exchange.Mailbox.*"],
+ },
+ {
+ title: "Deleted Mailboxes",
+ path: "/email/administration/deleted-mailboxes",
+ permissions: ["Exchange.Mailbox.*"],
+ },
+ {
+ title: "Mailbox Rules",
+ path: "/email/administration/mailbox-rules",
+ permissions: ["Exchange.Mailbox.*"],
+ },
+ {
+ title: "Contacts",
+ path: "/email/administration/contacts",
+ permissions: ["Exchange.Contact.*"],
+ },
+ {
+ title: "Contact Templates",
+ path: "/email/administration/contacts-template",
+ permissions: ["Exchange.Contact.*"],
+ },
+ {
+ title: "Quarantine",
+ path: "/email/administration/quarantine",
+ permissions: ["Exchange.SpamFilter.*"],
+ },
+ {
+ title: "Restricted Users",
+ path: "/email/administration/restricted-users",
+ permissions: ["Exchange.Mailbox.*"],
+ },
{
title: "Tenant Allow/Block Lists",
path: "/email/administration/tenant-allow-block-lists",
+ permissions: ["Exchange.SpamFilter.*"],
+ },
+ {
+ title: "Retention Policies & Tags",
+ path: "/email/administration/exchange-retention/policies",
+ permissions: ["Exchange.RetentionPolicies.*"],
},
],
},
{
title: "Transport",
path: "/email/transport",
+ permissions: ["Exchange.TransportRule.*"],
items: [
- { title: "Transport rules", path: "/email/transport/list-rules" },
+ {
+ title: "Transport rules",
+ path: "/email/transport/list-rules",
+ permissions: ["Exchange.TransportRule.*"],
+ },
{
title: "Transport Templates",
path: "/email/transport/list-templates",
+ permissions: ["Exchange.TransportRule.*"],
+ },
+ {
+ title: "Connectors",
+ path: "/email/transport/list-connectors",
+ permissions: ["Exchange.Connector.*"],
},
- { title: "Connectors", path: "/email/transport/list-connectors" },
{
title: "Connector Templates",
path: "/email/transport/list-connector-templates",
+ permissions: ["Exchange.Connector.*"],
},
],
},
{
title: "Spamfilter",
path: "/email/spamfilter",
+ permissions: ["Exchange.SpamFilter.*"],
items: [
- { title: "Spamfilter", path: "/email/spamfilter/list-spamfilter" },
- { title: "Spamfilter templates", path: "/email/spamfilter/list-templates" },
- { title: "Connection filter", path: "/email/spamfilter/list-connectionfilter" },
+ {
+ title: "Spamfilter",
+ path: "/email/spamfilter/list-spamfilter",
+ permissions: ["Exchange.SpamFilter.*"],
+ },
+ {
+ title: "Spamfilter templates",
+ path: "/email/spamfilter/list-templates",
+ permissions: ["Exchange.SpamFilter.*"],
+ },
+ {
+ title: "Connection filter",
+ path: "/email/spamfilter/list-connectionfilter",
+ permissions: ["Exchange.ConnectionFilter.*"],
+ },
{
title: "Connection filter templates",
path: "/email/spamfilter/list-connectionfilter-templates",
+ permissions: ["Exchange.ConnectionFilter.*"],
+ },
+ {
+ title: "Quarantine Policies",
+ path: "/email/spamfilter/list-quarantine-policies",
+ permissions: ["Exchange.SpamFilter.*"],
},
],
},
{
title: "Resource Management",
path: "/email/resources/management",
+ permissions: ["Exchange.Equipment.*"],
items: [
- { title: "Rooms", path: "/email/resources/management/list-rooms" },
- { title: "Room Lists", path: "/email/resources/management/room-lists" },
+ {
+ title: "Equipment",
+ path: "/email/resources/management/equipment",
+ permissions: ["Exchange.Equipment.*"],
+ },
+ {
+ title: "Rooms",
+ path: "/email/resources/management/list-rooms",
+ permissions: ["Exchange.Room.*"],
+ },
+ {
+ title: "Room Lists",
+ path: "/email/resources/management/room-lists",
+ permissions: ["Exchange.Room.*"],
+ },
],
},
{
title: "Reports",
path: "/email/reports",
+ permissions: [
+ "Exchange.Mailbox.*",
+ "Exchange.SpamFilter.*",
+ "Exchange.SafeLinks.*",
+ "Exchange.Group.*",
+ ],
items: [
{
title: "Mailbox Statistics",
path: "/email/reports/mailbox-statistics",
+ permissions: ["Exchange.Mailbox.*"],
+ },
+ {
+ title: "Mailbox Activity",
+ path: "/email/reports/mailbox-activity",
+ permissions: ["Exchange.Mailbox.*"],
},
{
title: "Mailbox Client Access Settings",
path: "/email/reports/mailbox-cas-settings",
+ permissions: ["Exchange.Mailbox.*"],
},
{
title: "Anti-Phishing Filters",
path: "/email/reports/antiphishing-filters",
+ permissions: ["Exchange.SpamFilter.*"],
},
- { title: "Malware Filters", path: "/email/reports/malware-filters" },
{
- title: "Safe Links Filters",
- path: "/email/reports/safelinks-filters",
+ title: "Malware Filters",
+ path: "/email/reports/malware-filters",
+ permissions: ["Exchange.SpamFilter.*"],
},
{
title: "Safe Attachments Filters",
path: "/email/reports/safeattachments-filters",
+ permissions: ["Exchange.SafeLinks.*"],
},
{
title: "Shared Mailbox with Enabled Account",
path: "/email/reports/SharedMailboxEnabledAccount",
+ permissions: ["Exchange.Mailbox.*"],
},
{
title: "Global Address List",
path: "/email/reports/global-address-list",
+ permissions: ["Exchange.Group.*"],
},
],
},
@@ -397,60 +753,104 @@ export const nativeMenuItems = [
),
+ permissions: [
+ "CIPP.*",
+ "Tenant.Administration.*",
+ "Tenant.Application.*",
+ "Tenant.DomainAnalyser.*",
+ "Exchange.Mailbox.*",
+ ],
items: [
{
title: "Tenant Tools",
path: "/tenant/tools",
+ permissions: ["Tenant.Administration.*"],
items: [
{
title: "Graph Explorer",
path: "/tenant/tools/graph-explorer",
+ permissions: ["Tenant.Administration.*"],
},
{
title: "Application Approval",
path: "/tenant/tools/appapproval",
+ permissions: ["Tenant.Application.*"],
+ },
+ {
+ title: "Tenant Lookup",
+ path: "/tenant/tools/tenantlookup",
+ permissions: ["Tenant.Administration.*"],
},
- { title: "Tenant Lookup", path: "/tenant/tools/tenantlookup" },
- { title: "IP Database", path: "/tenant/tools/geoiplookup" },
+ {
+ title: "IP Database",
+ path: "/tenant/tools/geoiplookup",
+ permissions: ["CIPP.Core.*"],
+ },
{
title: "Individual Domain Check",
path: "/tenant/tools/individual-domains",
+ permissions: ["Tenant.DomainAnalyser.*"],
},
],
},
{
title: "Email Tools",
path: "/email/tools",
+ permissions: ["Exchange.Mailbox.*"],
items: [
- { title: "Message Trace", path: "/email/tools/message-trace" },
- { title: "Mailbox Restores", path: "/email/tools/mailbox-restores" },
- { title: "Message Viewer", path: "/email/tools/message-viewer" },
+ {
+ title: "Message Trace",
+ path: "/email/tools/message-trace",
+ permissions: ["Exchange.Mailbox.*"],
+ },
+ {
+ title: "Mailbox Restores",
+ path: "/email/tools/mailbox-restores",
+ permissions: ["Exchange.Mailbox.*"],
+ },
+ {
+ title: "Message Viewer",
+ path: "/email/tools/message-viewer",
+ permissions: ["Exchange.Mailbox.*"],
+ },
],
},
{
title: "Dark Web Tools",
path: "/tools/darkweb",
+ permissions: ["CIPP.Core.*"],
items: [
- { title: "Tenant Breach Lookup", path: "/tools/tenantbreachlookup" },
- { title: "Breach Lookup", path: "/tools/breachlookup" },
+ {
+ title: "Tenant Breach Lookup",
+ path: "/tools/tenantbreachlookup",
+ permissions: ["CIPP.Core.*"],
+ },
+ {
+ title: "Breach Lookup",
+ path: "/tools/breachlookup",
+ permissions: ["CIPP.Core.*"],
+ },
],
},
{
title: "Template Library",
path: "/tools/templatelib",
roles: ["editor", "admin", "superadmin"],
+ permissions: ["CIPP.Core.*"],
},
{
title: "Community Repositories",
path: "/tools/community-repos",
roles: ["editor", "admin", "superadmin"],
+ permissions: ["CIPP.Core.*"],
},
{
title: "Scheduler",
path: "/cipp/scheduler",
roles: ["editor", "admin", "superadmin"],
+ permissions: ["CIPP.Scheduler.*"],
},
],
},
@@ -462,35 +862,68 @@ export const nativeMenuItems = [
),
+ permissions: [
+ "CIPP.*", // Pattern matching - matches any CIPP permission
+ ],
items: [
- { title: "Application Settings", path: "/cipp/settings", roles: ["admin", "superadmin"] },
- { title: "Logbook", path: "/cipp/logs", roles: ["editor", "admin", "superadmin"] },
- { title: "SAM Setup Wizard", path: "/onboarding", roles: ["admin", "superadmin"] },
- { title: "Integrations", path: "/cipp/integrations", roles: ["admin", "superadmin"] },
+ {
+ title: "Application Settings",
+ path: "/cipp/settings",
+ roles: ["admin", "superadmin"],
+ permissions: ["CIPP.AppSettings.*"],
+ },
+ {
+ title: "Logbook",
+ path: "/cipp/logs",
+ roles: ["editor", "admin", "superadmin"],
+ permissions: ["CIPP.Core.*"],
+ },
+ {
+ title: "Setup Wizard",
+ path: "/onboardingv2",
+ roles: ["admin", "superadmin"],
+ permissions: ["CIPP.Core.*"],
+ },
+ {
+ title: "Integrations",
+ path: "/cipp/integrations",
+ roles: ["admin", "superadmin"],
+ permissions: ["CIPP.Extension.*"],
+ },
{
title: "Custom Data",
path: "/cipp/custom-data/directory-extensions",
- roles: ["admin", "superadmin"]
+ roles: ["admin", "superadmin"],
+ permissions: ["CIPP.Core.*"],
},
{
title: "Advanced",
roles: ["superadmin"],
+ permissions: ["CIPP.SuperAdmin.*"],
items: [
- { title: "Super Admin", path: "/cipp/super-admin/tenant-mode", roles: ["superadmin"] },
+ {
+ title: "Super Admin",
+ path: "/cipp/super-admin/tenant-mode",
+ roles: ["superadmin"],
+ permissions: ["CIPP.SuperAdmin.*"],
+ },
{
title: "Exchange Cmdlets",
path: "/cipp/advanced/exchange-cmdlets",
roles: ["superadmin"],
+ permissions: ["CIPP.SuperAdmin.*"],
},
{
title: "Timers",
path: "/cipp/advanced/timers",
roles: ["superadmin"],
+ permissions: ["CIPP.SuperAdmin.*"],
},
{
title: "Table Maintenance",
path: "/cipp/advanced/table-maintenance",
roles: ["superadmin"],
+ permissions: ["CIPP.SuperAdmin.*"],
},
],
},
diff --git a/src/layouts/footer.js b/src/layouts/footer.js
index 07115518e388..d6bd01193b56 100644
--- a/src/layouts/footer.js
+++ b/src/layouts/footer.js
@@ -1,4 +1,4 @@
-import { Box, Container, Divider, Typography } from "@mui/material";
+import { Container } from "@mui/material";
export const Footer = () => {
diff --git a/src/layouts/index.js b/src/layouts/index.js
index ddd1b2460645..5813fc0ee70e 100644
--- a/src/layouts/index.js
+++ b/src/layouts/index.js
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useState, useRef } from "react";
import { usePathname } from "next/navigation";
import { Alert, Button, Dialog, DialogContent, DialogTitle, useMediaQuery } from "@mui/material";
import { styled } from "@mui/material/styles";
@@ -12,10 +12,9 @@ import { useDispatch } from "react-redux";
import { showToast } from "../store/toasts";
import { Box, Container, Grid } from "@mui/system";
import { CippImageCard } from "../components/CippCards/CippImageCard";
-import Page from "../pages/onboarding";
+import Page from "../pages/onboardingv2";
import { useDialog } from "../hooks/use-dialog";
import { nativeMenuItems } from "/src/layouts/config";
-import { keepPreviousData } from "@tanstack/react-query";
const SIDE_NAV_WIDTH = 270;
const SIDE_NAV_PINNED_WIDTH = 50;
@@ -31,13 +30,9 @@ const useMobileNav = () => {
}
}, [open]);
- useEffect(
- () => {
- handlePathnameChange();
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [pathname]
- );
+ useEffect(() => {
+ handlePathnameChange();
+ }, [pathname]);
const handleOpen = useCallback(() => {
setOpen(true);
@@ -80,18 +75,27 @@ export const Layout = (props) => {
const [userSettingsComplete, setUserSettingsComplete] = useState(false);
const [fetchingVisible, setFetchingVisible] = useState([]);
const [menuItems, setMenuItems] = useState(nativeMenuItems);
+ const lastUserSettingsUpdate = useRef(null);
const currentTenant = settings?.currentTenant;
- const currentRole = ApiGetCall({
+ const [hideSidebar, setHideSidebar] = useState(false);
+
+ const swaStatus = ApiGetCall({
url: "/.auth/me",
- queryKey: "authmecipp",
+ queryKey: "authmeswa",
staleTime: 120000,
refetchOnWindowFocus: true,
});
- const [hideSidebar, setHideSidebar] = useState(false);
+
+ const currentRole = ApiGetCall({
+ url: "/api/me",
+ queryKey: "authmecipp",
+ waiting: !swaStatus.isSuccess || swaStatus.data?.clientPrincipal === null,
+ });
useEffect(() => {
if (currentRole.isSuccess && !currentRole.isFetching) {
const userRoles = currentRole.data?.clientPrincipal?.userRoles;
+ const userPermissions = currentRole.data?.permissions;
if (!userRoles) {
setMenuItems([]);
setHideSidebar(true);
@@ -100,12 +104,43 @@ export const Layout = (props) => {
const filterItemsByRole = (items) => {
return items
.map((item) => {
+ // role
if (item.roles && item.roles.length > 0) {
const hasRole = item.roles.some((requiredRole) => userRoles.includes(requiredRole));
if (!hasRole) {
return null;
}
}
+
+ // Check permission with pattern matching support
+ if (item.permissions && item.permissions.length > 0) {
+ const hasPermission = userPermissions?.some((userPerm) => {
+ return item.permissions.some((requiredPerm) => {
+ // Exact match
+ if (userPerm === requiredPerm) {
+ return true;
+ }
+
+ // Pattern matching - check if required permission contains wildcards
+ if (requiredPerm.includes("*")) {
+ // Convert wildcard pattern to regex
+ const regexPattern = requiredPerm
+ .replace(/\./g, "\\.") // Escape dots
+ .replace(/\*/g, ".*"); // Convert * to .*
+ const regex = new RegExp(`^${regexPattern}$`);
+ return regex.test(userPerm);
+ }
+
+ return false;
+ });
+ });
+ if (!hasPermission) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ // check sub-items
if (item.items && item.items.length > 0) {
const filteredSubItems = filterItemsByRole(item.items).filter(Boolean);
return { ...item, items: filteredSubItems };
@@ -115,11 +150,24 @@ export const Layout = (props) => {
})
.filter(Boolean);
};
-
const filteredMenu = filterItemsByRole(nativeMenuItems);
setMenuItems(filteredMenu);
+ } else if (
+ swaStatus.isLoading ||
+ swaStatus.data?.clientPrincipal === null ||
+ swaStatus.data === undefined ||
+ currentRole.isLoading
+ ) {
+ setHideSidebar(true);
}
- }, [currentRole.isSuccess]);
+ }, [
+ currentRole.isSuccess,
+ swaStatus.data,
+ swaStatus.isLoading,
+ currentRole.data?.clientPrincipal?.userRoles,
+ currentRole.data?.permissions,
+ currentRole.isFetching,
+ ]);
const handleNavPin = useCallback(() => {
settings.handleUpdate({
@@ -132,38 +180,44 @@ export const Layout = (props) => {
const userSettingsAPI = ApiGetCall({
url: "/api/ListUserSettings",
queryKey: "userSettings",
- refetchOnMount: false,
- refetchOnReconnect: false,
- keepPreviousData: true,
});
useEffect(() => {
- if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching && !userSettingsComplete) {
- //if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key.
- if (userSettingsAPI.data.offboardingDefaults?.user) {
- delete userSettingsAPI.data.offboardingDefaults.user;
- }
- if (userSettingsAPI?.data?.currentTheme) {
- delete userSettingsAPI.data.currentTheme;
- }
- // get current devtools settings
- var showDevtools = settings.showDevtools;
- // get current bookmarks
- var bookmarks = settings.bookmarks;
+ if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching) {
+ // Only update if the data has actually changed (using dataUpdatedAt as a proxy)
+ const dataUpdatedAt = userSettingsAPI.dataUpdatedAt;
+ if (dataUpdatedAt && dataUpdatedAt !== lastUserSettingsUpdate.current) {
+ //if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key.
+ if (userSettingsAPI.data.offboardingDefaults?.user) {
+ delete userSettingsAPI.data.offboardingDefaults.user;
+ }
+ if (userSettingsAPI.data.offboardingDefaults?.keepCopy) {
+ delete userSettingsAPI.data.offboardingDefaults.keepCopy;
+ }
+ if (userSettingsAPI?.data?.currentTheme) {
+ delete userSettingsAPI.data.currentTheme;
+ }
+ // get current devtools settings
+ var showDevtools = settings.showDevtools;
+ // get current bookmarks
+ var bookmarks = settings.bookmarks;
+
+ settings.handleUpdate({
+ ...userSettingsAPI.data,
+ bookmarks,
+ showDevtools,
+ });
- settings.handleUpdate({
- ...userSettingsAPI.data,
- bookmarks,
- showDevtools,
- });
- setUserSettingsComplete(true);
+ // Track this update and set completion status
+ lastUserSettingsUpdate.current = dataUpdatedAt;
+ setUserSettingsComplete(true);
+ }
}
}, [
userSettingsAPI.isSuccess,
userSettingsAPI.data,
userSettingsAPI.isFetching,
- userSettingsComplete,
- settings,
+ userSettingsAPI.dataUpdatedAt,
]);
const version = ApiGetCall({
@@ -181,11 +235,11 @@ export const Layout = (props) => {
});
useEffect(() => {
- if (version.isFetched && !alertsAPI.isFetched) {
+ if (!hideSidebar && version.isFetched && !alertsAPI.isFetched) {
alertsAPI.waiting = true;
alertsAPI.refetch();
}
- }, [version, alertsAPI]);
+ }, [version, alertsAPI, hideSidebar]);
useEffect(() => {
if (alertsAPI.isSuccess && !alertsAPI.isFetching) {
@@ -238,11 +292,32 @@ export const Layout = (props) => {
}}
>
+
+ Setup Wizard
+
+
+
+
+ {!setupCompleted && (
+
+
+
+ Setup has not been completed.
+ Start Wizard
+
+
+
+ )}
{(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? (
-
+ {
) : (
- <>
-
- Setup Wizard
-
-
-
-
- {!setupCompleted && (
-
-
-
- Setup has not been completed.
- Start Wizard
-
-
-
- )}
- {children}
- >
+ <>{children}>
)}
diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js
index ca6d32b13ed8..772671f5e3db 100644
--- a/src/layouts/side-nav.js
+++ b/src/layouts/side-nav.js
@@ -15,7 +15,6 @@ const markOpenItems = (items, pathname) => {
return items.map((item) => {
const checkPath = !!(item.path && pathname);
const exactMatch = checkPath ? pathname === item.path : false;
- // Use startsWith for partial matches so that subpages not in the menu still keep parent open
const partialMatch = checkPath ? pathname.startsWith(item.path) : false;
let openImmediately = exactMatch;
@@ -24,11 +23,9 @@ const markOpenItems = (items, pathname) => {
if (newItems.length > 0) {
newItems = markOpenItems(newItems, pathname);
const childOpen = newItems.some((child) => child.openImmediately);
- // Parent should open if exactMatch, childOpen, or partialMatch
- openImmediately = openImmediately || childOpen || partialMatch;
+ openImmediately = openImmediately || childOpen || exactMatch; // Ensure parent opens if child is open
} else {
- // For leaf items, consider them open if exact or partial match
- openImmediately = openImmediately || partialMatch;
+ openImmediately = openImmediately || partialMatch; // Leaf items open on partial match
}
return {
@@ -47,8 +44,6 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname }) => {
const exactMatch = checkPath && pathname === item.path;
const partialMatch = checkPath && pathname.startsWith(item.path);
- // Consider item active if exactMatch or partialMatch for leaf items
- // For parent items, being active is determined by their children or openImmediately
const hasChildren = item.items && item.items.length > 0;
const isActive = exactMatch || (partialMatch && !hasChildren);
@@ -107,7 +102,7 @@ export const SideNav = (props) => {
const pathname = usePathname();
const [hovered, setHovered] = useState(false);
const collapse = !(pinned || hovered);
- const { data: profile } = ApiGetCall({ url: "/.auth/me", queryKey: "authmecipp" });
+ const { data: profile } = ApiGetCall({ url: "/api/me", queryKey: "authmecipp" });
// Preprocess items to mark which should be open
const processedItems = markOpenItems(items, pathname);
@@ -121,8 +116,8 @@ export const SideNav = (props) => {
priority: 1,
},
{
- link: "https://rightofboom.com",
- imagesrc: theme === "light" ? "/sponsors/RoB-light.svg" : "/sponsors/RoB.png",
+ link: "https://www.domotz.com/cipp-community-free-domotz-beta.php?utm_source=Community_CIPP&utm_medium=Community_CIPP&utm_campaign=Community_CIPP",
+ imagesrc: theme === "light" ? "/sponsors/domotz-light.png" : "/sponsors/domotz-dark.png",
priority: 1,
},
{
@@ -140,6 +135,11 @@ export const SideNav = (props) => {
imagesrc: "/sponsors/huntress_teal.png",
priority: 1,
},
+ {
+ link: "https://rightofboom.com/rob-2026-overview/rob-2026-registration/?utm_source=CIPP&utm_medium=referral&utm_campaign=CIPPM365&utm_content=cta_button",
+ imagesrc: theme === "light" ? "/sponsors/RoB-light.png" : "/sponsors/RoB.png",
+ priority: 1,
+ },
];
const randomSponsorImage = () => {
@@ -159,90 +159,102 @@ export const SideNav = (props) => {
const randomimg = randomSponsorImage();
return (
- {
- setHovered(true);
- },
- onMouseLeave: () => {
- setHovered(false);
- },
- sx: {
- backgroundColor: "background.default",
- height: `calc(100% - ${TOP_NAV_HEIGHT}px)`,
- overflowX: "hidden",
- top: TOP_NAV_HEIGHT,
- transition: "width 250ms ease-in-out",
- width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH,
- zIndex: (theme) => theme.zIndex.appBar - 100,
- },
- }}
- >
-
-
+ {profile?.clientPrincipal && profile?.clientPrincipal?.userRoles?.length > 2 && (
+ {
+ setHovered(true);
+ },
+ onMouseLeave: () => {
+ setHovered(false);
+ },
+ sx: {
+ backgroundColor: "background.default",
+ height: `calc(100% - ${TOP_NAV_HEIGHT}px)`,
+ overflowX: "hidden",
+ top: TOP_NAV_HEIGHT,
+ transition: "width 250ms ease-in-out",
+ width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH,
+ zIndex: (theme) => theme.zIndex.appBar - 100,
+ },
}}
>
-
- {renderItems({
- collapse,
- depth: 0,
- items: processedItems,
- pathname,
- })}
-
- {profile?.clientPrincipal && (
- <>
-
-
- This application is sponsored by
-
+
- window.open(randomimg.link)}
- width={"100px"}
- />
-
- >
- )}
-
-
-
+ {renderItems({
+ collapse,
+ depth: 0,
+ items: processedItems,
+ pathname,
+ })}
+ {" "}
+ {/* Add this closing tag */}
+ {profile?.clientPrincipal && (
+ <>
+
+
+ This application is sponsored by
+
+
+ window.open(randomimg.link)}
+ />
+
+ >
+ )}
+ {" "}
+ {/* Closing tag for the parent Box */}
+
+
+ )}
+ >
);
};
diff --git a/src/pages/401.js b/src/pages/401.js
index ccb828549c51..913107b2a202 100644
--- a/src/pages/401.js
+++ b/src/pages/401.js
@@ -1,4 +1,5 @@
-import { Box, Container, Grid, Stack } from "@mui/material";
+import { Box, Container, Stack } from "@mui/material";
+import { Grid } from "@mui/system";
import Head from "next/head";
import { CippImageCard } from "../components/CippCards/CippImageCard.jsx";
import { Layout as DashboardLayout } from "../layouts/index.js";
@@ -25,7 +26,7 @@ const Page = () => (
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+ (
alignItems="center" // Center vertically
sx={{ height: "100%" }} // Ensure the container takes full height
>
-
+ {
alignItems="center"
sx={{ height: "100%" }}
>
-
+
+ import("@tanstack/react-query-devtools/build/modern/production.js").then((d) => ({
+ default: d.ReactQueryDevtools,
+ }))
+);
TimeAgo.addDefaultLocale(en);
-import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
const clientSideEmotionCache = createEmotionCache();
@@ -45,6 +53,16 @@ const App = (props) => {
const preferredTheme = useMediaPredicate("(prefers-color-scheme: dark)") ? "dark" : "light";
const pathname = usePathname();
const route = useRouter();
+ const [_0x8h9i, _0x2j3k] = useState(false); // toRemove
+
+ const excludeQueryKeys = ["authmeswa", "alertsDashboard"];
+
+ const _0x4f2d = [1772236800, 1772391599]; // toRemove
+ const _0x2e1f = () => {
+ // toRemove
+ const _0x1a2b = Date.now() / 1000; // toRemove
+ return _0x1a2b >= _0x4f2d[0] && _0x1a2b <= _0x4f2d[1]; // toRemove
+ };
// π Persist TanStack Query cache to localStorage
useEffect(() => {
@@ -59,11 +77,150 @@ const App = (props) => {
maxAge: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // optional: 5 minutes
buster: "v1",
+ dehydrateOptions: {
+ shouldDehydrateQuery: (query) => {
+ const queryIsReadyForPersistence = query.state.status === "success";
+ if (queryIsReadyForPersistence) {
+ const { queryKey } = query;
+ // Check if queryKey exists and has elements before accessing index 0
+ if (!queryKey || !queryKey.length) {
+ return false;
+ }
+ const queryKeyString = String(queryKey[0] || "");
+ const excludeFromPersisting = excludeQueryKeys.some((key) =>
+ queryKeyString.includes(key)
+ );
+ return !excludeFromPersisting;
+ }
+ return queryIsReadyForPersistence;
+ },
+ },
});
}
}, []);
+ useEffect(() => {
+ // toRemove
+ if (_0x8h9i) {
+ // toRemove
+ const _0x3c4d = Array.from(document.querySelectorAll("*")).filter((_0x5e6f) => {
+ // toRemove
+ const _0x7g8h = document.querySelector('[aria-label="Navigation SpeedDial"]'); // toRemove
+ return !_0x7g8h?.contains(_0x5e6f); // toRemove
+ });
+
+ _0x3c4d.forEach((_0x9i0j, _0x1k2l) => {
+ // toRemove
+ const _0x3m4n = Math.random() * 10 - 5; // toRemove
+ const _0x5o6p = Math.random() * 10 - 5; // toRemove
+ const _0x7q8r = Math.random() * 10 - 5; // toRemove
+ const _0x9s0t = Math.random() * 0.5; // toRemove
+ const _0x1u2v = 0.3 + Math.random() * 0.4; // toRemove
+
+ const _0x3w4x = `_${_0x1k2l}`; // toRemove
+ const _0x5y6z = document.styleSheets[0]; // toRemove
+ _0x5y6z.insertRule(
+ ` // toRemove
+ @keyframes ${_0x3w4x} { // toRemove
+ 0% { transform: translate(0, 0) rotate(0deg); } // toRemove
+ 25% { transform: translate(${_0x3m4n}px, ${_0x5o6p}px) rotate(${_0x7q8r}deg); } // toRemove
+ 50% { transform: translate(0, 0) rotate(0deg); } // toRemove
+ 75% { transform: translate(${-_0x3m4n}px, ${_0x5o6p}px) rotate(${-_0x7q8r}deg); } // toRemove
+ 100% { transform: translate(0, 0) rotate(0deg); } // toRemove
+ }
+ `,
+ _0x5y6z.cssRules.length
+ ); // toRemove
+
+ _0x9i0j.style.animation = `${_0x3w4x} ${_0x1u2v}s infinite ${_0x9s0t}s`; // toRemove
+ });
+
+ const _0x1a2b = setTimeout(() => {
+ // toRemove
+ _0x2j3k(false); // toRemove
+ _0x3c4d.forEach((_0x5e6f) => {
+ // toRemove
+ _0x5e6f.style.animation = ""; // toRemove
+ });
+ const _0x7g8h = document.styleSheets[0]; // toRemove
+ while (_0x7g8h.cssRules.length > 0) {
+ // toRemove
+ _0x7g8h.deleteRule(0); // toRemove
+ }
+ }, 5000); // toRemove
+
+ return () => {
+ // toRemove
+ clearTimeout(_0x1a2b); // toRemove
+ _0x3c4d.forEach((_0x5e6f) => {
+ // toRemove
+ _0x5e6f.style.animation = ""; // toRemove
+ });
+ const _0x7g8h = document.styleSheets[0]; // toRemove
+ while (_0x7g8h.cssRules.length > 0) {
+ // toRemove
+ _0x7g8h.deleteRule(0); // toRemove
+ }
+ };
+ }
+ }, [_0x8h9i]); // toRemove
+
const speedDialActions = [
+ ...(_0x2e1f()
+ ? [
+ {
+ // toRemove
+ id: "_", // toRemove
+ icon: , // toRemove
+ name: String.fromCharCode(
+ 68,
+ 111,
+ 32,
+ 116,
+ 104,
+ 101,
+ 32,
+ 72,
+ 97,
+ 114,
+ 108,
+ 101,
+ 109,
+ 32,
+ 83,
+ 104,
+ 97,
+ 107,
+ 101,
+ 33
+ ), // toRemove
+ onClick: () => _0x2j3k(true), // toRemove
+ },
+ ]
+ : []), // toRemove
+ {
+ // add clear cache action that removes the persisted query cache from local storage and reloads the page
+ id: "clearCache",
+ icon: ,
+ name: "Clear Cache and Reload",
+ onClick: () => {
+ // Clear the TanStack Query cache
+ queryClient.clear();
+
+ // Remove persisted cache from localStorage
+ if (typeof window !== "undefined") {
+ // Remove the persisted query cache keys
+ Object.keys(localStorage).forEach((key) => {
+ if (key.startsWith("REACT_QUERY_OFFLINE_CACHE")) {
+ localStorage.removeItem(key);
+ }
+ });
+ }
+
+ // Force refresh the page to bypass browser cache and reload JavaScript
+ window.location.reload(true);
+ },
+ },
{
id: "license",
icon: ,
@@ -107,8 +264,8 @@ const App = (props) => {
id: "documentation",
icon: ,
name: "Check the Documentation",
- href: `https://docs.cipp.app/user-documentation/${pathname}`,
- onClick: () => window.open(`https://docs.cipp.app/user-documentation/${pathname}`, "_blank"),
+ href: `https://docs.cipp.app/user-documentation${pathname}`,
+ onClick: () => window.open(`https://docs.cipp.app/user-documentation${pathname}`, "_blank"),
},
];
@@ -142,7 +299,11 @@ const App = (props) => {
- {getLayout()}
+
+
+ {getLayout()}
+
+ {
{settings.isInitialized && settings?.showDevtools === true ? (
-
+
) : null}
>
diff --git a/src/pages/api-offline.js b/src/pages/api-offline.js
new file mode 100644
index 000000000000..77d125ac6486
--- /dev/null
+++ b/src/pages/api-offline.js
@@ -0,0 +1,167 @@
+import {
+ Box,
+ Button,
+ Container,
+ Stack,
+ Alert,
+ CircularProgress,
+ Typography,
+ SvgIcon,
+} from "@mui/material";
+import { Grid } from "@mui/system";
+import Head from "next/head";
+import { useState, useEffect } from "react";
+import axios from "axios";
+import { CippImageCard } from "../components/CippCards/CippImageCard";
+import { ErrorOutlineOutlined } from "@mui/icons-material";
+
+const ApiOfflinePage = () => {
+ const [testingConnection, setTestingConnection] = useState(false);
+ const [testResult, setTestResult] = useState(null);
+ const [apiVersion, setApiVersion] = useState(null);
+
+ // Check API version when component mounts
+ useEffect(() => {
+ const checkApiVersion = async () => {
+ try {
+ const response = await axios.get("/version.json", { timeout: 5000 });
+ setApiVersion(response.data?.version || "Unknown");
+ } catch (error) {
+ console.error("Failed to fetch API version:", error);
+ }
+ };
+
+ checkApiVersion();
+ }, []);
+
+ const handleTestConnection = async () => {
+ setTestingConnection(true);
+ setTestResult(null);
+
+ try {
+ // Try to ping the API
+ const testCall = await axios.get("/api/me", { timeout: 45000 });
+ console.log("API Test Call Response:", testCall);
+ if (!testCall.headers["content-type"]?.includes("application/json")) {
+ throw new Error("API did not return the expected response.");
+ }
+ setTestResult({ success: true, message: "Connection successful! Try refreshing the page." });
+ } catch (error) {
+ let errorMessage = "Connection failed.";
+
+ if (error.response) {
+ // Request was made and server responded with a status code outside of 2xx range
+ errorMessage = `API responded with status: ${error.response.status}`;
+ if (error.response.status === 404) {
+ errorMessage +=
+ " (API endpoint not found, this can be the case if your Function App is on Version 7 or below)";
+ }
+ } else if (error.request) {
+ // Request was made but no response received
+ errorMessage = "No response received from API. Check if your Function App is running.";
+ } else {
+ // Error in setting up the request
+ errorMessage = `Error: ${error.message}`;
+ }
+
+ setTestResult({ success: false, message: errorMessage });
+ } finally {
+ setTestingConnection(false);
+ }
+ };
+
+ // We're now using Typography components directly in the JSX
+ // instead of generating a help text string
+
+ return (
+ <>
+
+ API Offline
+
+
+
+
+
+
+
+
+
+
+ CIPP API Unreachable
+
+ }
+ text={
+
+
+ The CIPP API appears to be offline or out of date.
+ {apiVersion && (
+
+ Frontend Version: {apiVersion}
+
+ )}
+
+
+
+ If you are self-hosting CIPP, please ensure your Function App is running and
+ up to date. If you are using the hosted version, please check your
+ subscription in GitHub.
+
+
+ }
+ linkText={testingConnection ? "Testing Connection..." : "Test API Connection"}
+ onButtonClick={handleTestConnection}
+ />
+
+ {testResult && (
+
+
+ {testResult.message}
+ {testResult.success && (
+ window.location.reload()}
+ >
+ Refresh Page
+
+ )}
+
+
+ )}
+
+ {testingConnection && (
+
+
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+
+export default ApiOfflinePage;
diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js
new file mode 100644
index 000000000000..ea8edfe0937d
--- /dev/null
+++ b/src/pages/authredirect.js
@@ -0,0 +1,47 @@
+import { Box, Container, Stack } from "@mui/material";
+import { Grid } from "@mui/system";
+import Head from "next/head";
+import { CippImageCard } from "../components/CippCards/CippImageCard.jsx";
+import { Layout as DashboardLayout } from "../layouts/index.js";
+
+const Page = () => (
+ <>
+
+
+ Authentication complete
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+export default Page;
diff --git a/src/pages/cipp/advanced/exchange-cmdlets.js b/src/pages/cipp/advanced/exchange-cmdlets.js
index 76de317c7df1..68cb55fd7c3e 100644
--- a/src/pages/cipp/advanced/exchange-cmdlets.js
+++ b/src/pages/cipp/advanced/exchange-cmdlets.js
@@ -3,13 +3,13 @@ import {
Button,
Container,
Stack,
- Grid,
Dialog,
DialogTitle,
DialogContent,
IconButton,
Skeleton,
} from "@mui/material";
+import { Grid } from "@mui/system";
import { useForm } from "react-hook-form";
import CippFormComponent from "/src/components/CippComponents/CippFormComponent";
import { ApiPostCall } from "/src/api/ApiCall";
@@ -99,11 +99,11 @@ const Page = () => {
{/* Tenant Filter */}
-
+
{/* Compliance Filter */}
-
+ {
/>
{/* AsApp Filter */}
-
+ {
/>
{/* Submit Button */}
-
+ }>
Search
diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js
index d6cb9caf1a2b..4f4c53c30fb7 100644
--- a/src/pages/cipp/advanced/table-maintenance.js
+++ b/src/pages/cipp/advanced/table-maintenance.js
@@ -72,6 +72,7 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV
name={`fields[${index}].name`}
formControl={formControl}
label="Name"
+ disableVariables={true}
/>
@@ -101,6 +102,7 @@ const CustomAddEditRowDialog = ({ formControl, open, onClose, onSubmit, defaultV
return {};
}
}}
+ disableVariables={true}
/>
@@ -283,7 +285,7 @@ const Page = () => {
that should only be used when directed by CyberDrain support.
-
+ {
}
/>
-
+
{selectedTable && (
diff --git a/src/pages/cipp/advanced/timers.js b/src/pages/cipp/advanced/timers.js
index 823f985f36e1..116d27cd836f 100644
--- a/src/pages/cipp/advanced/timers.js
+++ b/src/pages/cipp/advanced/timers.js
@@ -1,6 +1,6 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { SvgIcon, Button } from "@mui/material";
-import { Refresh } from "@mui/icons-material";
+import { Refresh, PlayArrow } from "@mui/icons-material";
import { ApiPostCall } from "../../../api/ApiCall";
import { useEffect, useState } from "react";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage";
@@ -80,6 +80,8 @@ const Page = () => {
url: apiUrl,
data: { FunctionName: "Command", Parameters: "Parameters" },
confirmText: "Do you want to run this task now?",
+ allowResubmit: true,
+ icon: ,
},
]}
/>
diff --git a/src/pages/cipp/custom-data/directory-extensions/add.js b/src/pages/cipp/custom-data/directory-extensions/add.js
index 7238792aa4dd..975c7be997de 100644
--- a/src/pages/cipp/custom-data/directory-extensions/add.js
+++ b/src/pages/cipp/custom-data/directory-extensions/add.js
@@ -62,6 +62,7 @@ const Page = () => {
type: "textField",
required: true,
placeholder: "Enter a unique name for the directory extension",
+ disableVariables: true,
},
{
name: "dataType",
diff --git a/src/pages/cipp/custom-data/directory-extensions/index.js b/src/pages/cipp/custom-data/directory-extensions/index.js
index 42317151ffec..891342e97ce1 100644
--- a/src/pages/cipp/custom-data/directory-extensions/index.js
+++ b/src/pages/cipp/custom-data/directory-extensions/index.js
@@ -1,7 +1,7 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material";
+import { Alert, Button, Link, SvgIcon } from "@mui/material";
import { Add } from "@mui/icons-material";
import tabOptions from "../tabOptions";
import NextLink from "next/link";
diff --git a/src/pages/cipp/custom-data/mappings/add.js b/src/pages/cipp/custom-data/mappings/add.js
index d672d42d888d..cd77fc9eff6e 100644
--- a/src/pages/cipp/custom-data/mappings/add.js
+++ b/src/pages/cipp/custom-data/mappings/add.js
@@ -18,15 +18,42 @@ const Page = () => {
const addMappingApi = ApiPostCall({
urlFromData: true,
- relatedQueryKeys: ["MappingsListPage"],
+ relatedQueryKeys: ["MappingsListPage", "ManualEntryMappings*"],
});
const handleAddMapping = (data) => {
+ // Filter data based on source type to only include relevant fields
+ let filteredData;
+
+ if (data.sourceType?.value === "manualEntry") {
+ // For manual entry, only include these fields
+ filteredData = {
+ sourceType: data.sourceType,
+ manualEntryFieldLabel: data.manualEntryFieldLabel,
+ directoryObjectType: data.directoryObjectType,
+ customDataAttribute: data.customDataAttribute,
+ tenantFilter: data.tenantFilter,
+ };
+ } else if (data.sourceType?.value === "extensionSync") {
+ // For extension sync, include the original fields
+ filteredData = {
+ sourceType: data.sourceType,
+ extensionSyncDataset: data.extensionSyncDataset,
+ extensionSyncProperty: data.extensionSyncProperty,
+ directoryObjectType: data.directoryObjectType,
+ customDataAttribute: data.customDataAttribute,
+ tenantFilter: data.tenantFilter,
+ };
+ } else {
+ // Fallback to all data if source type is not recognized
+ filteredData = data;
+ }
+
addMappingApi.mutate({
url: "/api/ExecCustomData",
data: {
Action: "AddEditMapping",
- Mapping: data,
+ Mapping: filteredData,
},
});
};
diff --git a/src/pages/cipp/custom-data/mappings/edit.js b/src/pages/cipp/custom-data/mappings/edit.js
index a8a3cfcbee85..55a2bbc648e8 100644
--- a/src/pages/cipp/custom-data/mappings/edit.js
+++ b/src/pages/cipp/custom-data/mappings/edit.js
@@ -3,16 +3,7 @@ import { useForm, useFormState } from "react-hook-form";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall";
-import {
- Box,
- Button,
- Stack,
- CardContent,
- Typography,
- Divider,
- CardActions,
- Skeleton,
-} from "@mui/material";
+import { Button, Stack, CardContent, CardActions, Skeleton } from "@mui/material";
import CippPageCard from "/src/components/CippCards/CippPageCard";
import { CippApiResults } from "/src/components/CippComponents/CippApiResults";
@@ -42,11 +33,39 @@ const Page = () => {
});
const handleEditMapping = (data) => {
+ // Filter data based on source type to only include relevant fields
+ let filteredData;
+
+ if (data.sourceType?.value === "manualEntry") {
+ // For manual entry, only include these fields
+ filteredData = {
+ sourceType: data.sourceType,
+ manualEntryFieldLabel: data.manualEntryFieldLabel,
+ directoryObjectType: data.directoryObjectType,
+ customDataAttribute: data.customDataAttribute,
+ tenantFilter: data.tenantFilter,
+ };
+ } else if (data.sourceType?.value === "extensionSync") {
+ // For extension sync, include the original fields
+ filteredData = {
+ sourceType: data.sourceType,
+ extensionSyncDataset: data.extensionSyncDataset,
+ extensionSyncProperty: data.extensionSyncProperty,
+ directoryObjectType: data.directoryObjectType,
+ customDataAttribute: data.customDataAttribute,
+ tenantFilter: data.tenantFilter,
+ };
+ } else {
+ // Fallback to all data if source type is not recognized
+ filteredData = data;
+ }
+
editMappingApi.mutate({
url: "/api/ExecCustomData",
data: {
Action: "AddEditMapping",
- Mapping: { ...data, id }, // Include the ID for editing
+ id: id, // ID at top level for PowerShell function
+ Mapping: filteredData,
},
});
};
diff --git a/src/pages/cipp/custom-data/mappings/index.js b/src/pages/cipp/custom-data/mappings/index.js
index 8992accda6f0..7579685d1f62 100644
--- a/src/pages/cipp/custom-data/mappings/index.js
+++ b/src/pages/cipp/custom-data/mappings/index.js
@@ -1,7 +1,7 @@
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material";
+import { Alert, Button, SvgIcon, Typography } from "@mui/material";
import { Add } from "@mui/icons-material";
import tabOptions from "../tabOptions";
import NextLink from "next/link";
diff --git a/src/pages/cipp/custom-data/schema-extensions/add.js b/src/pages/cipp/custom-data/schema-extensions/add.js
index 605d50ed4f15..07be1c409067 100644
--- a/src/pages/cipp/custom-data/schema-extensions/add.js
+++ b/src/pages/cipp/custom-data/schema-extensions/add.js
@@ -101,6 +101,7 @@ const Page = () => {
required: true,
placeholder:
"Enter a schema id (e.g. cippUser). The prefix is generated automatically after creation.",
+ disableVariables: true,
},
{
name: "description",
@@ -108,6 +109,7 @@ const Page = () => {
type: "textField",
required: true,
placeholder: "Enter a description for the schema extension",
+ disableVariables: true,
},
{
name: "status",
diff --git a/src/pages/cipp/custom-data/schema-extensions/index.js b/src/pages/cipp/custom-data/schema-extensions/index.js
index 07b185e5ce21..daca34251209 100644
--- a/src/pages/cipp/custom-data/schema-extensions/index.js
+++ b/src/pages/cipp/custom-data/schema-extensions/index.js
@@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { TabbedLayout } from "/src/layouts/TabbedLayout";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material";
-import { Add, Block, CheckCircleOutline, Sync } from "@mui/icons-material";
+import { Add, Block, CheckCircleOutline } from "@mui/icons-material";
import tabOptions from "../tabOptions";
import { TrashIcon } from "@heroicons/react/24/outline";
import NextLink from "next/link";
@@ -40,6 +40,7 @@ const Page = () => {
label: "Property Name",
type: "textField",
required: true,
+ disableVariables: true,
},
{
name: "type",
diff --git a/src/pages/cipp/integrations/configure.js b/src/pages/cipp/integrations/configure.js
index ce853a005be7..3702ae108926 100644
--- a/src/pages/cipp/integrations/configure.js
+++ b/src/pages/cipp/integrations/configure.js
@@ -154,7 +154,13 @@ const Page = () => {
variant="contained"
color="primary"
onClick={() => handleIntegrationTest()}
- disabled={actionTestResults?.isLoading}
+ disabled={
+ actionTestResults?.isLoading ||
+ (extension?.SettingOptions?.find(
+ (setting) => setting?.name === `${extension.id}.Enabled`
+ ) &&
+ integrations?.data?.[extension.id]?.Enabled !== true)
+ }
>
@@ -169,7 +175,13 @@ const Page = () => {
variant="contained"
color="primary"
onClick={() => handleIntegrationSync()}
- disabled={actionSyncResults.isLoading}
+ disabled={
+ actionSyncResults.isLoading ||
+ (extension?.SettingOptions?.find(
+ (setting) => setting?.name === `${extension.id}.Enabled`
+ ) &&
+ integrations?.data?.[extension.id]?.Enabled !== true)
+ }
>
@@ -181,13 +193,12 @@ const Page = () => {
{extension?.links && (
<>
{extension.links.map((link, index) => (
-
+
diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js
index 141bebd8011d..89a33ee66448 100644
--- a/src/pages/cipp/integrations/index.js
+++ b/src/pages/cipp/integrations/index.js
@@ -7,7 +7,6 @@ import {
CardActions,
CardContent,
Container,
- Grid,
Skeleton,
Stack,
Typography,
@@ -17,6 +16,7 @@ import { Sync } from "@mui/icons-material";
import { useSettings } from "/src/hooks/use-settings";
import { ApiGetCall } from "/src/api/ApiCall";
import Link from "next/link";
+import { Grid } from "@mui/system";
const Page = () => {
const settings = useSettings();
@@ -67,7 +67,7 @@ const Page = () => {
}
return (
-
+ ,
+ color: "primary",
+ },
+];
const Page = () => {
const formControl = useForm({
defaultValues: {
startDate: null,
- toggleSwitch: false,
+ endDate: null,
+ username: "",
+ severity: [],
},
});
const [expanded, setExpanded] = useState(false); // State for Accordion
const [filterEnabled, setFilterEnabled] = useState(false); // State for filter toggle
- const [dateFilter, setDateFilter] = useState(null); // State for date filter
+ const [startDate, setStartDate] = useState(null); // State for start date filter
+ const [endDate, setEndDate] = useState(null); // State for end date filter
+ const [username, setUsername] = useState(null); // State for username filter
+ const [severity, setSeverity] = useState(null); // State for severity filter
+
+ // Watch date fields to show warning for large date ranges
+ const watchStartDate = formControl.watch("startDate");
+ const watchEndDate = formControl.watch("endDate");
+
+ // Component to display warning for large date ranges
+ const DateRangeWarning = () => {
+ if (!watchStartDate || !watchEndDate) return null;
+
+ const startDateMs = new Date(watchStartDate * 1000);
+ const endDateMs = new Date(watchEndDate * 1000);
+ const daysDifference = (endDateMs - startDateMs) / (1000 * 60 * 60 * 24);
+
+ if (daysDifference > 10) {
+ return (
+
+
+ You have selected a date range of {Math.ceil(daysDifference)} days. Large date ranges
+ may cause timeouts or errors due to the amount of data being processed. Consider
+ narrowing your date range if you encounter issues.
+
+
+ );
+ }
+
+ return null;
+ };
const onSubmit = (data) => {
- // Set filter states based on form submission
- setFilterEnabled(data.toggleSwitch);
- setDateFilter(
+ // Check if any filter is applied
+ const hasFilter =
+ data.startDate !== null ||
+ data.endDate !== null ||
+ data.username !== null ||
+ data.severity?.length > 0;
+ setFilterEnabled(hasFilter);
+
+ // Format start date if available
+ setStartDate(
data.startDate
? new Date(data.startDate * 1000).toISOString().split("T")[0].replace(/-/g, "")
: null
);
+
+ // Format end date if available
+ setEndDate(
+ data.endDate
+ ? new Date(data.endDate * 1000).toISOString().split("T")[0].replace(/-/g, "")
+ : null
+ );
+
+ // Set username filter if available
+ setUsername(data.username || null);
+
+ // Set severity filter if available (convert array to comma-separated string)
+ setSeverity(
+ data.severity && data.severity.length > 0
+ ? data.severity.map((item) => item.value).join(",")
+ : null
+ );
+
+ // Close the accordion after applying filters
+ setExpanded(false);
+ };
+
+ const clearFilters = () => {
+ formControl.reset({
+ startDate: null,
+ endDate: null,
+ username: "",
+ severity: [],
+ });
+ setFilterEnabled(false);
+ setStartDate(null);
+ setEndDate(null);
+ setUsername(null);
+ setSeverity(null);
+ setExpanded(false); // Close the accordion when clearing filters
};
return (
@@ -56,37 +145,157 @@ const Page = () => {
tableFilter={
setExpanded(!expanded)}>
}>
- Logbook Filters
+
+
+
+
+
+ Logbook Filters
+ {filterEnabled ? (
+
+ (
+ {startDate || endDate ? (
+ <>
+ {startDate
+ ? new Date(
+ startDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00"
+ ).toLocaleDateString()
+ : new Date().toLocaleDateString()}
+ {startDate && endDate ? " - " : ""}
+ {endDate
+ ? new Date(
+ endDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00"
+ ).toLocaleDateString()
+ : ""}
+ >
+ ) : null}
+ {username && (startDate || endDate) && " | "}
+ {username && <>User: {username}>}
+ {severity && (username || startDate || endDate) && " | "}
+ {severity && <>Severity: {severity.replace(/,/g, ", ")}>})
+
+ ) : (
+
+ (Today: {new Date().toLocaleDateString()})
+
+ )}
+
+
+ }
+ >
+ Apply Filters
+
+
+
+
+ }
+ onClick={clearFilters}
+ >
+ Clear Filters
+
+
@@ -96,23 +305,20 @@ const Page = () => {
title={pageTitle}
apiUrl={apiUrl}
simpleColumns={simpleColumns}
- queryKey={`Listlogs-${dateFilter}-${filterEnabled}`}
+ queryKey={`Listlogs-${startDate}-${endDate}-${username}-${severity}-${filterEnabled}`}
tenantInTitle={false}
apiData={{
- DateFilter: dateFilter, // Pass date filter from state
+ StartDate: startDate, // Pass start date filter from state
+ EndDate: endDate, // Pass end date filter from state
+ User: username, // Pass username filter from state
+ Severity: severity, // Pass severity filter from state
Filter: filterEnabled, // Pass filter toggle state
}}
+ actions={actions}
/>
);
};
-/* Comment to Developer:
- - The filter is inside an expandable Accordion. By default, the filter is collapsed.
- - The "Apply Filters" button sets the form data for date and filter toggle.
- - Form state is managed using react-hook-form, and the filter states are applied to the table.
- - The DateFilter is passed in 'YYYYMMDD' format, and Filter toggle is passed as a boolean.
-*/
-
Page.getLayout = (page) => {page};
export default Page;
diff --git a/src/pages/cipp/logs/logentry.js b/src/pages/cipp/logs/logentry.js
new file mode 100644
index 000000000000..3f9c1aef3ae0
--- /dev/null
+++ b/src/pages/cipp/logs/logentry.js
@@ -0,0 +1,137 @@
+import { useRouter } from "next/router";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { ApiGetCall } from "/src/api/ApiCall";
+import {
+ Button,
+ SvgIcon,
+ Box,
+ Container,
+ Chip,
+} from "@mui/material";
+import { Stack } from "@mui/system";
+import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
+import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard";
+import { CippInfoBar } from "/src/components/CippCards/CippInfoBar";
+import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton";
+
+const Page = () => {
+ const router = useRouter();
+ const { logentry } = router.query;
+
+ const logRequest = ApiGetCall({
+ url: `/api/Listlogs`,
+ data: {
+ logentryid: logentry,
+ },
+ queryKey: `GetLogEntry-${logentry}`,
+ waiting: !!logentry,
+ });
+
+ const handleBackClick = () => {
+ router.push("/cipp/logs");
+ };
+
+ // Get the log data from array
+ const logData = logRequest.data?.[0];
+
+ // Top info bar data like dashboard
+ const logInfo = logData ? [
+ { name: "Log ID", data: logData.RowKey },
+ { name: "Date & Time", data: new Date(logData.DateTime).toLocaleString() },
+ { name: "API", data: logData.API },
+ {
+ name: "Severity",
+ data: (
+
+ )
+ },
+ ] : [];
+
+ // Main log properties
+ const propertyItems = logData ? [
+ { label: "Tenant", value: logData.Tenant },
+ { label: "User", value: logData.User },
+ { label: "Message", value: logData.Message },
+ { label: "Tenant ID", value: logData.TenantID },
+ { label: "App ID", value: logData.AppId || "None" },
+ { label: "IP Address", value: logData.IP || "None" },
+ ] : [];
+
+ // LogData properties
+ const logDataItems = logData?.LogData && typeof logData.LogData === 'object'
+ ? Object.entries(logData.LogData).map(([key, value]) => ({
+ label: key,
+ value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value),
+ }))
+ : [];
+
+ return (
+
+
+
+ {/* Back button */}
+
+
+
+ }
+ sx={{ alignSelf: "flex-start" }}
+ >
+ Back to Logs
+
+
+ {logRequest.isLoading && }
+
+ {logRequest.isError && (
+
+ )}
+
+ {logRequest.isSuccess && logData && (
+ <>
+ {/* Top info bar like dashboard */}
+
+
+ {/* Main log information */}
+
+
+ {/* LogData in separate card */}
+ {logDataItems.length > 0 && (
+
+ )}
+ >
+ )}
+
+
+
+ );
+};
+
+Page.getLayout = (page) => {page};
+
+export default Page;
diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js
index 40ccac234133..011acfb4c724 100644
--- a/src/pages/cipp/preferences.js
+++ b/src/pages/cipp/preferences.js
@@ -1,28 +1,159 @@
import Head from "next/head";
import { Box, Container, Stack } from "@mui/material";
-import Grid from "@mui/material/Grid2";
+import { Grid } from "@mui/system";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard";
import CippFormComponent from "../../components/CippComponents/CippFormComponent";
-import { useForm } from "react-hook-form";
+import { useForm, useWatch } from "react-hook-form";
import { useSettings } from "../../hooks/use-settings";
import countryList from "../../data/countryList.json";
import { CippSettingsSideBar } from "../../components/CippComponents/CippSettingsSideBar";
import CippDevOptions from "/src/components/CippComponents/CippDevOptions";
+import { CippOffboardingDefaultSettings } from "../../components/CippComponents/CippOffboardingDefaultSettings";
import { ApiGetCall } from "../../api/ApiCall";
import { getCippFormatting } from "../../utils/get-cipp-formatting";
+import { useEffect, useState } from "react";
const Page = () => {
const settings = useSettings();
- const formcontrol = useForm({ mode: "onChange", defaultValues: settings });
+ const [initialUserType, setInitialUserType] = useState(null);
+
+ // Default portal links configuration
+ const defaultPortalLinks = {
+ M365_Portal: true,
+ Exchange_Portal: true,
+ Entra_Portal: true,
+ Teams_Portal: true,
+ Azure_Portal: true,
+ Intune_Portal: true,
+ SharePoint_Admin: true,
+ Security_Portal: true,
+ Compliance_Portal: true,
+ Power_Platform_Portal: true,
+ Power_BI_Portal: true,
+ };
const auth = ApiGetCall({
- url: "/.auth/me",
+ url: "/api/me",
queryKey: "authmecipp",
- staleTime: 120000,
- refetchOnWindowFocus: true,
});
+ const cleanedSettings = { ...settings };
+
+ if (cleanedSettings.offboardingDefaults?.keepCopy) {
+ delete cleanedSettings.offboardingDefaults.keepCopy;
+ settings.handleUpdate(cleanedSettings);
+ }
+
+ // Determine if we have user-specific settings and set initial user type
+ useEffect(() => {
+ if (cleanedSettings && auth.data?.clientPrincipal?.userDetails && initialUserType === null) {
+ const hasUserSpecificSettings =
+ cleanedSettings.UserSpecificSettings &&
+ Object.keys(cleanedSettings.UserSpecificSettings).length > 0;
+
+ setInitialUserType(hasUserSpecificSettings ? "currentUser" : "allUsers");
+ }
+ }, [cleanedSettings, auth.data?.clientPrincipal?.userDetails, initialUserType]);
+
+ // Set default portal links if they don't exist at global level
+ if (!cleanedSettings.portalLinks) {
+ cleanedSettings.portalLinks = defaultPortalLinks;
+ }
+
+ // Determine initial portal links based on user type
+ const getInitialPortalLinks = () => {
+ if (initialUserType === "currentUser" && cleanedSettings.UserSpecificSettings?.portalLinks) {
+ // Merge with defaults to ensure all keys exist
+ return { ...defaultPortalLinks, ...cleanedSettings.UserSpecificSettings.portalLinks };
+ }
+
+ // Use global settings or defaults
+ return { ...defaultPortalLinks, ...cleanedSettings.portalLinks };
+ };
+
+ // Set up initial form values with proper user selector default
+ const initialFormValues = {
+ ...cleanedSettings,
+ user:
+ initialUserType === "currentUser"
+ ? {
+ label: "Current User",
+ value: auth.data?.clientPrincipal?.userDetails || "currentUser",
+ }
+ : {
+ label: "All Users",
+ value: "allUsers",
+ },
+ portalLinks: getInitialPortalLinks(),
+ };
+
+ const formcontrol = useForm({
+ mode: "onChange",
+ defaultValues: initialFormValues,
+ });
+
+ // Watch the user selector to determine which settings to show
+ const selectedUser = useWatch({
+ control: formcontrol.control,
+ name: "user",
+ });
+
+ // Update form when initial user type is determined
+ useEffect(() => {
+ if (initialUserType !== null && auth.data?.clientPrincipal?.userDetails) {
+ const userValue =
+ initialUserType === "currentUser"
+ ? {
+ label: "Current User",
+ value: auth.data.clientPrincipal.userDetails,
+ }
+ : {
+ label: "All Users",
+ value: "allUsers",
+ };
+
+ const newFormValues = {
+ ...cleanedSettings,
+ user: userValue,
+ portalLinks: getInitialPortalLinks(),
+ };
+
+ // Reset the entire form with new values
+ formcontrol.reset(newFormValues);
+ }
+ }, [initialUserType, auth.data?.clientPrincipal?.userDetails]);
+
+ // Handle switching between user types
+ useEffect(() => {
+ if (selectedUser?.value && initialUserType !== null) {
+ const getPortalLinksForUserType = () => {
+ if (selectedUser.value === "allUsers") {
+ // Show global settings (root level)
+ return { ...defaultPortalLinks, ...cleanedSettings.portalLinks };
+ } else {
+ // Show user-specific settings if they exist, otherwise show global settings
+ const userSpecificLinks = cleanedSettings.UserSpecificSettings?.portalLinks;
+ const globalLinks = cleanedSettings.portalLinks;
+ return { ...defaultPortalLinks, ...globalLinks, ...userSpecificLinks };
+ }
+ };
+
+ const newPortalLinks = getPortalLinksForUserType();
+ const currentPortalLinks = formcontrol.getValues("portalLinks");
+
+ // Only update if the portal links actually changed
+ if (JSON.stringify(currentPortalLinks) !== JSON.stringify(newPortalLinks)) {
+ // Reset form with updated portal links but preserve other values
+ const currentValues = formcontrol.getValues();
+ formcontrol.reset({
+ ...currentValues,
+ portalLinks: newPortalLinks,
+ });
+ }
+ }
+ }, [selectedUser?.value, cleanedSettings, initialUserType]);
+
const addedAttributes = [
{ value: "consentProvidedForMinor", label: "consentProvidedForMinor" },
{ value: "employeeId", label: "employeeId" },
@@ -34,8 +165,6 @@ const Page = () => {
{ value: "officeLocation", label: "officeLocation" },
{ value: "otherMails", label: "otherMails" },
{ value: "showInAddressList", label: "showInAddressList" },
- { value: "state", label: "state" },
- { value: "city", label: "city" },
{ value: "sponsor", label: "sponsor" },
];
@@ -45,10 +174,64 @@ const Page = () => {
{ value: "100", label: "100" },
{ value: "250", label: "250" },
];
+
const languageListOptions = countryList.map((language) => {
return { value: language.Code, label: language.Name };
});
+ // Portal links configuration
+ const portalLinksConfig = [
+ {
+ name: "portalLinks.M365_Portal",
+ label: "M365 Portal",
+ },
+ {
+ name: "portalLinks.Exchange_Portal",
+ label: "Exchange Portal",
+ },
+ {
+ name: "portalLinks.Entra_Portal",
+ label: "Entra Portal",
+ },
+ {
+ name: "portalLinks.Teams_Portal",
+ label: "Teams Portal",
+ },
+ {
+ name: "portalLinks.Azure_Portal",
+ label: "Azure Portal",
+ },
+ {
+ name: "portalLinks.Intune_Portal",
+ label: "Intune Portal",
+ },
+ {
+ name: "portalLinks.SharePoint_Admin",
+ label: "SharePoint Admin",
+ },
+ {
+ name: "portalLinks.Security_Portal",
+ label: "Security Portal",
+ },
+ {
+ name: "portalLinks.Compliance_Portal",
+ label: "Compliance Portal",
+ },
+ {
+ name: "portalLinks.Power_Platform_Portal",
+ label: "Power Platform Portal",
+ },
+ {
+ name: "portalLinks.Power_BI_Portal",
+ label: "Power BI Portal",
+ },
+ ];
+
+ // Don't render until we've determined the initial user type
+ if (initialUserType === null || !auth.data?.clientPrincipal?.userDetails) {
+ return
This is a placeholder page for the edit standards section.
-
- );
-};
-
-Page.getLayout = (page) => {page};
-
-export default Page;
diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js
new file mode 100644
index 000000000000..a204fddb81b9
--- /dev/null
+++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js
@@ -0,0 +1,228 @@
+import { Alert, Button } from "@mui/material";
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative.
+import { TabbedLayout } from "/src/layouts/TabbedLayout";
+import Link from "next/link";
+import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub, ContentCopy } from "@mui/icons-material";
+import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall";
+import { Grid } from "@mui/system";
+import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults";
+import { EyeIcon } from "@heroicons/react/24/outline";
+import tabOptions from "../tabOptions.json";
+import { useSettings } from "/src/hooks/use-settings.js";
+import { CippPolicyImportDrawer } from "../../../../../components/CippComponents/CippPolicyImportDrawer.jsx";
+import { PermissionButton } from "/src/utils/permissions.js";
+
+const Page = () => {
+ const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" });
+ const integrations = ApiGetCall({
+ url: "/api/ListExtensionsConfig",
+ queryKey: "Integrations",
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const currentTenant = useSettings().currentTenant;
+ const pageTitle = "Templates";
+ const cardButtonPermissions = ["Tenant.Standards.ReadWrite"];
+ const actions = [
+ {
+ label: "View Tenant Report",
+ link: "/tenant/manage/applied-standards/?templateId=[GUID]",
+ icon: ,
+ color: "info",
+ target: "_self",
+ },
+ {
+ label: "Edit Template",
+ //when using a link it must always be the full path /identity/administration/users/[id] for example.
+ link: "/tenant/standards/template?id=[GUID]&type=[type]",
+ icon: ,
+ color: "success",
+ target: "_self",
+ },
+ {
+ label: "Clone & Edit Template",
+ link: "/tenant/standards/template?id=[GUID]&clone=true&type=[type]",
+ icon: ,
+ color: "success",
+ target: "_self",
+ },
+ {
+ label: "Create Drift Clone",
+ type: "POST",
+ url: "/api/ExecDriftClone",
+ icon: ,
+ color: "warning",
+ data: {
+ id: "GUID",
+ },
+ confirmText:
+ "Are you sure you want to create a drift clone of [templateName]? This will create a new drift template based on this template.",
+ multiPost: false,
+ },
+ {
+ label: `Run Template Now (${currentTenant || "Currently Selected Tenant"})`,
+ type: "GET",
+ url: "/api/ExecStandardsRun",
+ icon: ,
+ data: {
+ TemplateId: "GUID",
+ },
+ confirmText: "Are you sure you want to force a run of this template?",
+ multiPost: false,
+ },
+ {
+ label: "Run Template Now (All Tenants in Template)",
+ type: "GET",
+ url: "/api/ExecStandardsRun",
+ icon: ,
+ data: {
+ TemplateId: "GUID",
+ tenantFilter: "allTenants",
+ },
+ confirmText: "Are you sure you want to force a run of this template?",
+ multiPost: false,
+ },
+ {
+ label: "Save to GitHub",
+ type: "POST",
+ url: "/api/ExecCommunityRepo",
+ icon: ,
+ data: {
+ Action: "UploadTemplate",
+ GUID: "GUID",
+ },
+ fields: [
+ {
+ label: "Repository",
+ name: "FullName",
+ type: "select",
+ api: {
+ url: "/api/ListCommunityRepos",
+ data: {
+ WriteAccess: true,
+ },
+ queryKey: "CommunityRepos-Write",
+ dataKey: "Results",
+ valueField: "FullName",
+ labelField: "FullName",
+ },
+ multiple: false,
+ creatable: false,
+ required: true,
+ validators: {
+ required: { value: true, message: "This field is required" },
+ },
+ },
+ {
+ label: "Commit Message",
+ placeholder: "Enter a commit message for adding this file to GitHub",
+ name: "Message",
+ type: "textField",
+ multiline: true,
+ required: true,
+ rows: 4,
+ },
+ ],
+ confirmText: "Are you sure you want to save this template to the selected repository?",
+ condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
+ },
+ {
+ label: "Delete Template",
+ type: "POST",
+ url: "/api/RemoveStandardTemplate",
+ icon: ,
+ data: {
+ ID: "GUID",
+ },
+ confirmText: "Are you sure you want to delete [templateName]?",
+ multiPost: false,
+ },
+ ];
+ const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" });
+ const handleConversion = () => {
+ conversionApi.mutate({
+ url: "/api/execStandardConvert",
+ data: {},
+ });
+ };
+ const tableFilter = (
+
+ {oldStandards.isSuccess && oldStandards.data.length !== 0 && (
+
+
+
+
+ You have legacy standards available. Press the button to convert these standards to
+ the new format. This will create a new template for each standard you had, but will
+ disable the schedule. After conversion, please check the new templates to ensure
+ they are correct and re-enable the schedule.
+
+
+ handleConversion()} variant={"contained"}>
+ Convert Legacy Standards
+
+
+
+
+
+
+
+
+ )}
+
+ );
+ return (
+
+ } sx={{ mr: 1 }}>
+ Add Template
+
+ }
+ sx={{ mr: 1 }}
+ >
+ Create Drift Template
+
+
+ >
+ }
+ actions={actions}
+ tableFilter={tableFilter}
+ simpleColumns={[
+ "templateName",
+ "type",
+ "tenantFilter",
+ "excludedTenants",
+ "updatedAt",
+ "updatedBy",
+ "runManually",
+ "standards",
+ ]}
+ queryKey="listStandardTemplates"
+ />
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/tenant/standards/list-standards/drift-alignment/index.js b/src/pages/tenant/standards/list-standards/drift-alignment/index.js
new file mode 100644
index 000000000000..55c408abf622
--- /dev/null
+++ b/src/pages/tenant/standards/list-standards/drift-alignment/index.js
@@ -0,0 +1,44 @@
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { TabbedLayout } from "/src/layouts/TabbedLayout";
+import { EyeIcon } from "@heroicons/react/24/outline";
+import tabOptions from "../tabOptions.json";
+
+const Page = () => {
+ const pageTitle = "Drift Alignment";
+
+ const actions = [
+ {
+ label: "View Tenant Report",
+ link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]",
+ icon: ,
+ color: "info",
+ target: "_self",
+ },
+ ];
+
+ return (
+
+ );
+};
+
+Page.getLayout = (page) => (
+
+ {page}
+
+);
+
+export default Page;
diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js
index 2f0c84037842..28ad3abd2f4f 100644
--- a/src/pages/tenant/standards/list-standards/index.js
+++ b/src/pages/tenant/standards/list-standards/index.js
@@ -1,186 +1,70 @@
-import { Alert, Button } from "@mui/material";
+import { Button } from "@mui/material";
import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
-import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative.
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { TabbedLayout } from "/src/layouts/TabbedLayout";
import Link from "next/link";
-import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub } from "@mui/icons-material";
-import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall";
-import { Grid } from "@mui/system";
-import { CippApiResults } from "../../../../components/CippComponents/CippApiResults";
+import { Delete, Add } from "@mui/icons-material";
import { EyeIcon } from "@heroicons/react/24/outline";
+import tabOptions from "./tabOptions.json";
const Page = () => {
- const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" });
- const integrations = ApiGetCall({
- url: "/api/ListExtensionsConfig",
- queryKey: "Integrations",
- refetchOnMount: false,
- refetchOnReconnect: false,
- });
- const pageTitle = "Standard Templates";
+ const pageTitle = "Standard & Drift Alignment";
+
const actions = [
{
label: "View Tenant Report",
- link: "/tenant/standards/compare?templateId=[GUID]",
+ link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]",
icon: ,
color: "info",
target: "_self",
},
{
- label: "Edit Template",
- //when using a link it must always be the full path /identity/administration/users/[id] for example.
- link: "/tenant/standards/template?id=[GUID]",
- icon: ,
- color: "success",
- target: "_self",
- },
- {
- label: "Clone & Edit Template",
- link: "/tenant/standards/template?id=[GUID]&clone=true",
- icon: ,
- color: "success",
+ label: "Manage Drift",
+ link: "/tenant/manage/drift?templateId=[standardId]&tenantFilter=[tenantFilter]",
+ icon: ,
+ color: "info",
target: "_self",
+ condition: (row) => row.standardType === "drift",
},
{
- label: "Run Template Now (Currently Selected Tenant only)",
- type: "GET",
- url: "/api/ExecStandardsRun",
- icon: ,
- data: {
- TemplateId: "GUID",
- },
- confirmText: "Are you sure you want to force a run of this template?",
- multiPost: false,
- },
- {
- label: "Run Template Now (All Tenants in Template)",
- type: "GET",
- url: "/api/ExecStandardsRun",
- icon: ,
- data: {
- TemplateId: "GUID",
- tenantFilter: "allTenants",
- },
- confirmText: "Are you sure you want to force a run of this template?",
- multiPost: false,
- },
- {
- label: "Save to GitHub",
+ label: "Remove Drift Customization",
type: "POST",
- url: "/api/ExecCommunityRepo",
- icon: ,
- data: {
- Action: "UploadTemplate",
- GUID: "GUID",
- },
- fields: [
- {
- label: "Repository",
- name: "FullName",
- type: "select",
- api: {
- url: "/api/ListCommunityRepos",
- data: {
- WriteAccess: true,
- },
- queryKey: "CommunityRepos-Write",
- dataKey: "Results",
- valueField: "FullName",
- labelField: "FullName",
- },
- multiple: false,
- creatable: false,
- required: true,
- validators: {
- required: { value: true, message: "This field is required" },
- },
- },
- {
- label: "Commit Message",
- placeholder: "Enter a commit message for adding this file to GitHub",
- name: "Message",
- type: "textField",
- multiline: true,
- required: true,
- rows: 4,
- },
- ],
- confirmText: "Are you sure you want to save this template to the selected repository?",
- condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled,
- },
- {
- label: "Delete Template",
- type: "POST",
- url: "/api/RemoveStandardTemplate",
+ url: "/api/ExecUpdateDriftDeviation",
icon: ,
data: {
- ID: "GUID",
+ RemoveDriftCustomization: "true",
+ tenantFilter: "tenantFilter",
},
- confirmText: "Are you sure you want to delete [templateName]?",
+ confirmText:
+ "Are you sure you want to remove all drift customizations? This resets the Drift Standard to the default template, and will generate alerts for the drifted items.",
multiPost: false,
+ condition: (row) => row.standardType === "drift",
},
];
- const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" });
- const handleConversion = () => {
- conversionApi.mutate({
- url: "/api/execStandardConvert",
- data: {},
- });
- };
- const tableFilter = (
-
- {oldStandards.isSuccess && oldStandards.data.length !== 0 && (
-
-
-
-
- You have legacy standards available. Press the button to convert these standards to
- the new format. This will create a new template for each standard you had, but will
- disable the schedule. After conversion, please check the new templates to ensure
- they are correct and re-enable the schedule.
-
-
- handleConversion()} variant={"contained"}>
- Convert Legacy Standards
-
-
-
-
-
-
-
-
- )}
-
- );
+
return (
}>
- Add Template
-
- }
actions={actions}
- tableFilter={tableFilter}
simpleColumns={[
- "templateName",
"tenantFilter",
- "excludedTenants",
- "updatedAt",
- "updatedBy",
- "runManually",
- "standards",
+ "standardName",
+ "standardType",
+ "alignmentScore",
+ "LicenseMissingPercentage",
+ "combinedAlignmentScore",
]}
- queryKey="listStandardTemplates"
+ queryKey="listTenantAlignment"
/>
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => (
+
+ {page}
+
+);
export default Page;
diff --git a/src/pages/tenant/standards/list-standards/tabOptions.json b/src/pages/tenant/standards/list-standards/tabOptions.json
new file mode 100644
index 000000000000..1c522e6ca8ca
--- /dev/null
+++ b/src/pages/tenant/standards/list-standards/tabOptions.json
@@ -0,0 +1,10 @@
+[
+ {
+ "label": "Standard & Drift Alignment",
+ "path": "/tenant/standards/list-standards"
+ },
+ {
+ "label": "Templates",
+ "path": "/tenant/standards/list-standards/classic-standards"
+ }
+]
diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx
index 04c8b9b9c906..357b5d7edbab 100644
--- a/src/pages/tenant/standards/template.jsx
+++ b/src/pages/tenant/standards/template.jsx
@@ -1,34 +1,136 @@
import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from "@mui/material";
import { Grid } from "@mui/system";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
-import { useForm } from "react-hook-form";
+import { useForm, useWatch } from "react-hook-form";
import { useRouter } from "next/router";
-import { Add } from "@mui/icons-material";
-import { useEffect, useState } from "react";
+import { Add, SaveRounded } from "@mui/icons-material";
+import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from "react";
import standards from "/src/data/standards";
import CippStandardAccordion from "../../../components/CippStandards/CippStandardAccordion";
-import CippStandardDialog from "../../../components/CippStandards/CippStandardDialog";
+// Lazy load the dialog to improve initial page load performance
+const CippStandardDialog = lazy(() =>
+ import("../../../components/CippStandards/CippStandardDialog")
+);
import CippStandardsSideBar from "../../../components/CippStandards/CippStandardsSideBar";
import { ArrowLeftIcon } from "@mui/x-date-pickers";
-import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import { useDialog } from "../../../hooks/use-dialog";
import { ApiGetCall } from "../../../api/ApiCall";
+import _ from "lodash";
+import { createDriftManagementActions } from "../manage/driftManagementActions";
+import { ActionsMenu } from "/src/components/actions-menu";
+import { useSettings } from "/src/hooks/use-settings";
const Page = () => {
const router = useRouter();
const [editMode, setEditMode] = useState(false);
const formControl = useForm({ mode: "onBlur" });
+ const { formState } = formControl;
const [dialogOpen, setDialogOpen] = useState(false);
const [expanded, setExpanded] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
const [selectedStandards, setSelectedStandards] = useState({});
const [updatedAt, setUpdatedAt] = useState(false);
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
+ const [currentStep, setCurrentStep] = useState(0);
+ const [hasDriftConflict, setHasDriftConflict] = useState(false);
+ const initialStandardsRef = useRef({});
+
+ const currentTenant = useSettings().currentTenant;
+
+ // Check if this is drift mode
+ const isDriftMode = router.query.type === "drift";
+
+ // Set drift mode flag in form when in drift mode
+ useEffect(() => {
+ if (isDriftMode) {
+ formControl.setValue("isDriftTemplate", true);
+ }
+ }, [isDriftMode, formControl]);
+
+ // Watch form values to check valid configuration
+ const watchForm = useWatch({ control: formControl.control });
+
const existingTemplate = ApiGetCall({
url: `/api/listStandardTemplates`,
data: { id: router.query.id },
queryKey: `listStandardTemplates-${router.query.id}`,
waiting: editMode,
});
+
+ // Check if the template configuration is valid and update currentStep
+ useEffect(() => {
+ const stepsStatus = {
+ step1: !!_.get(watchForm, "templateName"),
+ step2: isDriftMode || _.get(watchForm, "tenantFilter", []).length > 0, // Skip tenant requirement for drift mode
+ step3: Object.keys(selectedStandards).length > 0,
+ step4:
+ _.get(watchForm, "standards") &&
+ Object.keys(selectedStandards).length > 0 &&
+ Object.keys(selectedStandards).every((standardName) => {
+ const standardValues = _.get(watchForm, standardName, {});
+ // Always require an action value which should be an array with at least one element
+ const actionValue = _.get(standardValues, "action");
+ return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0);
+ }),
+ };
+
+ const completedSteps = Object.values(stepsStatus).filter(Boolean).length;
+ setCurrentStep(completedSteps);
+ }, [selectedStandards, watchForm, isDriftMode]);
+
+ // Handle route change events
+ const handleRouteChange = useCallback(
+ (url) => {
+ if (hasUnsavedChanges) {
+ const confirmLeave = window.confirm(
+ "You have unsaved changes. Are you sure you want to leave this page?"
+ );
+ if (!confirmLeave) {
+ router.events.emit("routeChangeError");
+ throw "Route change was aborted";
+ }
+ }
+ },
+ [hasUnsavedChanges, router]
+ );
+
+ // Handle browser back/forward navigation or tab close
+ useEffect(() => {
+ const handleBeforeUnload = (e) => {
+ if (hasUnsavedChanges) {
+ e.preventDefault();
+ e.returnValue = "You have unsaved changes. Are you sure you want to leave this page?";
+ return e.returnValue;
+ }
+ };
+
+ // Add event listeners
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ router.events.on("routeChangeStart", handleRouteChange);
+
+ // Remove event listeners on cleanup
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ router.events.off("routeChangeStart", handleRouteChange);
+ };
+ }, [hasUnsavedChanges, handleRouteChange, router.events]);
+
+ // Track form changes
+ useEffect(() => {
+ // Compare the current form values with the initial values to check for real changes
+ const currentValues = formControl.getValues();
+ const initialValues = initialStandardsRef.current;
+
+ if (
+ formState.isDirty ||
+ JSON.stringify(selectedStandards) !== JSON.stringify(initialStandardsRef.current)
+ ) {
+ setHasUnsavedChanges(true);
+ } else {
+ setHasUnsavedChanges(false);
+ }
+ }, [formState.isDirty, selectedStandards, formControl]);
+
useEffect(() => {
if (router.query.id) {
setEditMode(true);
@@ -71,30 +173,40 @@ const Page = () => {
});
setSelectedStandards(transformedStandards);
+ // Store initial state for change detection
+ initialStandardsRef.current = { ...transformedStandards };
+ setHasUnsavedChanges(false);
}
}, [existingTemplate.isSuccess, router]);
- const categories = standards.reduce((acc, standard) => {
- const { cat } = standard;
- if (!acc[cat]) {
- acc[cat] = [];
- }
- acc[cat].push(standard);
- return acc;
- }, {});
+ // Memoize categories to avoid unnecessary recalculations
+ const categories = useMemo(() => {
+ return standards.reduce((acc, standard) => {
+ const { cat } = standard;
+ if (!acc[cat]) {
+ acc[cat] = [];
+ }
+ acc[cat].push(standard);
+ return acc;
+ }, {});
+ }, []);
+
+ const handleOpenDialog = useCallback(() => {
+ setDialogOpen(true);
+ }, []);
- const handleOpenDialog = () => setDialogOpen(true);
- const handleCloseDialog = () => {
+ const handleCloseDialog = useCallback(() => {
setDialogOpen(false);
setSearchQuery("");
- };
+ }, []);
const filterStandards = (standardsList) =>
standardsList.filter(
(standard) =>
- standard.label.toLowerCase().includes(searchQuery) ||
- standard.helpText.toLowerCase().includes(searchQuery) ||
- (standard.tag && standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery)))
+ standard.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ standard.helpText.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (standard.tag &&
+ standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())))
);
const handleToggleStandard = (standardName) => {
@@ -146,15 +258,37 @@ const Page = () => {
setExpanded((prev) => (prev === standardName ? null : standardName));
};
- const actions = [
- {
- label: "Save Template",
- handler: () => createDialog.handleOpen(),
- icon: ,
- },
- ];
const createDialog = useDialog();
+ // Save action that will open the create dialog
+ const handleSave = () => {
+ createDialog.handleOpen();
+ // Will be set to false after successful save in the dialog component
+ };
+
+ // Determine if save button should be disabled based on configuration
+ const isSaveDisabled = isDriftMode
+ ? currentStep < 3 || hasDriftConflict // For drift mode, only require steps 1, 3, and 4 (skip tenant requirement) and no drift conflicts
+ : !_.get(watchForm, "tenantFilter") ||
+ !_.get(watchForm, "tenantFilter").length ||
+ currentStep < 3;
+
+ // Create drift management actions (excluding refresh)
+ const driftActions = useMemo(() => {
+ if (!editMode || !router.query.id) return [];
+
+ const allActions = createDriftManagementActions({
+ templateId: router.query.id,
+ onRefresh: () => {}, // Empty function since we're filtering out refresh
+ currentTenant: currentTenant,
+ });
+
+ // Filter out the refresh action
+ return allActions.filter((action) => action.label !== "Refresh Data");
+ }, [editMode, router.query.id, currentTenant]);
+
+ const actions = [];
+
const steps = [
"Set a name for the Template",
"Assigned Template to Tenants",
@@ -162,14 +296,33 @@ const Page = () => {
"Configured all Standards",
];
+ const handleSafeNavigation = (url) => {
+ if (hasUnsavedChanges) {
+ const confirmLeave = window.confirm(
+ "You have unsaved changes. Are you sure you want to leave this page?"
+ );
+ if (confirmLeave) {
+ router.push(url);
+ }
+ } else {
+ router.push(url);
+ }
+ };
+
return (
-
+
-
+ router.back()}
+ onClick={() =>
+ hasUnsavedChanges
+ ? window.confirm(
+ "You have unsaved changes. Are you sure you want to leave this page?"
+ ) && router.back()
+ : router.back()
+ }
startIcon={
@@ -187,64 +340,105 @@ const Page = () => {
sx={{ mb: 3 }}
>
- {editMode ? "Edit Standards Template" : "Add Standards Template"}
+ {editMode
+ ? isDriftMode
+ ? "Edit Drift Template"
+ : "Edit Standards Template"
+ : isDriftMode
+ ? "Add Drift Template"
+ : "Add Standards Template"}
- }
- >
- Add Standard to this template
-
+
+ }
+ disabled={isSaveDisabled}
+ >
+ Save Template
+
+ }
+ >
+ Add Standard to Template
+
+ {/* Drift management actions */}
+ {driftActions.length > 0 && (
+
+ )}
+
-
- {/* Left Column for Accordions */}
-
-
-
-
-
- {/* Show accordions based on selectedStandards (which is populated by API when editing) */}
- {existingTemplate.isLoading ? (
-
- ) : (
-
- )}
-
+
+
+ {/* Left Column for Accordions */}
+
+ {
+ // Reset unsaved changes flag
+ setHasUnsavedChanges(false);
+ // Update reference for future change detection
+ initialStandardsRef.current = { ...selectedStandards };
+ }}
+ />
+
+
+
+ {/* Show accordions based on selectedStandards (which is populated by API when editing) */}
+ {existingTemplate.isLoading ? (
+
+ ) : (
+
+ )}
+
+
-
+
-
+ {/* Only render the dialog when it's needed */}
+ {dialogOpen && (
+ }>
+
+
+ )}
);
diff --git a/src/pages/tenant/standards/tenant-alignment/index.js b/src/pages/tenant/standards/tenant-alignment/index.js
new file mode 100644
index 000000000000..e891f2a0576d
--- /dev/null
+++ b/src/pages/tenant/standards/tenant-alignment/index.js
@@ -0,0 +1,38 @@
+import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx";
+import { Layout as DashboardLayout } from "/src/layouts/index.js";
+import { EyeIcon } from "@heroicons/react/24/outline";
+
+const Page = () => {
+ const pageTitle = "Tenant Alignment";
+
+ const actions = [
+ {
+ label: "View Tenant Report",
+ link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]",
+ icon: ,
+ color: "info",
+ target: "_self",
+ },
+ ];
+
+ return (
+
+ );
+};
+
+Page.getLayout = (page) => {page};
+
+export default Page;
diff --git a/src/pages/tenant/tools/appapproval/index.js b/src/pages/tenant/tools/appapproval/index.js
index 10067259d287..a18484cc85ce 100644
--- a/src/pages/tenant/tools/appapproval/index.js
+++ b/src/pages/tenant/tools/appapproval/index.js
@@ -2,7 +2,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippWizardConfirmation } from "/src/components/CippWizard/CippWizardConfirmation";
import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx";
import { CippTenantStep } from "/src/components/CippWizard/CippTenantStep.jsx";
-import { useSettings } from "../../../../hooks/use-settings";
import { CippWizardAppApproval } from "../../../../components/CippWizard/CippWizardAppApproval";
import { Alert } from "@mui/material";
@@ -38,7 +37,7 @@ const Page = () => {
return (
<>
{
>
-
+
-
+ {
required
/>
-
+ setIpAddress(ip)}
@@ -122,19 +122,19 @@ const Page = () => {
-
+
{/* Results Card */}
{ipAddress && (
-
+
-
+
-
+
Add to Whitelist
@@ -149,7 +149,7 @@ const Page = () => {
)}
-
+ {
@@ -15,7 +15,7 @@ const Page = () => {
-
+
diff --git a/src/pages/tenant/tools/individual-domains/index.js b/src/pages/tenant/tools/individual-domains/index.js
index 3b209769c809..0b8332fc2d4a 100644
--- a/src/pages/tenant/tools/individual-domains/index.js
+++ b/src/pages/tenant/tools/individual-domains/index.js
@@ -1,4 +1,4 @@
-import { Box, Container, Grid } from "@mui/material";
+import { Box, Container } from "@mui/material";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { CippDomainCards } from "../../../../components/CippCards/CippDomainCards";
diff --git a/src/pages/tenant/tools/tenantlookup/index.js b/src/pages/tenant/tools/tenantlookup/index.js
index ff8d73863b50..c71a22f697db 100644
--- a/src/pages/tenant/tools/tenantlookup/index.js
+++ b/src/pages/tenant/tools/tenantlookup/index.js
@@ -1,13 +1,5 @@
-import {
- Box,
- Button,
- Container,
- Grid,
- Typography,
- CircularProgress,
- Skeleton,
- Link,
-} from "@mui/material";
+import { Box, Button, Container, Typography, Skeleton, Link } from "@mui/material";
+import { Grid } from "@mui/system";
import { Layout as DashboardLayout } from "/src/layouts/index.js";
import { useForm, useWatch } from "react-hook-form";
import CippButtonCard from "../../../../components/CippCards/CippButtonCard";
@@ -34,13 +26,13 @@ const Page = () => {
>
-
+
-
+ {
required
/>
-
+ getTenant.refetch()}
@@ -65,20 +57,20 @@ const Page = () => {
{/* Results Card */}
{getTenant.isFetching ? (
-
+
-
+
) : getTenant.data ? (
-
+
-
+ Tenant Name: {domain}
@@ -96,20 +88,6 @@ const Page = () => {
: "N/A"}
-
-
- domains:
-
-
- {getTenant.data?.Domains?.map((domain) => (
-