fix(frontend): hide viewer-only risk-compute UI, proactively renew session
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
- Executive Dashboard no longer auto-triggers POST /risk/compute or shows the Refresh scores button for roles the backend rejects (viewer), which was causing a 403 on load - AuthContext now proactively renews the session cookie every 20 minutes while the app is open, so an actively-used session doesn't rely solely on the reactive on-401 refresh racing against its own token's expiry
This commit is contained in:
@@ -35,6 +35,11 @@ export async function getMe(): Promise<User> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Silently renew the session cookie. Throws if there is nothing to renew. */
|
||||||
|
export async function refreshToken(): Promise<void> {
|
||||||
|
await client.post("/auth/refresh");
|
||||||
|
}
|
||||||
|
|
||||||
/** Change the current user's password. */
|
/** Change the current user's password. */
|
||||||
export async function changePassword(
|
export async function changePassword(
|
||||||
currentPassword: string,
|
currentPassword: string,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
login as apiLogin,
|
login as apiLogin,
|
||||||
logout as apiLogout,
|
logout as apiLogout,
|
||||||
getMe,
|
getMe,
|
||||||
|
refreshToken as apiRefreshToken,
|
||||||
} from "../api/auth";
|
} from "../api/auth";
|
||||||
import type { User } from "../types/models";
|
import type { User } from "../types/models";
|
||||||
import ChangePasswordModal from "../components/ChangePasswordModal";
|
import ChangePasswordModal from "../components/ChangePasswordModal";
|
||||||
@@ -45,6 +46,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
refreshUser().finally(() => setIsLoading(false));
|
refreshUser().finally(() => setIsLoading(false));
|
||||||
}, [refreshUser]);
|
}, [refreshUser]);
|
||||||
|
|
||||||
|
// Proactively renew the session cookie while the app is open, so an
|
||||||
|
// actively-used session never actually reaches the hard 8-hour expiry
|
||||||
|
// (relying solely on the reactive on-401 refresh risks losing the race
|
||||||
|
// against the token's own expiry — see api/client.ts).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
const PROACTIVE_REFRESH_INTERVAL_MS = 20 * 60 * 1000;
|
||||||
|
const id = setInterval(() => {
|
||||||
|
apiRefreshToken().catch(() => {
|
||||||
|
// Reactive on-401 handling in api/client.ts takes over from here.
|
||||||
|
});
|
||||||
|
}, PROACTIVE_REFRESH_INTERVAL_MS);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
const login = useCallback(async (username: string, password: string) => {
|
const login = useCallback(async (username: string, password: string) => {
|
||||||
await apiLogin(username, password);
|
await apiLogin(username, password);
|
||||||
const me = await getMe();
|
const me = await getMe();
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ import { getCoverageByTactic } from "../api/metrics";
|
|||||||
import { getThreatActors } from "../api/threat-actors";
|
import { getThreatActors } from "../api/threat-actors";
|
||||||
import { getTechniques, type TechniqueSummary } from "../api/techniques";
|
import { getTechniques, type TechniqueSummary } from "../api/techniques";
|
||||||
import { getRiskProfiles, computeRiskScores, type RiskProfile } from "../api/risk";
|
import { getRiskProfiles, computeRiskScores, type RiskProfile } from "../api/risk";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
|
||||||
|
// Roles allowed to trigger a risk-score recompute (matches the backend's
|
||||||
|
// POST /risk/compute role gate) — viewer must never call this endpoint.
|
||||||
|
const CAN_COMPUTE_RISK_ROLES = ["admin", "red_lead", "blue_lead"];
|
||||||
|
|
||||||
// ── Score Gauge Component ────────────────────────────────────────────
|
// ── Score Gauge Component ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -159,6 +164,8 @@ function sinceDate(days: number | null): string | undefined {
|
|||||||
|
|
||||||
export default function ExecutiveDashboardPage() {
|
export default function ExecutiveDashboardPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canComputeRisk = CAN_COMPUTE_RISK_ROLES.includes(user?.role ?? "");
|
||||||
|
|
||||||
const [timeRange, setTimeRange] = useState<TimeRange>("all");
|
const [timeRange, setTimeRange] = useState<TimeRange>("all");
|
||||||
const rangeOption = TIME_RANGE_OPTIONS.find((o) => o.value === timeRange)!;
|
const rangeOption = TIME_RANGE_OPTIONS.find((o) => o.value === timeRange)!;
|
||||||
@@ -214,9 +221,10 @@ export default function ExecutiveDashboardPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-compute on first load if no profiles exist
|
// Auto-compute on first load if no profiles exist — only for roles the
|
||||||
|
// backend actually allows to call POST /risk/compute (viewer would 403).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadingRisk && riskProfiles !== undefined && riskProfiles.length === 0) {
|
if (canComputeRisk && !loadingRisk && riskProfiles !== undefined && riskProfiles.length === 0) {
|
||||||
computeMutation.mutate();
|
computeMutation.mutate();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -708,19 +716,21 @@ export default function ExecutiveDashboardPage() {
|
|||||||
Critical Gaps — Top 10 by Risk Priority
|
Critical Gaps — Top 10 by Risk Priority
|
||||||
<MetricTooltip title="Critical Gaps" description="The 10 most dangerous attack techniques with no detection coverage, ranked by a composite risk score based on: how many threat actors use this technique, recent threat intelligence, and test history." context="These are your highest-priority gaps — real adversaries actively use these techniques and you cannot currently detect them." />
|
<MetricTooltip title="Critical Gaps" description="The 10 most dangerous attack techniques with no detection coverage, ranked by a composite risk score based on: how many threat actors use this technique, recent threat intelligence, and test history." context="These are your highest-priority gaps — real adversaries actively use these techniques and you cannot currently detect them." />
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
{canComputeRisk && (
|
||||||
onClick={() => computeMutation.mutate()}
|
<button
|
||||||
disabled={computeMutation.isPending}
|
onClick={() => computeMutation.mutate()}
|
||||||
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-400 hover:text-cyan-400 hover:border-cyan-500/30 disabled:opacity-50 transition-colors"
|
disabled={computeMutation.isPending}
|
||||||
title="Recompute risk scores"
|
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-400 hover:text-cyan-400 hover:border-cyan-500/30 disabled:opacity-50 transition-colors"
|
||||||
>
|
title="Recompute risk scores"
|
||||||
{computeMutation.isPending ? (
|
>
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
{computeMutation.isPending ? (
|
||||||
) : (
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
<RefreshCw className="h-3 w-3" />
|
) : (
|
||||||
)}
|
<RefreshCw className="h-3 w-3" />
|
||||||
{computeMutation.isPending ? "Computing…" : "Refresh scores"}
|
)}
|
||||||
</button>
|
{computeMutation.isPending ? "Computing…" : "Refresh scores"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user