Update .kiro: remove SQLite hooks, add PostgreSQL migration hook, add workflow steering, sync specs

This commit is contained in:
Jordan Ramos
2026-05-12 14:45:58 -06:00
parent 3ee8487286
commit 1bb8ec1658
35 changed files with 4645 additions and 48 deletions

View File

@@ -0,0 +1,244 @@
# Design Document: Multi-BU Tenancy (Option B)
## Overview
This design adds per-user BU team scoping to the existing single-tenant dashboard. The approach uses a single Ivanti API key with a broadened sync filter, stores all findings in the existing cache, and filters at query time based on the authenticated user's assigned teams. The compliance page leverages its existing team-partitioned data model — only the frontend team selector needs scoping.
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend │
│ │
│ AuthContext.teams ──► ReportingPage (filter by teams) │
│ ──► CompliancePage (scope team selector) │
│ ──► ExportsPage (scope exports) │
│ ──► CVE HomePage (NO filtering) │
│ │
└───────────────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Backend API │
│ │
│ GET /api/auth/me ──► returns { ...user, teams: ['STEAM',...] } │
│ GET /api/ivanti/findings?teams=STEAM,ACCESS-ENG │
│ GET /api/ivanti/findings/counts?teams=STEAM │
│ GET /api/compliance/items?team=STEAM (unchanged) │
│ │
│ Ivanti Sync: uses IVANTI_BU_FILTER env var (broadened) │
│ │
└───────────────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Database │
│ │
│ users.bu_teams TEXT DEFAULT '' (comma-separated team IDs) │
│ ivanti_findings_cache.findings_json (all BUs, filtered on read) │
│ compliance_items.team (already team-partitioned) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Database Changes
### Migration: `add_user_bu_teams.js`
```sql
ALTER TABLE users ADD COLUMN bu_teams TEXT NOT NULL DEFAULT '';
```
- Stores comma-separated team identifiers (e.g. `'STEAM,ACCESS-ENG'`)
- Empty string means no teams assigned (new users start unscoped)
- No foreign key — validated at application layer against known teams list
- Existing users get empty string (admin must assign teams post-migration)
### Known Teams Constant
```js
const KNOWN_TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'];
```
Defined once in a shared location (e.g. `backend/helpers/teams.js`) and imported by routes that need validation.
## Backend Changes
### 1. Auth Middleware (`middleware/auth.js`)
Update the session query to SELECT `bu_teams` and parse it onto `req.user`:
```js
req.user = {
id: session.user_id,
username: session.username,
email: session.email,
group: session.user_group,
teams: session.bu_teams ? session.bu_teams.split(',').filter(Boolean) : []
};
```
### 2. Auth Routes (`routes/auth.js`)
- `POST /login` response includes `teams` array
- `GET /me` response includes `teams` array
### 3. User Management Routes (`routes/users.js`)
- `POST /` (create user): accepts optional `bu_teams` field, validates against KNOWN_TEAMS
- `PATCH /:id` (update user): accepts `bu_teams` field, validates, logs change to audit
- `GET /` and `GET /:id`: return `bu_teams` in user records
### 4. Ivanti Findings Routes (`routes/ivantiFindings.js`)
**Sync filter change:**
```js
// Before (hardcoded):
const FINDINGS_FILTERS = [
{ field: 'assetCustomAttributes.1550_host_1.value', value: 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM', ... }
];
// After (configurable):
const BU_FILTER_VALUE = process.env.IVANTI_BU_FILTER || 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM';
const FINDINGS_FILTERS = [
{ field: 'assetCustomAttributes.1550_host_1.value', value: BU_FILTER_VALUE, ... }
];
```
**Query-time filtering on GET /findings:**
```js
router.get('/findings', requireAuth(db), async (req, res) => {
const state = await readState(db);
let findings = state.findings || [];
// Filter by teams if provided
const teamsParam = req.query.teams;
if (teamsParam) {
const teams = teamsParam.split(',').map(t => t.trim().toUpperCase());
findings = findings.filter(f =>
teams.some(t => (f.buOwnership || '').toUpperCase().includes(t))
);
}
res.json({ findings, total: findings.length, ... });
});
```
Same pattern for `/findings/counts` — filter before counting.
### 5. BU Ownership Matching Strategy
The Ivanti `buOwnership` field contains full BU names like `"NTS-AEO-STEAM"` or `"NTS-AEO-ACCESS-ENG"`. The user's `bu_teams` stores short names like `"STEAM"` or `"ACCESS-ENG"`. Matching uses a contains/includes check:
```js
// Match: user team "STEAM" matches finding buOwnership "NTS-AEO-STEAM"
const matchesTeam = (buOwnership, userTeams) =>
userTeams.some(t => (buOwnership || '').toUpperCase().includes(t.toUpperCase()));
```
This is intentionally loose to handle variations in Ivanti's BU naming without requiring exact string equality.
## Frontend Changes
### 1. AuthContext
Add `teams` to the user state from `/api/auth/me`:
```js
const [user, setUser] = useState(null);
// user.teams = ['STEAM', 'ACCESS-ENG'] or []
```
Add helper:
```js
const hasTeams = () => (user?.teams?.length ?? 0) > 0;
const isTeamMember = (team) => user?.teams?.includes(team) || isAdmin();
// Admin scope toggle — persisted in localStorage
const [adminScope, setAdminScope] = useState(
() => localStorage.getItem('admin_bu_scope') || 'my-teams'
);
const toggleAdminScope = () => { /* flip between 'my-teams' and 'all', persist */ };
// Returns the teams param string for API calls based on current scope
const getActiveTeamsParam = () => {
if (isAdmin() && adminScope === 'all') return ''; // no filter
return (user?.teams || []).join(',');
};
```
### 2. Reporting Page
- On mount, pass `?teams=${getActiveTeamsParam()}` to findings fetch
- If `user.teams` is empty and user is not Admin, show "No BU teams assigned" info panel instead of table
- Admin users get a scope toggle ("My Teams" / "All BUs") persisted in localStorage
- "My Teams" mode uses the admin's own bu_teams; "All BUs" omits the teams param entirely
- Default on login: "My Teams" so the admin's daily workflow is immediately scoped
- Counts endpoint also receives teams parameter matching current scope mode
### 3. Compliance Page
- Replace hardcoded `const TEAMS = ['STEAM', 'ACCESS-ENG']` with dynamic list from AuthContext
- Non-admin: shows only their assigned teams
- Admin in "My Teams" mode: shows only their assigned teams in the selector
- Admin in "All BUs" mode: shows all KNOWN_TEAMS in the selector
- Default `activeTeam` to first item in available teams list
- If available teams is empty and user is not Admin, show "No teams assigned" message
### 4. Exports Page
- Pass teams filter when fetching findings for export
- Respects the same scope toggle state as Reporting/Compliance
- Admin in "My Teams" exports only their BUs; "All BUs" exports everything
### 5. Admin Scope Toggle Component
- Rendered in the top nav/header area, visible only to Admin users
- Two-state toggle: "My Teams" | "All BUs"
- State stored in localStorage key `admin_bu_scope` (values: `'my-teams'` | `'all'`)
- Defaults to `'my-teams'` if no localStorage value exists
- Exposed via AuthContext so all pages can read the current scope without prop drilling
- When admin's bu_teams is empty, both modes behave identically (no filtering)
### 6. User Management UI
- Add multi-select checkbox group for BU teams in create/edit user forms
- Display assigned teams as badges in user list table
- Show warning icon for users with no teams assigned
## Environment Variable
Add to `.env.example`:
```bash
# Ivanti BU Filter — comma-separated list of BU values to sync from Ivanti.
# Broadening this pulls findings for additional BUs into the local cache.
# Users see only their assigned teams' findings (filtered at query time).
# Default: NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM
IVANTI_BU_FILTER=NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM,NTS-AEO-INTELDEV,NTS-AEO-ACCESS-OPS
```
## Security Considerations
- Backend compliance endpoints remain open to any valid team parameter (Admin flexibility)
- Reporting endpoint filtering is advisory — an Admin or API caller can omit teams to see all
- The `bu_teams` field is not a security boundary for compliance data; it's a UX scoping mechanism
- True data isolation would require separate databases or row-level security (out of scope for Option B)
- Audit logging captures all team assignment changes
## Migration Path
1. Run migration to add `bu_teams` column (all existing users get empty string)
2. Admin assigns teams to existing users via user management UI
3. Update `IVANTI_BU_FILTER` in `.env` to include all desired BUs
4. Trigger a manual sync to pull the broader dataset
5. Frontend automatically scopes based on user's teams after login
## Backward Compatibility
- Users with empty `bu_teams` and Admin group see everything (current behavior preserved)
- If `IVANTI_BU_FILTER` env var is not set, defaults to current hardcoded value
- Compliance backend endpoints unchanged — only frontend selector is scoped
- CVE page completely unaffected

View File

@@ -0,0 +1,141 @@
# Requirements Document
## Introduction
Add per-user Business Unit (BU) team assignment so that multiple users with different BU responsibilities can share the same dashboard instance. The Reporting page (Ivanti findings) and Compliance page will display only data relevant to the logged-in user's assigned BU teams, while the CVE home page remains a shared global view. This follows "Option B" — a single Ivanti API key with a broadened sync filter, storing all BU findings in the local cache and filtering at query time based on the user's team assignments.
## Glossary
- **BU**: Business Unit — an organizational group within the Ivanti platform (e.g. NTS-AEO-STEAM, NTS-AEO-ACCESS-ENG, NTS-AEO-INTELDEV)
- **bu_teams**: A per-user field storing the comma-separated list of BU team identifiers the user is authorized to view
- **FINDINGS_FILTERS**: The hardcoded Ivanti API filter array that currently scopes synced findings to specific BU values
- **Compliance Team**: One of the allowed team identifiers used in the compliance data model (STEAM, ACCESS-ENG, ACCESS-OPS, INTELDEV)
- **Reporting Page**: The Ivanti host findings triage page that displays findings from the local cache
- **Compliance Page**: The AEO compliance posture page that displays non-compliant devices by team
- **CVE Home Page**: The shared CVE tracking page — remains unscoped and visible to all users
- **Admin**: A user in the Admin user_group who can manage users and assign BU teams
- **Team Selector**: The UI dropdown on the Compliance page that lets users switch between their assigned teams
## Requirements
### Requirement 1: Per-User BU Team Assignment
**User Story:** As an admin, I want to assign one or more BU teams to each user, so that their dashboard experience is scoped to the data relevant to their responsibilities.
#### Acceptance Criteria
1. THE users table SHALL include a `bu_teams` column storing a comma-separated list of team identifiers
2. WHEN a new user is created without explicit bu_teams, THE Dashboard SHALL default bu_teams to an empty string (no teams assigned)
3. THE Dashboard SHALL accept any combination of known team identifiers: STEAM, ACCESS-ENG, ACCESS-OPS, INTELDEV
4. THE Dashboard SHALL allow Admin users to assign or update bu_teams for any user via the user management interface
5. THE Dashboard SHALL validate that bu_teams contains only recognized team identifiers before persisting
### Requirement 2: Expose User Teams in Session
**User Story:** As a logged-in user, I want my assigned BU teams to be available in my session context, so that the frontend can scope data requests appropriately.
#### Acceptance Criteria
1. WHEN a user authenticates, THE auth middleware SHALL attach the user's parsed bu_teams array to `req.user.teams`
2. THE `GET /api/auth/me` endpoint SHALL return the user's teams array in the response
3. THE login response SHALL include the user's teams array
4. IF a user has no bu_teams assigned (empty string), THEN the teams array SHALL be empty `[]`
### Requirement 3: Broaden Ivanti Sync Filter
**User Story:** As a system operator, I want the Ivanti sync to pull findings for all configured BU values, so that users from any BU can see their relevant findings without needing separate API keys.
#### Acceptance Criteria
1. THE FINDINGS_FILTERS BU value SHALL be configurable via an environment variable `IVANTI_BU_FILTER` rather than hardcoded
2. IF `IVANTI_BU_FILTER` is not set, THE Dashboard SHALL default to the current value `'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM'` for backward compatibility
3. THE CLOSED_COUNT_FILTERS SHALL use the same configurable BU filter value
4. THE Dashboard SHALL document the `IVANTI_BU_FILTER` variable in `.env.example` with usage instructions
### Requirement 4: Filter Reporting Page by User's Teams
**User Story:** As a user assigned to specific BU teams, I want the Reporting page to show only findings belonging to my BUs, so that I see relevant data without noise from other teams.
#### Acceptance Criteria
1. THE `GET /api/ivanti/findings` endpoint SHALL accept an optional `teams` query parameter
2. WHEN `teams` is provided, THE endpoint SHALL filter the cached findings to only those whose `buOwnership` matches one of the specified team values
3. WHEN `teams` is not provided, THE endpoint SHALL return all cached findings (backward-compatible for Admin or unscoped views)
4. THE frontend Reporting page SHALL pass the logged-in user's teams when fetching findings
5. IF a user has no teams assigned (empty array) and the user is not Admin, THE Reporting page SHALL display a message indicating no BU teams are configured and direct them to contact an admin
6. Admin users SHALL have a scope toggle with three modes: "My Teams" (filters to admin's own bu_teams), "All BUs" (shows everything unfiltered)
7. THE Admin scope toggle SHALL default to "My Teams" so the admin's daily workflow view is scoped to their assigned BUs
8. WHEN an Admin switches to "All BUs" mode, THE Dashboard SHALL fetch findings without a teams filter
9. THE scope toggle selection SHALL persist across page navigations within the same session (stored in component state or localStorage)
### Requirement 5: Filter Compliance Page by User's Teams
**User Story:** As a user assigned to specific BU teams, I want the Compliance page team selector to show only my assigned teams, so that I cannot accidentally view or interact with another team's compliance data.
#### Acceptance Criteria
1. THE frontend Compliance page team selector SHALL display only the teams present in the user's teams array
2. IF a user has only one team assigned, THE team selector SHALL default to that team and may be hidden or disabled
3. IF a user has no teams assigned and the user is not Admin, THE Compliance page SHALL display a message indicating no teams are configured
4. Admin users SHALL see all available teams in the selector regardless of their personal bu_teams assignment
5. Admin users SHALL have the same scope toggle as the Reporting page: "My Teams" shows only their assigned teams in the selector, "All BUs" shows all available teams
6. THE Admin scope toggle state SHALL be shared across Reporting and Compliance pages (single toggle, consistent behavior)
7. THE backend compliance endpoints SHALL continue to accept any valid team parameter (authorization is frontend-scoped, not backend-enforced, to keep the API flexible for Admin use)
### Requirement 6: Admin User Management for BU Teams
**User Story:** As an admin, I want a UI to assign and modify BU teams for users, so that I can onboard new team members and adjust assignments as responsibilities change.
#### Acceptance Criteria
1. THE user management create form SHALL include a multi-select or checkbox group for BU team assignment
2. THE user management edit form SHALL include a multi-select or checkbox group for BU team assignment
3. WHEN an admin updates a user's bu_teams, THE Dashboard SHALL log the change in the audit trail with previous and new values
4. THE user list table SHALL display each user's assigned BU teams
5. THE Dashboard SHALL provide a visual indicator when a user has no teams assigned (warning state)
### Requirement 7: CVE Home Page Remains Shared
**User Story:** As any user, I want the CVE tracking page to remain a shared global view, so that all teams can collaborate on vulnerability tracking regardless of BU assignment.
#### Acceptance Criteria
1. THE CVE home page SHALL NOT filter data based on the user's bu_teams
2. ALL authenticated users SHALL see the same CVE data regardless of team assignment
3. THE CVE creation, editing, and status workflows SHALL remain unaffected by bu_teams
### Requirement 8: Ivanti Findings Count Scoping
**User Story:** As a user, I want the findings count badges and status cards on the Reporting page to reflect only my BU's findings, so that metrics are meaningful to my scope.
#### Acceptance Criteria
1. THE `GET /api/ivanti/findings/counts` endpoint SHALL accept an optional `teams` parameter and return counts filtered to those BUs
2. THE open/closed count cards on the Reporting page SHALL reflect the user's scoped findings
3. WHEN viewing all BUs (Admin toggle), THE counts SHALL reflect the full unfiltered totals
### Requirement 9: Export Scoping
**User Story:** As a user exporting findings data, I want exports to respect my BU scope, so that I don't inadvertently share another team's data.
#### Acceptance Criteria
1. THE Exports page Ivanti findings exports SHALL filter by the user's teams by default
2. Admin users in "My Teams" mode SHALL export only their assigned BUs; in "All BUs" mode SHALL export everything
3. THE exported filename or metadata SHALL indicate which BU scope was applied
### Requirement 10: Admin Scope Toggle
**User Story:** As an admin with assigned BU teams, I want a global scope toggle that lets me switch between "My Teams" (my daily workflow) and "All BUs" (full visibility), so that I can work efficiently in my own BU context while retaining the ability to see the full picture when needed.
#### Acceptance Criteria
1. THE Dashboard SHALL display a scope toggle control visible only to Admin users
2. THE scope toggle SHALL offer two modes: "My Teams" and "All BUs"
3. WHEN "My Teams" is active, THE Dashboard SHALL filter Reporting, Compliance, and Exports to the admin's own bu_teams assignment (identical behavior to a non-admin user)
4. WHEN "All BUs" is active, THE Dashboard SHALL show all data across all BUs without filtering
5. THE scope toggle SHALL default to "My Teams" on login so the admin's daily workflow is immediately relevant
6. THE scope toggle state SHALL persist in localStorage so it survives page refreshes within the same browser
7. THE scope toggle SHALL be displayed in a consistent location (e.g. top nav bar or header area) accessible from any page
8. WHEN the admin's bu_teams is empty, "My Teams" mode SHALL behave the same as "All BUs" (no filtering applied)

View File

@@ -0,0 +1,201 @@
# Implementation Plan: Multi-BU Tenancy (Option B)
## Overview
Add per-user BU team assignment with query-time filtering on the Reporting and Compliance pages. Uses a single broadened Ivanti sync and filters cached findings based on the logged-in user's assigned teams. The CVE home page remains shared/global.
## Tasks
- [x] 1. Database migration and shared teams constant
- [x] 1.1 Create `backend/helpers/teams.js` shared constant
- Export `KNOWN_TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV']`
- Export `validateTeams(teamsString)` helper that parses and validates a comma-separated string
- Returns `{ valid: boolean, teams: string[], invalid: string[] }`
- _Requirements: 1.3, 1.5_
- [x] 1.2 Create `backend/migrations/add_user_bu_teams.js`
- Add `bu_teams` column (TEXT, NOT NULL, DEFAULT '') to users table
- Idempotent — check if column exists before adding
- Follow existing migration pattern (open db, serialize, log progress, close)
- _Requirements: 1.1, 1.2_
- [x] 1.3 Update `backend/setup.js` to include bu_teams in fresh schema
- Add `bu_teams TEXT NOT NULL DEFAULT ''` to the users CREATE TABLE statement
- _Requirements: 1.1, 1.2_
- [x] 2. Backend auth changes — expose teams in session
- [x] 2.1 Update `backend/middleware/auth.js` requireAuth
- Ensure the session JOIN query SELECTs `bu_teams` from users table
- Parse `bu_teams` into array and attach as `req.user.teams`
- Empty string becomes empty array `[]`
- _Requirements: 2.1, 2.4_
- [x] 2.2 Update `backend/routes/auth.js` login response
- Include `teams` array in the login success response object
- _Requirements: 2.3_
- [x] 2.3 Update `backend/routes/auth.js` GET /me endpoint
- Include `teams` array in the /me response
- _Requirements: 2.2_
- [x] 3. User management — CRUD for bu_teams
- [x] 3.1 Update `backend/routes/users.js` POST / (create user)
- Accept optional `bu_teams` field in request body
- Validate using `validateTeams()` helper — return 400 if invalid teams present
- Store validated comma-separated string in DB
- _Requirements: 1.4, 1.5, 6.1_
- [x] 3.2 Update `backend/routes/users.js` PATCH /:id (update user)
- Accept optional `bu_teams` field in request body
- Validate using `validateTeams()` helper — return 400 if invalid teams present
- Log previous and new bu_teams values in audit trail
- _Requirements: 1.4, 1.5, 6.2, 6.3_
- [x] 3.3 Update `backend/routes/users.js` GET endpoints
- Include `bu_teams` (as raw string) and `teams` (as parsed array) in user response objects
- _Requirements: 6.4_
- [x] 4. Checkpoint: Verify migration and auth
- Run migration, create a test user with bu_teams, login, verify /me returns teams array.
- [x] 5. Broaden Ivanti sync filter
- [x] 5.1 Make FINDINGS_FILTERS BU value configurable in `backend/routes/ivantiFindings.js`
- Read `process.env.IVANTI_BU_FILTER` — default to `'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM'`
- Replace hardcoded value in FINDINGS_FILTERS array
- Apply same change to CLOSED_COUNT_FILTERS
- _Requirements: 3.1, 3.2, 3.3_
- [x] 5.2 Update `backend/.env.example` with IVANTI_BU_FILTER documentation
- Add commented variable with explanation of how broadening works
- _Requirements: 3.4_
- [x] 6. Add query-time team filtering to findings endpoints
- [x] 6.1 Update GET /findings endpoint in `backend/routes/ivantiFindings.js`
- Accept optional `teams` query parameter (comma-separated)
- Filter findings array by buOwnership matching (case-insensitive includes)
- Return filtered count in response
- If teams param is empty/missing, return all findings (backward-compatible)
- _Requirements: 4.1, 4.2, 4.3_
- [x] 6.2 Update GET /findings/counts endpoint
- Accept optional `teams` query parameter
- Filter open count by matching buOwnership against provided teams
- Closed count: if teams provided, note in response that closed count is approximate (full cache doesn't store per-finding closed data)
- _Requirements: 8.1, 8.2_
- [x] 6.3 Update GET /findings/counts/history endpoint (if team-scoped history is needed)
- Consider whether historical counts should be team-scoped or remain global
- For MVP: keep global (history is aggregate), document limitation
- _Requirements: 8.3_
- [x] 7. Checkpoint: Verify backend filtering
- Test GET /findings with and without teams param. Verify filtering works correctly.
- [x] 8. Frontend AuthContext — expose teams and admin scope toggle
- [x] 8.1 Update `frontend/src/contexts/AuthContext.js`
- Store `user.teams` from /me response
- Add `hasTeams()` helper — returns true if teams array is non-empty
- Add `isTeamMember(team)` helper — returns true if team is in user's teams or user is Admin
- Add `adminScope` state backed by localStorage key `admin_bu_scope` (default: `'my-teams'`)
- Add `toggleAdminScope()` function to flip between `'my-teams'` and `'all'`
- Add `getActiveTeamsParam()` helper — returns comma-joined teams for API calls; returns empty string when Admin is in "All BUs" mode
- Add `getAvailableTeams(knownTeams)` helper — returns user's teams, or all knownTeams if Admin in "All BUs" mode
- _Requirements: 2.2, 4.4, 4.6, 4.7, 4.9, 10.1, 10.2, 10.5, 10.6_
- [x] 9. Frontend Admin Scope Toggle component
- [x] 9.1 Create `frontend/src/components/AdminScopeToggle.js`
- Render a two-state toggle: "My Teams" | "All BUs"
- Only visible when `isAdmin()` is true
- Calls `toggleAdminScope()` from AuthContext on click
- Styled to fit in the top nav/header area
- _Requirements: 10.1, 10.2, 10.7_
- [x] 9.2 Mount AdminScopeToggle in the app header/nav
- Place in NavDrawer or top bar so it's accessible from any page
- _Requirements: 10.7_
- [x] 10. Frontend Reporting Page — scope by teams
- [x] 10.1 Update findings fetch in `ReportingPage.js`
- Use `getActiveTeamsParam()` from AuthContext to build the teams query param
- If user has no teams and is not Admin, show "No BU teams assigned" info panel instead of table
- _Requirements: 4.4, 4.5_
- [x] 10.2 Update counts fetch in `ReportingPage.js`
- Append teams param to counts endpoint using same `getActiveTeamsParam()`
- _Requirements: 8.2_
- [x] 10.3 Re-fetch findings when admin scope toggle changes
- Listen to `adminScope` value from AuthContext; trigger re-fetch on change
- _Requirements: 4.8_
- [x] 11. Frontend Compliance Page — scope team selector
- [x] 11.1 Update `CompliancePage.js` TEAMS constant
- Replace hardcoded `['STEAM', 'ACCESS-ENG']` with `getAvailableTeams(KNOWN_TEAMS)` from AuthContext
- Non-admin sees only their assigned teams
- Admin in "My Teams" mode sees only their assigned teams
- Admin in "All BUs" mode sees all KNOWN_TEAMS
- _Requirements: 5.1, 5.4, 5.5_
- [x] 11.2 Handle single-team and no-team states
- If user has one team: default to it, optionally hide selector
- If user has no teams and is not Admin: show "No teams assigned" message
- _Requirements: 5.2, 5.3_
- [ ] 11.3 Update `ComplianceChartsPanel.js` team colors
- Ensure chart only renders data for user's available teams
- _Requirements: 5.1_
- [x] 11.4 Re-render team selector when admin scope toggle changes
- Listen to `adminScope` from AuthContext; update available teams list on change
- _Requirements: 5.5, 5.6_
- [x] 12. Frontend Exports Page — scope by teams
- [x] 12.1 Update `ExportsPage.js` findings fetch
- Use `getActiveTeamsParam()` to pass teams filter
- _Requirements: 9.1_
- [x] 12.2 Add BU scope indicator to export filenames
- Append team names to exported file names (e.g. `findings-STEAM-2026-05-05.xlsx`)
- When in "All BUs" mode, use `findings-ALL-2026-05-05.xlsx`
- _Requirements: 9.3_
- [ ] 13. Frontend User Management — BU team assignment UI
- [ ] 13.1 Add team multi-select to create user form in `UserManagement.js`
- Checkbox group with all KNOWN_TEAMS options
- Sends comma-separated string as `bu_teams` field
- _Requirements: 6.1_
- [ ] 13.2 Add team multi-select to edit user form
- Pre-populate with user's current teams
- _Requirements: 6.2_
- [ ] 13.3 Display teams in user list table
- Show team badges for each user
- Show warning indicator for users with no teams assigned
- _Requirements: 6.4, 6.5_
- [ ] 14. Verify CVE page is unaffected
- [ ] 14.1 Confirm no team filtering on CVE routes or frontend
- Verify CVE page does not reference user.teams for data fetching
- _Requirements: 7.1, 7.2, 7.3_
- [ ] 15. Final checkpoint: End-to-end verification
- Create two test users with different bu_teams assignments. Log in as each and verify:
- Reporting page shows only their BU's findings
- Compliance page shows only their teams in selector
- CVE page shows same data for both
- Exports are scoped correctly
- Admin scope toggle switches between "My Teams" and "All BUs" correctly
- Admin in "My Teams" mode sees same scoped view as a regular user with same teams
- Toggle state persists across page refresh
## Notes
- The `bu_teams` field is a UX scoping mechanism, not a hard security boundary. Admin and direct API callers can always access all data.
- The buOwnership matching uses case-insensitive `includes()` to handle Ivanti's full BU names (e.g. "NTS-AEO-STEAM" matches user team "STEAM").
- Historical counts (IvantiCountsChart) remain global for MVP — per-BU historical tracking would require schema changes to the counts history table.
- The compliance backend endpoints are unchanged — only the frontend team selector is scoped.
- After running the migration, an admin must assign bu_teams to existing users. Until then, non-Admin users with empty teams will see the "no teams assigned" message on Reporting/Compliance.
- The Admin scope toggle defaults to "My Teams" so that your daily workflow (STEAM + ACCESS-ENG) is the default view. Switch to "All BUs" when you need the full picture across all business units.
- The scope toggle state is shared across Reporting, Compliance, and Exports — one toggle controls all three pages consistently.
- Admin users with empty bu_teams: "My Teams" mode behaves identically to "All BUs" (no filtering in either case).