Skip to content

Commit bd7a1aa

Browse files
committed
Move InsightAppSec to the DAST Admin Integrations tab
Rapid7 InsightAppSec is a dynamic scanning integration, so it now lives with the other dynamic scanning settings, under External sites on the DAST Admin page, on a new Integrations tab. It leaves the general Settings page, which was getting crowded. The External sites section in Settings points to its new home. Everything the integration does is there: turn it on, the region, the API key, how many scans at once, the scan config catalog with Refresh, and the hourly import of Rapid7's results with Import now and the list of hosts that are not sites here. Turning it on still brings in what Rapid7 already has straight away. The key is still stored encrypted and never shown again.
1 parent 4d8fb1e commit bd7a1aa

5 files changed

Lines changed: 135 additions & 105 deletions

File tree

‎app/web/help_text.py‎

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -276,31 +276,6 @@
276276
"payloads. Only scan sites the company owns and is allowed "
277277
"to test.",
278278
}),
279-
("insightappsec", "Rapid7 InsightAppSec", {
280-
"title": "Rapid7 InsightAppSec",
281-
"what": [
282-
"Starts dynamic scans on Rapid7 InsightAppSec from a site's page "
283-
"under External sites, follows them, and reads the findings back "
284-
"into the site's scans, with a link to each one in Rapid7.",
285-
"A scan there always runs from a scan config made in "
286-
"InsightAppSec. The config holds the start address, the attack "
287-
"template, the engine and the sign in. This product only says "
288-
"which config to run. Refresh scan configs reads the list again "
289-
"after somebody adds one in Rapid7.",
290-
"Scans run in Rapid7 itself, old ones too, come in every hour as "
291-
"scans of the sites they touched. Import results now runs it "
292-
"straight away. Hosts with Rapid7 results that are not sites here "
293-
"are listed on the card.",
294-
"A scan with more than 5 findings keeps only its counts and date "
295-
"here; the detail stays in Rapid7. Rapid7 false positives and "
296-
"duplicates are left out.",
297-
"The key needs to see the apps and scan configs, and to start "
298-
"scans. The setup guide says which role does that.",
299-
],
300-
"caution": "An InsightAppSec scan sends attack payloads to the site, "
301-
"from Rapid7's engines. Only scan sites the company owns "
302-
"and is allowed to test.",
303-
}),
304279
("limits", "Limits", {
305280
"title": "Limits",
306281
"what": [
@@ -342,9 +317,6 @@ def sections() -> list:
342317
("signin", "Sign in", ["signin", "sessions"]),
343318
("reports", "Reports", ["output", "reports", "display"]),
344319
("web", "Web and external sites", ["domains", "webtest", "external"]),
345-
# One card per product this one works with. The next integration is a
346-
# section here and a card on the page.
347-
("integrations", "Integrations", ["insightappsec"]),
348320
]
349321

350322

‎app/web/routes_api.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2275,7 +2275,7 @@ async def test_settings(request: Request, user: User = Depends(current_user)):
22752275
def _integrations_back(message: str, kind: str):
22762276
from urllib.parse import quote
22772277

2278-
return redirect(f"/settings?msg={quote(message[:300])}&kind={kind}#tab-integrations")
2278+
return redirect(f"/dast-admin?msg={quote(message[:300])}&kind={kind}#tab-integrations")
22792279

22802280

22812281
@router.post("/settings/integrations/insightappsec/test")

‎app/web/routes_external.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,10 +1520,19 @@ def _rule_context(db, user):
15201520
available_rulesets=_available_rulesets(db),
15211521
oob_url=settings_store.get(db, "dast.oob_url") or "",
15221522
drafts=drafts, drafting=any(d.status == "working" for d in drafts),
1523+
values=settings_store.all_values(db), ias=_ias_context(),
15231524
ai_on=ai.enabled(ai.config(db)))
15241525
return context
15251526

15261527

1528+
def _ias_context() -> dict:
1529+
"""The InsightAppSec console data, the same the Settings page used to show."""
1530+
from app.core import insightappsec as ias
1531+
1532+
return {"regions": ias.REGIONS, "guide": ias.GUIDE,
1533+
"catalog": ias.load_catalog(), "imported": ias.load_import()["last"]}
1534+
1535+
15271536
@router.api_route("/oob/{token}", methods=["GET", "POST", "HEAD"],
15281537
include_in_schema=False)
15291538
async def oob_callback(token: str, request: Request):
@@ -1564,6 +1573,47 @@ def dast_admin_page(request: Request, user: User = Depends(current_user)):
15641573
return page(request, "dast_admin.html", **context)
15651574

15661575

1576+
@router.post("/dast-admin/integrations")
1577+
async def save_integrations(request: Request, user: User = Depends(current_user)):
1578+
await check_csrf(request)
1579+
form = await request.form()
1580+
back = "/dast-admin#tab-integrations"
1581+
from app.core import insightappsec as ias
1582+
from app.core import settings_store
1583+
1584+
key_typed = str(form.get("ias.api_key") or "").strip()[:500]
1585+
enabled = _checked(form, "ias.enabled")
1586+
try:
1587+
at_once = max(1, min(20, int(form.get("ias.max_concurrent") or 2)))
1588+
except (TypeError, ValueError):
1589+
at_once = 2
1590+
with session_scope() as db:
1591+
_guard(db, user)
1592+
region = str(form.get("ias.region") or "us").strip().lower()[:10]
1593+
if region not in ias.REGIONS:
1594+
return redirect(_back(back, "Pick an Insight platform region for "
1595+
"InsightAppSec.", "error"))
1596+
if enabled and not settings_store.get(db, "ias.api_key") and not key_typed:
1597+
return redirect(_back(back, "Give the InsightAppSec API key before "
1598+
"turning it on. The setup guide says how to "
1599+
"make one.", "error"))
1600+
settings_store.set_value(db, "ias.region", region, user.username)
1601+
key_changed = bool(key_typed) and settings_store.set_value(
1602+
db, "ias.api_key", key_typed, user.username)
1603+
settings_store.set_value(db, "ias.max_concurrent", at_once, user.username)
1604+
enabled_changed = settings_store.set_value(db, "ias.enabled", enabled,
1605+
user.username)
1606+
record(db, "integration.saved", actor=user.username, target="insightappsec",
1607+
detail=f"{region}, {'on' if enabled else 'off'}",
1608+
source_ip=client_ip(request))
1609+
# Turning it on (or changing the key while on) brings in what Rapid7 has now.
1610+
if enabled and (enabled_changed or key_changed):
1611+
from app.core import external
1612+
1613+
external.start_ias_import()
1614+
return redirect(_back(back, "Saved InsightAppSec settings."))
1615+
1616+
15671617
@router.post("/dast-admin/settings")
15681618
async def save_dast_settings(request: Request, user: User = Depends(current_user)):
15691619
await check_csrf(request)

‎app/web/templates/dast_admin.html‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<nav class="section-nav" role="tablist" aria-label="DAST admin">
1818
<a href="#tab-rules" role="tab" data-tab="rules" class="active">Premium Curated Rules</a>
1919
<a href="#tab-profiles" role="tab" data-tab="profiles">Scan profiles</a>
20+
<a href="#tab-integrations" role="tab" data-tab="integrations">Integrations</a>
2021
<a href="#tab-settings" role="tab" data-tab="settings">General DAST settings</a>
2122
</nav>
2223

@@ -209,6 +210,87 @@
209210
</form></div>
210211
</div>
211212

213+
<section data-pane="integrations" hidden>
214+
<div class="card" id="insightappsec">
215+
<div class="card-head"><span class="head-text">Rapid7 InsightAppSec</span>
216+
<a class="btn sm" href="{{ base }}{{ ias.guide }}" style="margin-left:auto">Setup guide</a></div>
217+
<form method="post" action="{{ base }}/dast-admin/integrations">
218+
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
219+
<div class="small faint" style="margin-bottom:.6rem">
220+
Start dynamic scans on InsightAppSec from a site's page under External sites, and read
221+
the findings back. Each site runs from a scan config made in InsightAppSec.
222+
New to this? Follow the <a href="{{ base }}{{ ias.guide }}">step by step setup guide</a>.
223+
</div>
224+
<label class="check"><input type="checkbox" name="ias.enabled" value="1" {{ 'checked' if values['ias.enabled'] }}>
225+
<span>Turn on InsightAppSec scans. They show under Run it on, on each site's scan form.
226+
External sites has to be on as well.</span></label>
227+
<div class="form-grid">
228+
<label>Region
229+
<select name="ias.region">
230+
{% for code, entry in ias.regions.items() %}<option value="{{ code }}" {{ 'selected' if values['ias.region'] == code }}>{{ entry[0] }} ({{ code }})</option>{% endfor %}
231+
</select>
232+
<span class="faint small">Where the Insight account is hosted. It is the first part of the
233+
address you sign in to InsightAppSec at: us2.appsec.insight.rapid7.com is us2.</span>
234+
</label>
235+
<label>API key
236+
<input type="password" name="ias.api_key" autocomplete="new-password" class="mono"
237+
placeholder="{{ 'stored, leave empty to keep it' if values['ias.api_key'] else 'an Insight platform API key' }}">
238+
<span class="faint small">Stored encrypted. A user key for an account made for this,
239+
not a person's own key. The guide says which role it needs.</span>
240+
</label>
241+
<label>Scans at once
242+
<input type="number" name="ias.max_concurrent" min="1" max="20"
243+
value="{{ values['ias.max_concurrent'] }}">
244+
<span class="faint small">InsightAppSec scans started from here that may run at the same
245+
time, 1 to 20. A start past it is refused. Keep it inside what the license and the
246+
engines allow.</span>
247+
</label>
248+
</div>
249+
<div class="small" style="margin-top:.6rem">
250+
{% if ias.catalog.fetched_at %}
251+
Scan configs: <strong>{{ ias.catalog.configs | length }}</strong> in {{ ias.catalog.apps }}
252+
app{{ '' if ias.catalog.apps == 1 else 's' }}, read {{ ias.catalog.fetched_at[:16] | replace('T', ' ') }} UTC
253+
{%- if ias.catalog.region != values['ias.region'] %} <span class="badge high">from the {{ ias.catalog.region }} region; refresh it</span>{% endif %}.
254+
{% set broken = ias.catalog.configs | selectattr('errors') | list %}
255+
{% if broken %}<span class="badge medium" title="{{ broken | map(attribute='name') | join(', ') }}">{{ broken | length }} with errors</span>
256+
InsightAppSec fails a scan from a config with errors.{% endif %}
257+
{% set unread = ias.catalog.configs | rejectattr('seeds_read') | list %}
258+
{% if unread %}<span class="faint">{{ unread | length }} whose start addresses could not be read, so they are not matched to a site.</span>{% endif %}
259+
{% if ias.catalog.truncated %}<span class="badge medium">only the first 1000 are kept</span>{% endif %}
260+
{% else %}<span class="faint">No scan configs read yet. Save the key, then press Refresh scan configs.</span>{% endif %}
261+
</div>
262+
{% set imp = ias.imported %}
263+
<div class="small" style="margin-top:.6rem" id="ias-import">
264+
<strong>Results from Rapid7.</strong>
265+
<span class="faint">While this is on, every scan that finished in InsightAppSec, old ones too,
266+
comes in each hour as a scan of each site whose host it touched. A host that is not a site
267+
under External sites is listed here instead.</span><br>
268+
{% if imp.ran_at %}
269+
Last import {{ imp.ran_at[:16] | replace('T', ' ') }} UTC: {{ imp.finished }} finished
270+
scan{{ '' if imp.finished == 1 else 's' }} in Rapid7, {{ imp.read }} read, {{ imp.imported }}
271+
brought in{% if imp.sites %} for {{ imp.sites | join(', ') }}{% endif %}.
272+
{% if imp.left %}<span class="badge medium">{{ imp.left }} more on the next run</span>{% endif %}
273+
{% if imp.error %}<span class="badge high">{{ imp.error }}</span>{% endif %}
274+
{% if imp.not_sites %}<br><span class="faint">{{ imp.not_sites_count }} host{{ '' if imp.not_sites_count == 1 else 's' }}
275+
with Rapid7 results {{ 'is' if imp.not_sites_count == 1 else 'are' }} not a site here:</span>
276+
{% for h in imp.not_sites[:20] %}<span class="mono">{{ h.host }}</span> ({{ h.scans }}){{ ', ' if not loop.last }}{% endfor %}
277+
{%- if imp.not_sites_count > 20 %} and {{ imp.not_sites_count - 20 }} more{% endif %}.{% endif %}
278+
{% elif imp.skipped %}<span class="faint">Last run skipped: {{ imp.skipped }}.</span>
279+
{% else %}<span class="faint">Nothing imported yet.</span>{% endif %}
280+
</div>
281+
<div class="page-actions" style="margin-top:.8rem">
282+
<button class="btn primary" type="submit">Save</button>
283+
<button class="btn" type="submit" formaction="{{ base }}/settings/integrations/insightappsec/test"
284+
title="Reads only. Uses the key typed above, or the stored one when the box is empty.">Test the connection</button>
285+
<button class="btn" type="submit" formaction="{{ base }}/settings/integrations/insightappsec/refresh"
286+
title="Reads every app and scan config the stored key can see, and their start addresses">Refresh scan configs</button>
287+
<button class="btn" type="submit" formaction="{{ base }}/settings/integrations/insightappsec/import"
288+
title="Brings in Rapid7's finished scans now instead of at the next hourly run">Import results now</button>
289+
</div>
290+
</form>
291+
</div>
292+
</section>
293+
212294
<section data-pane="settings" hidden>
213295
<div class="card">
214296
<div class="card-head">General DAST settings</div>

‎app/web/templates/settings.html‎

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,8 @@ <h2 style="margin-top:.9rem">Rules</h2>
669669
<label class="check"><input type="checkbox" name="external.enabled" value="1" {{ 'checked' if values['external.enabled'] }}>
670670
<span>Turn on External sites. It appears in the Administration menu.</span></label>
671671
<div class="faint small" style="margin:.2rem 0 .6rem">Scans on Rapid7 InsightAppSec, and other
672-
products this one works with, are set on the <a href="#tab-integrations">Integrations tab</a>.</div>
672+
products this one works with, are set under External sites, on the
673+
<a href="{{ base }}/dast-admin#tab-integrations">DAST Admin Integrations tab</a>.</div>
673674
<label class="check"><input type="checkbox" name="external.screenshots" value="1" {{ 'checked' if values['external.screenshots'] }}>
674675
<span>Take a screenshot of each site when its telemetry is read.</span></label>
675676
<label class="check"><input type="checkbox" name="external.whitebox" value="1" {{ 'checked' if values['external.whitebox'] }}>
@@ -719,81 +720,6 @@ <h2 style="margin-top:.9rem">Rules</h2>
719720
</label>
720721
</div>
721722

722-
<div class="card" id="insightappsec">
723-
<div class="card-head"><span class="head-text">Rapid7 InsightAppSec<button type="button" class="help-dot" data-modal="help-insightappsec" aria-label="What this section is for">?</button></span>
724-
<a class="btn sm" href="{{ base }}{{ ias.guide }}" style="margin-left:auto">Setup guide</a></div>
725-
<div class="small faint" style="margin-bottom:.6rem">
726-
Start dynamic scans on InsightAppSec from a site's page under External sites, and read
727-
the findings back. Each site runs from a scan config made in InsightAppSec.
728-
New to this? Follow the <a href="{{ base }}{{ ias.guide }}">step by step setup guide</a>.
729-
</div>
730-
<label class="check"><input type="checkbox" name="ias.enabled" value="1" {{ 'checked' if values['ias.enabled'] }}>
731-
<span>Turn on InsightAppSec scans. They show under Run it on, on each site's scan form.
732-
External sites has to be on as well.</span></label>
733-
<div class="form-grid">
734-
<label>Region
735-
<select name="ias.region">
736-
{% for code, entry in ias.regions.items() %}<option value="{{ code }}" {{ 'selected' if values['ias.region'] == code }}>{{ entry[0] }} ({{ code }})</option>{% endfor %}
737-
</select>
738-
<span class="faint small">Where the Insight account is hosted. It is the first part of the
739-
address you sign in to InsightAppSec at: us2.appsec.insight.rapid7.com is us2.</span>
740-
</label>
741-
<label>API key
742-
<input type="password" name="ias.api_key" autocomplete="new-password" class="mono"
743-
placeholder="{{ 'stored, leave empty to keep it' if values['ias.api_key'] else 'an Insight platform API key' }}">
744-
<span class="faint small">Stored encrypted. A user key for an account made for this,
745-
not a person's own key. The guide says which role it needs.</span>
746-
</label>
747-
<label>Scans at once
748-
<input type="number" name="ias.max_concurrent" min="1" max="20"
749-
value="{{ values['ias.max_concurrent'] }}">
750-
<span class="faint small">InsightAppSec scans started from here that may run at the same
751-
time, 1 to 20. A start past it is refused. Keep it inside what the license and the
752-
engines allow.</span>
753-
</label>
754-
</div>
755-
<div class="small" style="margin-top:.6rem">
756-
{% if ias.catalog.fetched_at %}
757-
Scan configs: <strong>{{ ias.catalog.configs | length }}</strong> in {{ ias.catalog.apps }}
758-
app{{ '' if ias.catalog.apps == 1 else 's' }}, read {{ ias.catalog.fetched_at[:16] | replace('T', ' ') }} UTC
759-
{%- if ias.catalog.region != values['ias.region'] %} <span class="badge high">from the {{ ias.catalog.region }} region; refresh it</span>{% endif %}.
760-
{% set broken = ias.catalog.configs | selectattr('errors') | list %}
761-
{% if broken %}<span class="badge medium" title="{{ broken | map(attribute='name') | join(', ') }}">{{ broken | length }} with errors</span>
762-
InsightAppSec fails a scan from a config with errors.{% endif %}
763-
{% set unread = ias.catalog.configs | rejectattr('seeds_read') | list %}
764-
{% if unread %}<span class="faint">{{ unread | length }} whose start addresses could not be read, so they are not matched to a site.</span>{% endif %}
765-
{% if ias.catalog.truncated %}<span class="badge medium">only the first 1000 are kept</span>{% endif %}
766-
{% else %}<span class="faint">No scan configs read yet. Save the key, then press Refresh scan configs.</span>{% endif %}
767-
</div>
768-
{% set imp = ias.imported %}
769-
<div class="small" style="margin-top:.6rem" id="ias-import">
770-
<strong>Results from Rapid7.</strong>
771-
<span class="faint">While this is on, every scan that finished in InsightAppSec, old ones too,
772-
comes in each hour as a scan of each site whose host it touched. A host that is not a site
773-
under External sites is listed here instead.</span><br>
774-
{% if imp.ran_at %}
775-
Last import {{ imp.ran_at[:16] | replace('T', ' ') }} UTC: {{ imp.finished }} finished
776-
scan{{ '' if imp.finished == 1 else 's' }} in Rapid7, {{ imp.read }} read, {{ imp.imported }}
777-
brought in{% if imp.sites %} for {{ imp.sites | join(', ') }}{% endif %}.
778-
{% if imp.left %}<span class="badge medium">{{ imp.left }} more on the next run</span>{% endif %}
779-
{% if imp.error %}<span class="badge high">{{ imp.error }}</span>{% endif %}
780-
{% if imp.not_sites %}<br><span class="faint">{{ imp.not_sites_count }} host{{ '' if imp.not_sites_count == 1 else 's' }}
781-
with Rapid7 results {{ 'is' if imp.not_sites_count == 1 else 'are' }} not a site here:</span>
782-
{% for h in imp.not_sites[:20] %}<span class="mono">{{ h.host }}</span> ({{ h.scans }}){{ ', ' if not loop.last }}{% endfor %}
783-
{%- if imp.not_sites_count > 20 %} and {{ imp.not_sites_count - 20 }} more{% endif %}.{% endif %}
784-
{% elif imp.skipped %}<span class="faint">Last run skipped: {{ imp.skipped }}.</span>
785-
{% else %}<span class="faint">Nothing imported yet.</span>{% endif %}
786-
</div>
787-
<div class="page-actions" style="margin-top:.8rem">
788-
<button class="btn" type="submit" formaction="{{ base }}/settings/integrations/insightappsec/test"
789-
title="Reads only. Uses the key typed above, or the stored one when the box is empty.">Test the connection</button>
790-
<button class="btn" type="submit" formaction="{{ base }}/settings/integrations/insightappsec/refresh"
791-
title="Reads every app and scan config the stored key can see, and their start addresses">Refresh scan configs</button>
792-
<button class="btn" type="submit" formaction="{{ base }}/settings/integrations/insightappsec/import"
793-
title="Brings in Rapid7's finished scans now instead of at the next hourly run">Import results now</button>
794-
</div>
795-
</div>
796-
797723
<div class="card" id="limits">
798724
<div class="card-head"><span class="head-text">Limits<button type="button" class="help-dot" data-modal="help-limits" aria-label="What this section is for">?</button></span></div>
799725
<div class="small faint" style="margin-bottom:.6rem">

0 commit comments

Comments
 (0)