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
47 changes: 45 additions & 2 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ on:
- "v*"

permissions:
contents: read
contents: write
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.set_version.outputs.version }}
steps:
- uses: actions/checkout@v6

Expand All @@ -21,12 +23,15 @@ jobs:
registry-url: "https://registry.npmjs.org"

- name: Set package version from tag
id: set_version
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [ "$VERSION" = "$GITHUB_REF_NAME" ]; then
echo "Expected tag name to start with v, got: $GITHUB_REF_NAME" >&2
exit 1
fi
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
npm version "$VERSION" --no-git-tag-version --allow-same-version

- run: npm ci
Expand All @@ -35,9 +40,47 @@ jobs:
- run: npm run build --if-present
- name: Publish package
run: |
VERSION="${GITHUB_REF_NAME#v}"
if [[ "$VERSION" == *-* ]]; then
npm publish --tag next
else
npm publish
fi

package-powershell-module:
runs-on: windows-latest
needs: publish
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: "24"

- name: Package PowerShell module
shell: pwsh
run: |
$version = "${{ needs.publish.outputs.version }}"
./scripts/package-powershell-module.ps1 -Version $version

- name: Upload PowerShell module release assets
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$version = "${{ needs.publish.outputs.version }}"
$tag = $env:GITHUB_REF_NAME
$zipPath = "artifacts/release/OwnerLens-$version-win-x64.zip"
$checksumPath = "$zipPath.sha256"

gh release view $tag *> $null
if ($LASTEXITCODE -ne 0) {
gh release create $tag --verify-tag --title $tag --notes ""
if ($LASTEXITCODE -ne 0) {
throw "Failed to create GitHub Release for $tag."
}
}

gh release upload $tag $zipPath $checksumPath --clobber
if ($LASTEXITCODE -ne 0) {
throw "Failed to upload PowerShell module release assets for $tag."
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ dependency-graph.svg
*.changes
src/providers/azure/inputTransferObject/
playwright-report/
test-results/
test-results/
testResults.xml
103 changes: 103 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Development

## Local Development

Clone the repository, install dependencies, then run the development server:

```bash
npm install
npm run dev
```

Open the Vite URL printed by the command, usually `http://127.0.0.1:5173`.

You can also exercise the published CLI entrypoint from a repository checkout:

```bash
npm run start
npm run preview
npm run collect:azure -- -SubscriptionIds "sub-id-1,sub-id-2"
npm run collect:entra -- -TenantId "<tenant-id>"
```

For a production build:

```bash
npm run build
```

## Configure Ownership Rules

Edit [src/core/config.ts](src/core/config.ts) to change ownership resolution defaults.

`ownerTags` is ordered by priority. The tag value is treated as the owner
identity and can be a group name, security group alias, or user email.

```ts
export const appConfig = {
azure: {
ownership: {
ownerTags: [
{ name: "ownerGroup", confidence: "high" },
{ name: "costCenter", confidence: "high" },
{ name: "owner", confidence: "medium" }
]
}
}
};
```

## Test

```bash
npm test
```

Run only component tests:

```bash
npm run test:components
```

Track component-test coverage:

```bash
npm run test:components:coverage
```

The component coverage report is written to `coverage/components`. Jest also
enforces the current component coverage baseline so new UI changes do not
silently reduce coverage.

## Dependency Graph

Generate a folder-level dependency graph:

```bash
npm run deps:graph
```

The generated SVG is written to `output/dependency-folders.svg`.

Generate a file-level dependency graph:

```bash
npm run deps:graph:files
```

The generated SVG is written to `output/dependency-files.svg`.

## Project Structure

- `src/App.tsx` loads snapshot files and renders the report.
- `src/core/config.ts` contains ownership resolution configuration.
- `src/report` contains report UI, filtering, view helpers, and tests.
- `src/providers/azure` contains Azure and Entra domain models and ownership
analysis logic.
- `powershell/OwnerLens` contains the PowerShell module and collector entrypoints for exporting local snapshot files.
- `tools` contains local development and test helper scripts.

## Contributing

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for local
development expectations.
126 changes: 27 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ flowchart TD

## Requirements

- Node.js 20 or newer
- npm
- PowerShell 7 or Windows PowerShell for snapshot export scripts
- Azure PowerShell and Microsoft Graph PowerShell modules when exporting data

Expand Down Expand Up @@ -84,121 +82,51 @@ Sign in to Microsoft Graph:
Connect-MgGraph -TenantId "<tenant-id>" -Scopes "Application.Read.All","Group.Read.All","Directory.Read.All"
```

Create the resource snapshot:

```bash
npx ownerlens collect:azure -SubscriptionIds "sub-id-1,sub-id-2"
```

Create the Entra snapshot:

```bash
npx ownerlens collect:entra -TenantId "<tenant-id>"
```

More script options are documented in [tools/README.md](tools/README.md).

Snapshot files can contain tenant, subscription, resource, identity, group, and
activity-log metadata. Review them before sharing. Files matching
`data/*snapshot.json` are ignored by git.

## Local Development

Clone the repository, install dependencies, then run the development server:

```bash
npm install
npm run dev
```

Open the Vite URL printed by the command, usually `http://127.0.0.1:5173`.

You can also exercise the published CLI entrypoint from a repository checkout:

```bash
npm run start
npm run preview
npm run collect:azure -- -SubscriptionIds "sub-id-1,sub-id-2"
npm run collect:entra -- -TenantId "<tenant-id>"
```

For a production build:
Import the PowerShell module:

```bash
npm run build
```

## Configure Ownership Rules

Edit [src/core/config.ts](src/core/config.ts) to change ownership resolution defaults.

`ownerTags` is ordered by priority. The tag value is treated as the owner
identity and can be a group name, security group alias, or user email.

```ts
export const appConfig = {
azure: {
ownership: {
ownerTags: [
{ name: "ownerGroup", confidence: "high" },
{ name: "costCenter", confidence: "high" },
{ name: "owner", confidence: "medium" }
]
}
}
};
```

## Test

```bash
npm test
```powershell
Import-Module ./artifacts/OwnerLens/OwnerLens.psd1 -Force
```

Run only component tests:
Start OwnerLens from PowerShell on Windows:

```bash
npm run test:components
```powershell
Start-OwnerLens
Open-OwnerLens
Get-OwnerLensStatus
Stop-OwnerLens
```

Track component-test coverage:
`Start-OwnerLens` starts the local app on `127.0.0.1` using a free port and
stores runtime state under `$env:LOCALAPPDATA\OwnerLens`. To use a specific data
directory or port, pass them explicitly:

```bash
npm run test:components:coverage
```powershell
Start-OwnerLens -DataPath C:\OwnerLensData -Port 4174
```

The component coverage report is written to `coverage/components`. Jest also
enforces the current component coverage baseline so new UI changes do not
silently reduce coverage.

## Dependency Graph

Generate a folder-level dependency graph:
Create the resource snapshot:

```bash
npm run deps:graph
```powershell
Invoke-OwnerLensCollectAzure -SubscriptionIds "sub-id-1,sub-id-2"
```

The generated SVG is written to `output/dependency-folders.svg`.

Generate a file-level dependency graph:
Create the Entra snapshot:

```bash
npm run deps:graph:files
```powershell
Invoke-OwnerLensCollectEntra -TenantId "<tenant-id>"
```

The generated SVG is written to `output/dependency-files.svg`.
More collector options are documented in [tools/README.md](tools/README.md).

## Project Structure
Snapshot files can contain tenant, subscription, resource, identity, group, and
activity-log metadata. Review them before sharing. Files matching
`data/*snapshot.json` are ignored by git.

- `src/App.tsx` loads snapshot files and renders the report.
- `src/core/config.ts` contains ownership resolution configuration.
- `src/report` contains report UI, filtering, view helpers, and tests.
- `src/providers/azure` contains Azure and Entra domain models and ownership
analysis logic.
- `tools` contains PowerShell scripts for exporting local snapshot files.
## Development

## Contributing
See [DEVELOPMENT.md](DEVELOPMENT.md) for local development, testing, dependency
graph, project structure, and ownership rule configuration notes.

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for local
development expectations.
Expand Down
12 changes: 6 additions & 6 deletions bin/ownerlens.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const dataDir = ensureDataDirectory(invocationRoot);
printDataDirectorySummary(dataDir);

const commands = new Map([
["collect:entra", "collect-entra.ps1"],
["collect-azure", "collect-azure.ps1"],
["collect:azure", "collect-azure.ps1"],
["collect-entra", "collect-entra.ps1"]
["collect:entra", { root: "powershell", script: join("OwnerLens", "Public", "Invoke-OwnerLensCollectEntra.ps1") }],
["collect-azure", { root: "powershell", script: join("OwnerLens", "Public", "Invoke-OwnerLensCollectAzure.ps1") }],
["collect:azure", { root: "powershell", script: join("OwnerLens", "Public", "Invoke-OwnerLensCollectAzure.ps1") }],
["collect-entra", { root: "powershell", script: join("OwnerLens", "Public", "Invoke-OwnerLensCollectEntra.ps1") }]
]);

if (command === "help" || command === "--help" || command === "-h") {
Expand All @@ -36,9 +36,9 @@ if (commands.has(command)) {
process.exit(1);
}

function runPowerShellScript(scriptName, args, options = {}) {
function runPowerShellScript(script, args, options = {}) {
const pwsh = resolvePowerShell();
const scriptPath = join(packageRoot, "tools", scriptName);
const scriptPath = join(packageRoot, script.root, script.script);
const psArgs = [
"-NoProfile",
"-ExecutionPolicy",
Expand Down
2 changes: 1 addition & 1 deletion docs/EPIC_1.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ If `-SubscriptionIds` is provided, split it by comma and export only those subsc
Example:

```powershell
.\tools\prepare-resource-snapshot.ps1 -SubscriptionIds "sub-id-1,sub-id-2"
npm run collect:azure -- -SubscriptionIds "sub-id-1,sub-id-2"
```

Subscription names are also accepted for admin convenience.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dist",
"index.html",
"migrations",
"powershell",
"src",
"tools",
"contracts",
Expand Down
Loading
Loading