mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 19:50:54 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cc22cb794 | ||
|
|
3b6eb2fd2c | ||
|
|
2ab1046746 | ||
|
|
c84f124cb4 |
+8
-2
@@ -633,14 +633,20 @@ LIMIT $1`, malformedIPAssetsSampled)
|
||||
if total == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return malformedIPAssetWarningText(total, samples), nil
|
||||
}
|
||||
|
||||
// malformedIPAssetWarningText words the warning for total offending assets, of
|
||||
// which only samples are named; the ones left out are counted in the suffix.
|
||||
func malformedIPAssetWarningText(total int, samples []string) string {
|
||||
warning := fmt.Sprintf(
|
||||
"%d assets have an ip field that is not a valid IP; skipped for IP/CIDR scope matching (these assets will not be attributed to a company by subnet rules): %s",
|
||||
total, strings.Join(samples, ", "),
|
||||
)
|
||||
if total > len(samples) {
|
||||
warning += fmt.Sprintf(" and %d more", total)
|
||||
warning += fmt.Sprintf(" and %d more", total-len(samples))
|
||||
}
|
||||
return warning, nil
|
||||
return warning
|
||||
}
|
||||
|
||||
// UpdateScope replaces all scope rules for a company and reattributes.
|
||||
|
||||
@@ -458,3 +458,21 @@ func TestListCompanies(t *testing.T) {
|
||||
t.Error("ListCompanies: created company not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedIPAssetWarningTextCountsOmitted(t *testing.T) {
|
||||
samples := []string{"#1 a", "#2 b", "#3 c", "#4 d", "#5 e"}
|
||||
// The suffix must count the assets left out, not repeat the total, otherwise
|
||||
// the operator is told about more bad rows than exist.
|
||||
got := malformedIPAssetWarningText(8, samples)
|
||||
if !strings.HasPrefix(got, "8 assets have an ip field") {
|
||||
t.Fatalf("the warning should lead with the total of 8, got %q", got)
|
||||
}
|
||||
if !strings.HasSuffix(got, ": #1 a, #2 b, #3 c, #4 d, #5 e and 3 more") {
|
||||
t.Fatalf("8 assets with 5 named should note the 3 omitted, got %q", got)
|
||||
}
|
||||
// When every offending asset is named there is nothing to add.
|
||||
got = malformedIPAssetWarningText(5, samples)
|
||||
if !strings.HasSuffix(got, ": #1 a, #2 b, #3 c, #4 d, #5 e") {
|
||||
t.Fatalf("5 assets with 5 named should have no suffix, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,18 @@ import { ConfigField, FilterSummary } from "./_components/channel-form";
|
||||
import { DeliveryList } from "./_components/delivery-list";
|
||||
import { formatBacklog, StatTile } from "./_components/stat-tile";
|
||||
|
||||
// Upper bounds of the two message render limits. They mirror the backend, which answers 400 outside [0, max].
|
||||
const SUMMARY_LIMIT_MAX = 4000;
|
||||
const ASSETS_LIMIT_MAX = 50;
|
||||
|
||||
// limitInvalid reports whether a limit input holds anything other than a whole number in [0, max].
|
||||
// Empty is valid: it is not submitted, so the stored value stays as it is.
|
||||
function limitInvalid(raw: string, max: number): boolean {
|
||||
if (raw.trim() === "") return false;
|
||||
const n = Number(raw);
|
||||
return !Number.isInteger(n) || n < 0 || n > max;
|
||||
}
|
||||
|
||||
// This page only orchestrates: loading data, holding form state, calling the API.
|
||||
// Field definitions and parsing live in _components/channel-fields.ts, controls and the filter summary in
|
||||
// _components/channel-form.tsx, and the delivery log in _components/delivery-list.tsx —
|
||||
@@ -50,6 +62,8 @@ export default function NotifyPage() {
|
||||
const [globalSaving, setGlobalSaving] = React.useState(false);
|
||||
const [baseURL, setBaseURL] = React.useState("");
|
||||
const [digestMin, setDigestMin] = React.useState("");
|
||||
const [summaryMax, setSummaryMax] = React.useState("");
|
||||
const [assetsMax, setAssetsMax] = React.useState("");
|
||||
|
||||
const load = React.useCallback(() => {
|
||||
api
|
||||
@@ -60,6 +74,14 @@ export default function NotifyPage() {
|
||||
setDigestMin(m.digest_interval_min);
|
||||
})
|
||||
.catch((e) => toast.error("Failed to load notification settings: " + (e as Error).message));
|
||||
// The two render limits are not part of /notify/meta; they are only exposed through /api/settings.
|
||||
api
|
||||
.settings()
|
||||
.then((s) => {
|
||||
setSummaryMax(String(s.notify_summary_max_runes ?? ""));
|
||||
setAssetsMax(String(s.notify_assets_max_shown ?? ""));
|
||||
})
|
||||
.catch((e) => toast.error(`Failed to load message limits: ${(e as Error).message}`));
|
||||
// Channel-list load failures must be reported: failing silently would render as "no channels at all",
|
||||
// and users would think their config was lost — scarier than a straight-up error.
|
||||
api
|
||||
@@ -244,11 +266,23 @@ export default function NotifyPage() {
|
||||
}
|
||||
|
||||
async function saveGlobal() {
|
||||
// Checked before anything is sent: the backend applies the fields of one PUT in order,
|
||||
// so a 400 on a limit would leave the URL and digest interval already saved.
|
||||
if (limitInvalid(summaryMax, SUMMARY_LIMIT_MAX)) {
|
||||
toast.error(`Summary length must be a whole number between 0 and ${SUMMARY_LIMIT_MAX}`);
|
||||
return;
|
||||
}
|
||||
if (limitInvalid(assetsMax, ASSETS_LIMIT_MAX)) {
|
||||
toast.error(`Assets listed must be a whole number between 0 and ${ASSETS_LIMIT_MAX}`);
|
||||
return;
|
||||
}
|
||||
setGlobalSaving(true);
|
||||
try {
|
||||
const patch: Record<string, unknown> = { notify_public_base_url: baseURL.trim() };
|
||||
const n = Number(digestMin);
|
||||
if (Number.isFinite(n) && n > 0) patch.notify_digest_interval_min = n;
|
||||
if (summaryMax.trim() !== "") patch.notify_summary_max_runes = Number(summaryMax);
|
||||
if (assetsMax.trim() !== "") patch.notify_assets_max_shown = Number(assetsMax);
|
||||
await api.setSettings(patch);
|
||||
toast.success("Saved");
|
||||
load();
|
||||
@@ -331,6 +365,40 @@ export default function NotifyPage() {
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">Only applies to channels in "digest" mode.</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="n-summary-max">Summary length (characters)</Label>
|
||||
<Input
|
||||
id="n-summary-max"
|
||||
type="number"
|
||||
min={0}
|
||||
max={SUMMARY_LIMIT_MAX}
|
||||
step={1}
|
||||
placeholder="120"
|
||||
value={summaryMax}
|
||||
onChange={(e) => setSummaryMax(e.target.value)}
|
||||
aria-invalid={limitInvalid(summaryMax, SUMMARY_LIMIT_MAX)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Longest finding summary shown in a message, 0–{SUMMARY_LIMIT_MAX}. 0 = no truncation.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="n-assets-max">Assets listed per finding</Label>
|
||||
<Input
|
||||
id="n-assets-max"
|
||||
type="number"
|
||||
min={0}
|
||||
max={ASSETS_LIMIT_MAX}
|
||||
step={1}
|
||||
placeholder="3"
|
||||
value={assetsMax}
|
||||
onChange={(e) => setAssetsMax(e.target.value)}
|
||||
aria-invalid={limitInvalid(assetsMax, ASSETS_LIMIT_MAX)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Most asset names listed for a finding, 0–{ASSETS_LIMIT_MAX}. 0 = no truncation (all assets listed).
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Button onClick={saveGlobal} disabled={globalSaving}>
|
||||
Save global settings
|
||||
|
||||
@@ -957,6 +957,9 @@ export interface Settings {
|
||||
notify_enabled?: boolean; // master push switch, on by default; one-click kill switch for maintenance windows
|
||||
notify_public_base_url?: string; // external base URL for vulnerability detail links; empty = messages carry no link
|
||||
notify_digest_interval_min?: number; // digest mode period (minutes), default 30
|
||||
// Render limits of IM messages; 0 = no truncation. The backend answers 400 outside the bounds.
|
||||
notify_summary_max_runes?: number; // summary characters per finding, 0-4000, default 120
|
||||
notify_assets_max_shown?: number; // asset names listed per finding, 0-50, default 3
|
||||
}
|
||||
|
||||
// ---- Vulnerability IM push ----
|
||||
|
||||
Reference in New Issue
Block a user