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.** - -<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] --> - -**Describe the solution you'd like** - -<!-- A clear and concise description of what you want to happen. --> - -**Describe alternatives you've considered** - -<!-- A clear and concise description of any alternative solutions or features you've considered. --> - -**Additional context** - -<!-- Add any other context or screenshots about the feature request here. --> 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] <title>' +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] <title>' +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"<!-- samples:(\w+)(.*?) -->", + 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 +<!-- samples:rules --> 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: <github_username> +--- + +# <title> + +<!-- a short one line description which will be included in table of contents --> + +## Summary + +<!-- describe what your sample does --> + +## Usage + +<!-- how to use your sample --> + +## References + +<!-- references to docs that help explain the detail. here is a few to get started, but remove if they are not relevant. --> + +- [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/<category>/<your_sample>`. +- Prefix your sample rule or convention files with your folder name. i.e. `<your_sample>.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/<moduleName>/<version>/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)