diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
deleted file mode 100644
index 9aefa91fa76..00000000000
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-name: Bug report
-about: Report errors or unexpected behavior
-labels: 'bug', 'Needs: Triage :mag:'
----
-
-**Description of the issue**
-
-
-
-**To Reproduce**
-
-Steps to reproduce the issue:
-
-```powershell
-
-```
-
-**Expected behaviour**
-
-
-
-**Error output**
-
-
-
-```text
-
-```
-
-**Module in use and version:**
-
-- Module: PSRule.Rules.Azure
-- Version: **[e.g. 1.29.0]**
-
-Captured output from `$PSVersionTable`:
-
-```text
-
-```
-
-**Additional context**
-
-
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml
index cc5b48de883..3c5a8ec8485 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yaml
@@ -1,6 +1,7 @@
name: Bug report
description: Report errors or unexpected behavior
title: '[BUG]
'
+type: Bug
labels: ['bug', 'Needs: Triage :mag:']
body:
- type: input
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
deleted file mode 100644
index 7df082474aa..00000000000
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-name: Feature request
-about: Suggest an idea
-labels: 'enhancement', 'Needs: Triage :mag:'
----
-
-**Is your feature request related to a problem? Please describe.**
-
-
-
-**Describe the solution you'd like**
-
-
-
-**Describe alternatives you've considered**
-
-
-
-**Additional context**
-
-
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml
index 47919bb40ba..932cf996c4c 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yaml
@@ -1,6 +1,7 @@
name: Feature request
description: Suggest an idea
title: '[FEATURE] '
+type: Feature
labels: ['enhancement', 'Needs: Triage :mag:']
body:
- type: textarea
diff --git a/.github/ISSUE_TEMPLATE/rule-request.yaml b/.github/ISSUE_TEMPLATE/rule-request.yaml
index b32f268971b..4166494846a 100644
--- a/.github/ISSUE_TEMPLATE/rule-request.yaml
+++ b/.github/ISSUE_TEMPLATE/rule-request.yaml
@@ -1,6 +1,7 @@
name: Rule request
description: Suggest the creation of a new or to update an existing rule
title: '[RULE] '
+type: Feature
labels: ['rule', 'Needs: Triage :mag:']
body:
- type: input
diff --git a/.github/ISSUE_TEMPLATE/sample_proposal.yaml b/.github/ISSUE_TEMPLATE/sample_proposal.yaml
new file mode 100644
index 00000000000..965ef4289c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/sample_proposal.yaml
@@ -0,0 +1,43 @@
+name: Sample proposal
+description: Make a proposal for the community to add a new sample
+title: '[Sample]: '
+type: Feature
+labels: ['sample', 'Needs: Triage :mag:']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Fill out this form below to suggest a new sample to be added by the community.
+ - type: dropdown
+ id: exiting-sample-check
+ attributes:
+ label: |
+ Have you checked this sample does not already exist in the repository?
+ Samples are stored in `samples/` in the root of the repository.
+ options:
+ - 'Yes'
+ - 'No'
+ validations:
+ required: true
+ - type: textarea
+ id: why-the-sample
+ attributes:
+ label: Why is the sample needed?
+ description: Explain why the sample is needed. If there is a existing sample, explain why it cannot be updated to meet your needs and why a new one must be created.
+ validations:
+ required: true
+ - type: input
+ id: sample-path
+ attributes:
+ label: Sample path
+ description: The path to the new sample.
+ placeholder: samples/rules/name
+ validations:
+ required: true
+ - type: textarea
+ id: sample-description
+ attributes:
+ label: Describe the sample
+ description: A clear and concise description of the sample.
+ validations:
+ required: true
diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md
index bc6a1fb5afb..f22cf908c6b 100644
--- a/docs/CHANGELOG-v1.md
+++ b/docs/CHANGELOG-v1.md
@@ -29,6 +29,10 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
## Unreleased
+- Engineering:
+ - Migrated Azure samples into PSRule for Azure by @BernieWhite.
+ [#3085](https://github.com/Azure/PSRule.Rules.Azure/issues/3085)
+
## v1.39.1
What's changed since v1.39.0:
diff --git a/docs/hooks/samples.py b/docs/hooks/samples.py
new file mode 100644
index 00000000000..1b8f960b73f
--- /dev/null
+++ b/docs/hooks/samples.py
@@ -0,0 +1,130 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# NOTES:
+# This file implements generation of samples TOC.
+
+import logging
+import os
+import re
+
+from mkdocs.config.defaults import MkDocsConfig
+from mkdocs.structure.files import Files
+from mkdocs.structure.pages import Page
+
+log = logging.getLogger(f"mkdocs")
+
+#
+# Hooks
+#
+
+def on_pre_build(config: MkDocsConfig):
+ '''Hook on_pre_build event.'''
+
+ return generate_samples_toc_fragment(config)
+
+def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files: Files) -> str:
+ '''Hook on_page_markdown event.'''
+
+ return samples_shortcode(markdown, page, config, files)
+
+#
+# Supporting functions
+#
+
+def generate_samples_toc_fragment(config: MkDocsConfig):
+ '''Generate a markdown fragment that will be injected into files containing a short code.'''
+
+ # Get the samples directory which is in a parent directory.
+ repo_root_dir = os.path.join(config.docs_dir, "..")
+ samples_dir = os.path.join(repo_root_dir, "samples", "rules")
+
+ # Get the base repo URI.
+ base_repo_uri = config.repo_url
+ samples_repo_uri = f"{base_repo_uri}tree/main/samples/rules"
+
+ # Generate the TOC fragment, each sample exists as a README.md file in a subdirectory.
+ toc = []
+ for root, dirs, _ in os.walk(samples_dir):
+ for dir in dirs:
+ if dir == "common":
+ continue
+
+ current_sample_dir = os.path.join(root, dir)
+ title = ""
+ description = []
+ author = ""
+ block = "none"
+
+ # Read the file to get the title and lines until the first empty line.
+ with open(os.path.join(current_sample_dir, "README.md"), "r") as f:
+
+ # Read annotations and header.
+ for line in f:
+ if (block == "none" or block == "metadata") and line.strip() == "---":
+ if block == "none":
+ block = "metadata"
+ elif block == "metadata":
+ block = "header"
+
+ continue
+
+ if block == "metadata" and line.startswith("author:") and line.strip("author:").strip() != "":
+ author = line.strip("author:").strip()
+ author = f"@{author}"
+ continue
+
+ if block == "metadata":
+ continue
+
+ if block == "header" or line.startswith("# "):
+ if line.startswith("# "):
+ title = line.strip("# ").strip()
+ block = "header"
+ continue
+
+ if line.strip() == "":
+ continue
+
+ # Keep reading until the first H2 heading.
+ if line.startswith("## "):
+ break
+
+ description.append(line.strip())
+
+ # Write the TOC entry as a row in a markdown table.
+ toc.append(f"| [{title}]({'/'.join([samples_repo_uri, dir])}) | {' '.join(description)} | {author} |")
+
+ # Write the TOC to a markdown file in a table with title and description.
+ toc_file = os.path.join(repo_root_dir, "out", "samples_toc.md")
+ with open(toc_file, "w") as f:
+ f.write("| Title | Description | Author |\n")
+ f.write("| ----- | ----------- | ------ |\n")
+ for entry in toc:
+ f.write(f"{entry}\n")
+
+def samples_shortcode(markdown: str, page: Page, config: MkDocsConfig, files: Files) -> str:
+ '''Replace samples shortcodes in markdown.'''
+
+ # Callback for regular expression replacement.
+ def replace(match: re.Match) -> str:
+ type, args = match.groups()
+ args = args.strip()
+ if type == "rules":
+ return _samples_rules_fragment(args, page, config, files)
+
+ raise RuntimeError(f"Unknown shortcode samples:{type}")
+
+ # Replace samples shortcodes.
+ return re.sub(
+ r"",
+ replace, markdown, flags = re.I | re.M
+ )
+
+def _samples_rules_fragment(args: str, page: Page, config: MkDocsConfig, files: Files) -> str:
+ '''Replace samples shortcode with rules fragment.'''
+
+ # Get the TOC fragment from the file.
+ toc_file = os.path.join(config.docs_dir, "..", "out", "samples_toc.md")
+ with open(toc_file, "r") as f:
+ return f.read()
diff --git a/docs/samples.md b/docs/samples.md
index 767e56449b6..4828ef2e016 100644
--- a/docs/samples.md
+++ b/docs/samples.md
@@ -1,7 +1,8 @@
---
-reviewed: 2023-04-23
+reviewed: 2024-10-10
author: BernieWhite
discussion: false
+link_users: true
---
# Samples
@@ -22,11 +23,11 @@ This repository contains the following samples for PSRule for Azure:
[1]: https://aka.ms/ps-rule-azure-quickstart
-## PSRule samples
+## Community samples
-[:octicons-repo-24: Samples][2]
+### Custom rules and conventions
-A community collection of samples for PSRule.
-This repository includes samples for Azure as well as other use cases.
+The following sample rules and conventions for Azure are available.
+You can use these samples as a starting point for developing your own custom rules.
- [2]: https://github.com/microsoft/PSRule-samples
+
diff --git a/mkdocs.yml b/mkdocs.yml
index 7d5360fa599..a549552d271 100755
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -154,6 +154,7 @@ plugins:
setup/configuring-options.md: setup/index.md
hooks:
+ - docs/hooks/samples.py
- docs/hooks/shortcodes.py
- docs/hooks/metadata.py
- docs/hooks/aliases.py
diff --git a/samples/README.md b/samples/README.md
new file mode 100644
index 00000000000..15a479b4ddd
--- /dev/null
+++ b/samples/README.md
@@ -0,0 +1,49 @@
+# Samples
+
+You have reached the Azure community samples for PSRule.
+If you have a question about these samples, please start a discussion on GitHub.
+
+These samples are broken into the following categories:
+
+- `rules/` - Sample rules not shipped with PSRule for Azure.
+ These samples do no align to Azure Well-Architected Framework (WAF),
+ and should be considered as a starting point for custom rules.
+
+## Contributing samples
+
+Additional samples can be contributed.
+Please use the following structure for your `README.md`.
+Replace the comment placeholders with details about your sample.
+
+```markdown
+---
+author:
+---
+
+#
+
+
+
+## Summary
+
+
+
+## Usage
+
+
+
+## References
+
+
+
+- [Using custom rules](https://azure.github.io/PSRule.Rules.Azure/customization/using-custom-rules/)
+- [Conventions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Conventions/#including-with-options)
+```
+
+When contributing a sample:
+
+- README.md:
+ - Please update `author:` with your GitHub username to be credited in the table of contents.
+ - Please give the sample a title.
+- Store each sample in a unique folder. i.e. `samples//`.
+- Prefix your sample rule or convention files with your folder name. i.e. `.Rule.ps1`.
diff --git a/samples/rules/APIManagementPolicy/APIManagementPolicy.Rule.ps1 b/samples/rules/APIManagementPolicy/APIManagementPolicy.Rule.ps1
new file mode 100644
index 00000000000..556fa195dac
--- /dev/null
+++ b/samples/rules/APIManagementPolicy/APIManagementPolicy.Rule.ps1
@@ -0,0 +1,22 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# Synopsis: Imports API Management policy XML files in for analysis.
+Export-PSRuleConvention 'APIManagementPolicy.Import' -Initialize {
+ $policies = @(Get-ChildItem -Path 'policies/' -Include '*.xml' -Recurse -File | ForEach-Object {
+ $name = $_.Name
+ [PSCustomObject]@{
+ Name = $name
+ Content = [Xml](Get-Content -Path $_.FullName -Raw)
+ }
+ })
+ $PSRule.ImportWithType('Azure.APIM.PolicyContent', $policies)
+}
+
+# Synopsis: Checks that validate-jwt element exists in the policy.
+Rule 'APIManagementPolicy.ValidateJwt' -Type 'Azure.APIM.PolicyContent' {
+ $policy = @([Xml]$TargetObject.Content.SelectNodes('//validate-jwt'))
+
+ # Check that validate-jwt is used in the policy.
+ $Assert.Greater($policy, '.', 1)
+}
diff --git a/samples/rules/APIManagementPolicy/README.md b/samples/rules/APIManagementPolicy/README.md
new file mode 100644
index 00000000000..2ca9a97eed1
--- /dev/null
+++ b/samples/rules/APIManagementPolicy/README.md
@@ -0,0 +1,23 @@
+---
+author: BernieWhite
+---
+
+# API Management Policy
+
+Discover and test API Management `.xml` policy files.
+
+## Summary
+
+This sample discovers raw API Management policy `.xml` files with a convention.
+Once discovered, custom rules can be written to validate the policy by inspecting the XML.
+The custom rule `APIManagementPolicy.ValidateJwt` then checks that `validate-jwt` element exists in the policy.
+
+## Usage
+
+- Include the `APIManagementPolicy.Import` convention.
+- Store policy XML files in the `policies/` subdirectory.
+
+## References
+
+- [Using custom rules](https://azure.github.io/PSRule.Rules.Azure/customization/using-custom-rules/)
+- [Conventions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Conventions/#including-with-options)
diff --git a/samples/rules/BicepModuleRequires/BicepModuleRequires.Rule.ps1 b/samples/rules/BicepModuleRequires/BicepModuleRequires.Rule.ps1
new file mode 100644
index 00000000000..25e24703d4d
--- /dev/null
+++ b/samples/rules/BicepModuleRequires/BicepModuleRequires.Rule.ps1
@@ -0,0 +1,27 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# Synopsis: Imports in Bicep module paths for analysis.
+Export-PSRuleConvention 'BicepModuleRequires.Import' -Initialize {
+ # Find modules in the 'modules' directory and import them into PSRule as custom objects.
+ $modules = @(Get-ChildItem -Path 'modules/' -Include 'main.bicep' -Recurse -File | ForEach-Object {
+ $version = $_.Directory.Name
+ $name = $_.Directory.Parent.Name
+ [PSCustomObject]@{
+ Name = $name
+ Version = $version
+ Path = $_.Directory.FullName
+ }
+ })
+ $PSRule.ImportWithType('Azure.Bicep.ModuleInfo', $modules);
+}
+
+# Synopsis: A Bicep module must have a corresponding README file.
+Rule 'BicepModuleRequires.RequireReadme' -Type 'Azure.Bicep.ModuleInfo' {
+ $Assert.FilePath($TargetObject, 'Path', @('README.md'))
+}
+
+# Synopsis: A Bicep module must have a corresponding tests file.
+Rule 'BicepModuleRequires.RequireTests' -Type 'Azure.Bicep.ModuleInfo' {
+ $Assert.FilePath($TargetObject, 'Path', @('.tests/main.tests.bicep'))
+}
diff --git a/samples/rules/BicepModuleRequires/README.md b/samples/rules/BicepModuleRequires/README.md
new file mode 100644
index 00000000000..3dba7bb8ed5
--- /dev/null
+++ b/samples/rules/BicepModuleRequires/README.md
@@ -0,0 +1,30 @@
+---
+author: BernieWhite
+---
+
+# Bicep Module Requires
+
+Discover and test Bicep modules based on a directory structure convention.
+
+## Summary
+
+This sample discovers Bicep modules within a sub-directory such as `modules/` using a convention.
+Once discovered, custom rules can be used to validate the module.
+
+For example:
+
+- Does the module have a readme file? Checked using `BicepModuleRequires.RequireReadme`.
+- Have tests been created for the module? Checked using `BicepModuleRequires.RequireTests`.
+
+## Usage
+
+- Include the `BicepModuleRequires.Import` convention.
+- Create modules in the `modules/` sub-directory.
+- Store your modules in major versioned sub-directories.
+ i.e. `modules///main.bicep`
+ e.g. `modules/storage/v1/main.bicep`.
+
+## References
+
+- [Using custom rules](https://azure.github.io/PSRule.Rules.Azure/customization/using-custom-rules/)
+- [Conventions](https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Conventions/#including-with-options)