Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Versioning

This repository uses Semantic Versioning.

For deployable or user-visible changes:
- Update the root `VERSION` file using `X.Y.Z` format only.
- Update `CHANGELOG.md` under `## [Unreleased]` using Keep a Changelog categories: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
- Choose the bump level as follows:
- `MAJOR` for breaking changes or required migration.
- `MINOR` for backward-compatible features or enhancements.
- `PATCH` for bug fixes, documentation updates, refactors, and non-breaking maintenance.
- If the correct bump level is ambiguous, ask before changing `VERSION`.
- Do not describe or rely on versioning behaviors that are not implemented in this repository.

When making version-related edits, keep changelog entries user-facing rather than implementation-focused.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.

## [Unreleased]

### Added
- Semantic versioning scaffolding with a root `VERSION` file and changelog tracking.

### Fixed
- Executive summary counts now update locally when accepted remediation controls are excluded from active improvement opportunities.
- Top 5 Remediation Actions now keeps its active-item counts and empty-state message in sync with accepted controls.
- Accepted controls now expose a clear "Undo acceptance" action to move a control back into the active remediation list.

## [0.1.0] - 2026-05-19

### Added
- Initial release baseline for ADOQR.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ adopting the recommended best practices.
| **Affected areas** | Which projects (or Organization) are impacted |
| **Example finding** | A representative finding from the review |
| **How to adopt** | Expandable step-by-step instructions (click to reveal) |
| **Accept risk** | A justification text box that lets reviewers move a control into the **Accepted Controls** tab when the business accepts the risk |
| **Documentation link** | Direct link to the relevant Microsoft Learn page |

### Using the Remediation Plan
Expand All @@ -275,6 +276,11 @@ adopting the recommended best practices.
typically resolve 50–80% of all items.
- Click **"How to adopt"** on any card to expand the numbered steps showing
exactly where to navigate in Azure DevOps and what to change.
- If a control has an approved exception, click **"Accept risk"**, enter the
business justification, and the card will move to the **Accepted Controls**
tab with the recorded acceptance date. Accepted controls are stored per
organization in the browser and reused by future remediation reports opened
in the same browser.
- After applying changes, re-run adoqr to verify the items are resolved.

![sample remediation plan](assets/remediation_steps.png)
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.1.3
935 changes: 834 additions & 101 deletions invoke-adoqr.ps1

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions tests/ExecutiveHtml.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,72 @@ Describe 'Build-NotCheckedSectionHtml' {
# Must not be open by default (no `open` attribute on the outer details)
$html | Should -Not -Match '<details[^>]*\bopen\b[^>]*class="[^"]*\bsection-collapsible\b'
}
}

Describe 'Write-ExecutiveHtmlReport' {
It 'embeds accepted-risk recalculation hooks for the executive summary' {
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $tempDir | Out-Null
$filePath = Join-Path $tempDir 'executive.html'

$orgSummary = [PSCustomObject]@{
Pass = 1
Fail = 1
NotChecked = 0
ReportFile = 'org.md'
Results = @(
(New-Result 'AUTH-01' 'PASS' 'High' 'AAD Authentication' 'Organization is Azure AD backed.'),
(New-Result 'AUTH-05' 'FAIL' 'Medium' 'Conditional Access Policy' 'Conditional access is not enforced.')
)
}

$projectSummary = [PSCustomObject]@{
Project = 'Web'
Pass = 0
Fail = 1
NotChecked = 1
ReportFile = 'web.md'
Results = @(
(New-Result 'REPO-01' 'FAIL' 'High' 'Inactive Repositories' 'Repository is stale.'),
(New-Result 'AUDIT-01' 'NOT CHECKED' 'Medium' 'Audit Log Backup' 'Manual review required.')
)
}

$remediations = @(
[PSCustomObject]@{
ControlId = 'AUTH-05'
ControlName = 'Conditional Access Policy'
Severity = 'Medium'
Count = 1
AffectedAreas = @('Organization')
Finding = 'Conditional access is not enforced.'
},
[PSCustomObject]@{
ControlId = 'REPO-01'
ControlName = 'Inactive Repositories'
Severity = 'High'
Count = 1
AffectedAreas = @('Project: Web')
Finding = 'Repository is stale.'
}
)

try {
Write-ExecutiveHtmlReport -FilePath $filePath -OrgName 'Contoso' -OrgUrl 'https://dev.azure.com/Contoso' -ElapsedTime '00:00:05' -OrgSummary $orgSummary -ProjectSummaries @($projectSummary) -TopRemediations $remediations

$html = Get-Content -Path $filePath -Raw
$html | Should -Match 'adoqr\.acceptedControls\.Contoso'
$html | Should -Match 'window\.__adoqrCurrentRunControls\s*='
$html | Should -Match 'window\.__adoqrTopRemediations\s*='
$html | Should -Match 'data-summary-pass'
$html | Should -Match 'data-summary-fail'
$html | Should -Match 'data-org-fail'
$html | Should -Match 'data-project-row="Web"'
$html | Should -Match 'function applyAcceptedSummary'
$html | Should -Match 'visibilitychange'
}
finally {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
66 changes: 66 additions & 0 deletions tests/RemediationHtml.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }

BeforeAll {
. $PSScriptRoot/_Bootstrap.ps1
}

Describe 'Write-RemediationHtmlReport' {
It 'renders tabs and controls for accepted risk workflow' {
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $tempDir | Out-Null
$filePath = Join-Path $tempDir 'remediation.html'

$remediation = [PSCustomObject]@{
ControlId = 'AUTH-01'
ControlName = 'Conditional Access'
Severity = 'High'
Count = 3
AffectedAreas = @('Organization', 'Project: Web')
Finding = 'Conditional access is not enforced.'
}

try {
Write-RemediationHtmlReport -FilePath $filePath -OrgName 'Contoso' -ExecReportFile 'executive.html' -Remediations @($remediation)

$html = Get-Content -Path $filePath -Raw
$html | Should -Match 'Accepted Controls'
$html | Should -Match 'data-remed-tab="accepted"'
$html | Should -Match 'data-open-accept'
$html | Should -Match 'Reason for accepting this control'
$html | Should -Match 'Save accepted control'
$html | Should -Match 'Undo acceptance'
$html | Should -Match 'data-accepted-date'
$html | Should -Match 'data-control-key="AUTH-01\|Conditional Access"'
$html | Should -Match 'aria-required="true"'
$html | Should -Match 'localStorage'
$html | Should -Match 'saved per organization for future remediation reports opened in the same browser'
}
finally {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}

It 'uses the accepted-controls storage key scoped to the organization name' {
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $tempDir | Out-Null
$filePath = Join-Path $tempDir 'remediation.html'

$remediation = [PSCustomObject]@{
ControlId = 'AUTH-01'
ControlName = 'Conditional Access'
Severity = 'High'
Count = 1
AffectedAreas = @('Organization')
Finding = 'Conditional access is not enforced.'
}

try {
Write-RemediationHtmlReport -FilePath $filePath -OrgName 'Fabrikam' -ExecReportFile 'executive.html' -Remediations @($remediation)

(Get-Content -Path $filePath -Raw) | Should -Match 'adoqr\.acceptedControls\.Fabrikam'
}
finally {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
6 changes: 6 additions & 0 deletions tests/_Bootstrap.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ $wantedFns = @(
'Build-ComparisonSectionHtml'
'Get-NotCheckedReasonCategory'
'Build-NotCheckedSectionHtml'
'Get-RemediationSteps'
'Write-RemediationHtmlReport'
'Write-ExecutiveHtmlReport'
'Get-AdoqrLogoDataUri'
'Get-AdoqrHeaderCss'
'Get-AdoqrHeaderHtml'
'Import-AdoqrSettings'
)

Expand Down
Loading