# AttackForge Support

Welcome to the AttackForge Support Site

{% tabs %}
{% tab title="Getting Started" %}

### [How AttackForge Works](/app/getting-started/how-it-works)

### [Creating Projects](/app/getting-started/creating-and-managing-projects)

### [Creating Vulnerabilities](/app/getting-started/creating-vulnerabilities)

### [Data Concepts and Access](/app/data-concepts-and-access)

{% endtab %}

{% tab title="Reporting" %}

### [Overview](/app/modules/reporting/overview)

### [Getting Started](https://github.com/AttackForge/ReportGen)

### [Filters](/app/modules/reporting/template-filters)

### [Functions](/app/modules/reporting/template-functions)

### [Styles](/app/modules/reporting/template-styles)

{% endtab %}

{% tab title="Automations" %}

### [AttackForge Flows](/app/modules/flows)

### [Automations and Integrations Examples](https://support.attackforge.com/attackforge-enterprise/modules/flows)

{% endtab %}

{% tab title="APIs" %}

### [RESTful APIs](/app/modules/self-service-restful-api)

### [Event-driven APIs](/app/modules/self-service-events-api)

{% endtab %}

{% tab title="AI" %}

### [AI Model Context Protocol (MCP)](/app/modules/ai-mcp-and-skills)

### [Microsoft Copilot](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#microsoft-copilot-studio-self-registration)

### [Anthropic Claude](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#claude-self-registration)

### [OpenAI ChatGPT](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#chatgpt-self-registration)

### [Local / Open Source Models](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#configuring-local-mcp)

{% endtab %}

{% tab title="Scripting" %}

### [AFScript](/app/afscript)

{% endtab %}
{% endtabs %}

<figure><img src="/files/p5ehp4RXm9FlfFjq7MOG" alt=""><figcaption></figcaption></figure>


# Release Notes


# 2026

## 5 August 2026

### AI Agentic Workflows <a href="#ai-agentic-workflows" id="ai-agentic-workflows"></a>

Your agents. Your models. Your pentest lifecycle. Now wired together.

This one's big. We've just released [**over 60 New MCP Tools!**](https://support.attackforge.com/attackforge-enterprise/modules/ai-mcp-and-skills) so your AttackForge now plugs directly into your AI agents — whatever you're running, wherever it lives — so agents can hack, retest, enrich, analyse and write for you, with every action landing back in the platform as structured, auditable, reportable data. 🔥

<figure><img src="/files/PsRNYedQ9fjhUTGZWSE4" alt=""><figcaption></figcaption></figure>

No lock-in. No "our AI only." Bring Copilot Studio, bring OpenClaw, bring Microsoft Foundry, bring the agent stack your team built at 2am last quarter. If it can speak HTTP and MCP, it can work inside AttackForge.

Here's what just landed. 👇

#### Agentic Penetration Testing — Launch the Hackbots <a href="#agentic-penetration-testing-launch-the-hackbots" id="agentic-penetration-testing-launch-the-hackbots"></a>

Trigger a swarm of pentesting agents straight from a project [Action](https://support.attackforge.com/attackforge-enterprise/actions), and watch findings stream into AttackForge in real time as your agents identify, verify and enrich each vulnerability.

<figure><img src="/files/Cact6BIszYmmT4z0qsjp" alt=""><figcaption></figcaption></figure>

* 🚀 One click to launch. 👾 *Launch AI Hackbots* sits in the Actions menu on any project.
* 🔗 Real attack chaining, not scanner output. Agents enumerate, notice the weak permission, pivot to the adjacent host, and chain low-severity issues into high-impact paths — the way an attacker actually works.
* 🧰 Tool orchestration on the fly. No hard-coded "nmap → Nuclei → Burp" pipeline. The agent picks the right tool for what it's seeing and feeds results between them.
* 🔄 Continuous, not point-in-time. Run against staging and prod on every release and catch regressions as code ships.
* 📺 Watch it live. Monitor agent sessions and testing progress in real time via the OpenClaw Gateway Control UI.

> 💡 Agents cover breadth. Humans cover depth. This isn't a replacement for your testers — it's a force multiplier that puts both into the same lifecycle instead of competing.

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#agentic-penetration-testing)

#### Agents That Document Their Own Test Cases <a href="#agents-that-document-their-own-test-cases" id="agents-that-document-their-own-test-cases"></a>

This is the feature that turns agentic testing from a black box into an assessment. Your agents can now register their test procedures as *Test Cases* on the project — complete with execution flows, notes, outcomes, and linked vulnerabilities.

<figure><img src="/files/u548p1a9QMY7qAngzBz8" alt=""><figcaption></figcaption></figure>

Why it matters:

* 📊 Coverage — Turns "found 12 issues" into "executed 340 of 400 checks." That's the difference between an assertion and an assessment.
* 🔍 Reviewability — Per-test-case notes and evidence let a human spot-check the agent's work instead of re-doing the engagement.
* 💾 Durable state — Sessions can be interrupted, resumed, or split across multiple agents and humans without collision.
* 📜 Compliance evidence — A completed WSTG or MASTG testsuite is the artefact auditors want. A chat transcript isn't.
* ⚡ Automation triggers — Structured results let Flows fire automatically: draft the vuln, notify the client, open the ticket, schedule the retest.
* 📈 Measurability & trending — Finally know where your agent produces false positives, and watch controls regress quarter over quarter.
* 🛡️ Scope defence — "You missed this" now has a factual answer, with proof the agent stayed inside the authorised window.
* 📄 Reporting for free — Coverage tables and methodology appendices generate from data the agent already recorded.

When a vulnerability is found, the agent links it to the test case — resulting in a properly failed test case, exactly as a human tester would record it. ✅❌

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#id-6.-dynamically-registering-agent-test-procedures-as-project-test-cases)

#### Drive Agents From Your Project Test Cases <a href="#drive-agents-from-your-project-test-cases" id="drive-agents-from-your-project-test-cases"></a>

Flip it around: instead of the agent inventing its own methodology, have it strictly follow the test cases already allocated to the project.

The agent pulls each test case, studies its Execution Flows, builds a test plan, executes, documents the outcome, links any vulnerabilities, and updates the status. Rinse, repeat, done. 🔁

<figure><img src="/files/cnKoK9EnqQVMmJqJ9eph" alt=""><figcaption></figcaption></figure>

* 🚧 Scope containment — The allocated test cases are the agreed boundary. No wandering into systems nobody signed off on.
* 📐 Methodology guarantee — Sold as ASVS or WSTG coverage? Working the actual testsuite is the only way to prove it.
* 🏁 Definable completion — A finite list makes "done" measurable. Open-ended exploration has no terminal state.
* 💰 Bounded cost — Predictable time and token spend, so you can actually quote an agent-assisted engagement.
* 🐇 No rabbit-holing — A fixed backlog forces breadth before depth.
* 🤝 Clean division of labour — Split test cases between agent and human by assignment. No duplication, no gaps.
* ⚖️ Comparability & consistency — Identical test cases across runs make agent-vs-human benchmarking meaningful, and the methodology lives in the testsuite instead of whoever wrote the prompt that morning.
* 🛡️ Injection resistance — A hostile target can feed instructions to an autonomous agent. A fixed test case list is the anchor that makes deviation instantly visible.
* 🧭 Traceable deviation — Anything outside the suite has to be added as a new test case, turning improvisation into a logged decision.

> 💡 Pro tip: Load up your Execution Flows with detail — it dramatically improves the test plans your agents build. Can't write them all by hand? Get AI to bootstrap them and revise from there.

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#id-7.-driving-agent-test-procedures-from-attackforge-project-test-cases)

#### Custom Forms on Actions — Dynamic Agent Prompts <a href="#custom-forms-on-actions-dynamic-agent-prompts" id="custom-forms-on-actions-dynamic-agent-prompts"></a>

Stop shipping one-size-fits-all agent runs. Custom Forms on Actions let you ask the right questions before the agent starts.

<figure><img src="/files/RwqFpjYh9vuuxxwSu6pS" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/2hYSNcrDJpcqyKtTfUU5" alt=""><figcaption></figcaption></figure>

Click 👾 *Launch AI Hackbots* and a form appears, gathering the specific requirements for this assessment. That context flows straight through your [Flow](https://support.attackforge.com/attackforge-enterprise/modules/flows) — where you can validate and enrich it — and into the agent's directive.

Safer. More flexible. Better documented. Reusable across the whole team. 🎛️

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#id-5.-dynamic-agent-prompts-using-custom-forms-on-actions)

#### Agentic Vulnerability Enrichment <a href="#agentic-vulnerability-enrichment" id="agentic-vulnerability-enrichment"></a>

Point an agent at a project, a vulnerability, or a writeup — one, or a hundred at a time — and let it do the tab-hopping you've been doing manually.

<figure><img src="/files/5J7vUBYmRdr3pKoUKuRp" alt=""><figcaption></figcaption></figure>

* 🌐 Context from everywhere — CVE, NVD, EPSS, KEV, Exploit-DB, vendor advisories, threat intel, GitHub PoCs — synthesised and attached to the finding.
* 🎚️ Smarter risk scoring — Contextual severity factoring asset criticality, exposure, compensating controls and active exploitation in the wild.
* 🧹 Dedupe & cluster — 200 findings across 40 hosts collapse into one "fix once, resolve many" root cause.
* ✍️ Auto-drafted writeups — Title, Description, Attack Scenario, Remediation Recommendation and Steps to Reproduce, tailored to the actual tech stack detected.
* 🗺️ Framework mapping — MITRE ATT\&CK, CWE, CAPEC, OWASP Top 10, PCI-DSS, ISO 27001, NIST — automatically.
* 💼 Business impact translation — "This SQLi exposes the customer PII database," not "unsanitised input on /search.php."
* 🌏 Translation & localisation — Client's preferred language, technical accuracy preserved.

Before-and-after on a writeup? Night and day. 🌗

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#agentic-vulnerability-enrichment)

#### Agentic Retesting <a href="#agentic-retesting" id="agentic-retesting"></a>

The highest-value automation target in the whole lifecycle — because the hard thinking is already done.

<figure><img src="/files/hMVByQhFkDd8aswUHZIN" alt=""><figcaption></figcaption></figure>

* ⏱️ Minutes, not weeks. Agent retests the moment a developer marks a fix ready, collapsing the exposure window.
* 📸 Bulletproof evidence. Same reproduction steps every time, every request, response and payload logged. No more "the tester tried it again and it looked fixed."
* ⚡ Parallel at scale. Hundreds of findings across dev, staging and prod simultaneously — no scheduling overhead.
* 🔒 Continuous verification. Schedule it and catch rollbacks, redeploys, config drift and re-enabled debug endpoints. Retest becomes a control, not a gate.
* 💸 Changes the economics. Low-severity findings you used to skip can now be verified routinely.
* 😊 Happier devs. Near-real-time confirmation instead of a three-week context reload.

> Agents update vulnerability status, re-open or close findings, and create remediation notes automatically.

⚠️ Keep a human on the judgement call: agents are excellent at reproducing a known exploit, weaker at deciding whether a fundamentally different mitigation genuinely closes the underlying weakness.

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#agentic-retesting)

#### Agentic Executive Summary <a href="#agentic-executive-summary" id="agentic-executive-summary"></a>

The last-mile bottleneck on report delivery, gone.

<figure><img src="/files/rXhGVJqPXcUNQFmAIqnn" alt=""><figcaption></figcaption></figure>

* ⚡ Two to four hours of senior consultant time → seconds.
* 🎙️ One firm, one voice — not seven contractors, which matters when your reports land side-by-side in a board pack.
* 🔄 "SMB relay → Kerberoast → DA in 90 minutes" becomes "an attacker could reach domain administrator access within two hours, exposing customer data and financial systems."
* 🎯 Audience tailoring from one source — board, engineering leadership, or external auditor.
* 🧵 Cross-finding narrative — "Three of your five criticals share a root cause in identity misconfiguration." That's what executives actually act on.
* 📏 Calibrated risk language — no more "critical" quietly meaning different things in different engagements.

Writes straight into the project's Reporting section, ready for your tester to review and sign off. AI as first draft, tester as editor. 🖊️

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#agentic-executive-summary)

#### Agentic Attack Chain Analysis <a href="#agentic-attack-chain-analysis" id="agentic-attack-chain-analysis"></a>

Turn a flat list of mediums into a story with impact.

<figure><img src="/files/JIojXqb6IdQbzIj3PQHb" alt=""><figcaption></figcaption></figure>

* 💥 Prove the so what? — "Anonymous foothold → credential harvest → lateral movement → domain admin → crown jewels" hits differently than five separate tickets.
* 🎯 Break-chain remediation — If five findings are needed to reach impact, breaking one link kills the path. Cheaper, faster, defensible.
* 🔍 Finds what humans miss — Non-obvious chains where info disclosure → credential reuse → lateral movement adds up to a critical, spotted in seconds.
* 🗺️ Consistent ATT\&CK / Kill Chain mapping — comparable engagement-to-engagement, and a shared vocabulary for the blue team's detection opportunities.
* 🏢 Portfolio-scale patterns — "Misconfigured SSO is the pivot in 60% of your chains." Very hard to see manually.
* 🎓 Levels up junior testers — senior-quality analytical narrative without waiting on the senior reviewer.

> Chains are generated, rendered and uploaded straight to the project Workspace. 📁

⚠️ Human validation still required — someone needs to confirm the proposed pivot actually works in the target environment.

[**Learn More**](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows#agentic-attack-chain-analysis)

#### How It All Hangs Together <a href="#how-it-all-hangs-together" id="how-it-all-hangs-together"></a>

Every one of these workflows is built from pieces you already have:

[Actions](https://support.attackforge.com/attackforge-enterprise/actions) (the button your team clicks) → [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) (validate, enrich, route, secure) → your agent (via HTTP trigger) → AttackForge MCP (the agent writes results straight back).

* 🔐 Secrets, IP whitelisting and Authorization Keys supported on Flow triggers.
* 🧩 Request and Response Scripts give you full control — abort, continue, finish, log.
* 👥 Action access control so you can roll a workflow out to the whole team.
* 🛠️ MCP tool scoping — grant your agents exactly the tools each workflow needs, nothing more.

Every example in the docs uses Copilot Studio, OpenClaw and Microsoft Foundry — but any agentic system works. It's your stack. 🙌

#### Get Started <a href="#get-started" id="get-started"></a>

Full step-by-step guides, prompts, Flow scripts and MCP tool lists are in the docs: 👉 [AI Agentic Workflows](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows)

🔒 Security note: If you're deploying OpenClaw, harden it first — the default configuration is inherently insecure. Reverse proxy, IP whitelisting, and authorization keys are your friends.

Now go launch some hackbots. 👾 or watch this video for more details 👇

{% embed url="<https://youtu.be/G0lkH0_Ufms>" %}

### Create Forms on Actions <a href="#create-forms-on-actions" id="create-forms-on-actions"></a>

Your Actions can now ask questions before they run. 🙋

Actions have always been the "one button, infinite workflows" feature — click it, and a Flow fires. Powerful, but silent. Every run was identical, because the button had no way to ask the user anything.

Not anymore. Custom Forms on Actions let you attach a fully configurable form to any Action, so the person triggering it can supply exactly the context your workflow needs — right at the moment they click. 🎯

<figure><img src="/files/YiDeLUl5R1jp2dNmXoZw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/xHxO3WU9aBBadm20s1Wn" alt=""><figcaption></figcaption></figure>

#### What's New <a href="#whats-new" id="whats-new"></a>

**Build a form directly into your Action**:

* 🧱 Custom sections and fields — structure the form however your workflow needs it.
* 🖱️ Shows on run — when a user triggers the Action, the form appears and they fill it in before anything fires.
* 🔌 Straight into your Flow — form data lands in `data.form`, ready to validate, transform, branch on, or forward to an external system.
* 🛠️ Editable any time — adjust the form on an existing Action, hit Save, done.

Setting it up is genuinely three steps: create or edit an Action → add your sections and fields → save. Then open the linked Flow, select the Action in the top-right selection field, and your form data populates under `data.form` for use in AFScript. 🚀

💥 **Why This Changes Things**

From "run it and hope" to "run it properly"

An Action without a form is a fixed instruction. An Action with a form is a conversation. The same button now serves ten different scenarios instead of you building ten different buttons. 🔟➡️1️⃣

**Context capture at the point of intent**

The person clicking the button knows why they're clicking it. Forms capture that reasoning while it's fresh — the scope caveat, the ticket reference, the approval justification, the "don't touch prod" instruction — instead of losing it to a Slack message nobody can find later. 🧠

**Safer automation**

Ask the confirming question before the workflow runs, not after something's already happened. Gather the constraints, then let your Flow validate them and abort if they don't check out. 🛡️

**Self-documenting workflows**

Every Action Run now carries the inputs that shaped it. When someone asks "why did this fire, and with what parameters?" — the answer is in the run, not in someone's memory. 📋

**One workflow, whole team**

Build it once, share it with Roles, Groups or Users, and the form makes sure everyone provides the right inputs regardless of how well they know the underlying automation. Junior tester or principal consultant — same guardrails. 👥

🤖 **The Killer Use Case: Dynamic Agent Prompts**

This is where Custom Forms really earn their keep.

AI agents are only as good as the context they're given. Without a form, an agentic Action is a static prompt — the same directive every single time, regardless of client, scope, environment or intent.

With a form, the Action asks the right questions before the agent runs.

Click 👾 *Launch AI Hackbots* and a form appears, gathering the specific requirements for this assessment. That data flows into your [Flow](https://support.attackforge.com/attackforge-enterprise/modules/flows), where you can validate and enrich it, then pass it straight into the agent's directive. 🎛️

Custom Forms close the gap between non-configurable agent execution and one that carries the exact context and intention of the user who triggered it. The result: agent workflows that are safer, more flexible, better documented, and reusable across the whole team. ✅

#### Ideas To Steal <a href="#ideas-to-steal" id="ideas-to-steal"></a>

* 👾 Agent scoping — target hosts, exclusions, aggressiveness, testing window, environment.
* ✅ QA review requests — reviewer, priority, what specifically to look at.
* ⚖️ Risk acceptance workflows — business justification, accepting owner, review date.
* 🔎 On-demand scans — scan profile, target set, credentials reference.
* 📤 Custom exports and reports — date range, format, recipients, filters.
* 🎫 Ticket creation — project key, assignee, severity mapping, labels.
* 🔁 Retest triggers — which environment, which findings, notify whom.
* 🔐 Delegated privileged tasks — capture the reason a low-privileged user needs the privileged thing done.

Go make your buttons smarter. 🧾✨

### Retest Rounds, Right From Your Projects Table <a href="#retest-rounds-right-from-your-projects-table" id="retest-rounds-right-from-your-projects-table"></a>

🔄 **See where retesting is up to, without opening a project**

Every projects table now has an expand arrow on each row. Click it and that project's retest rounds unfold right underneath — status, request window, scope, retested vulnerabilities, and who requested and completed each one. No navigating away, no losing your place, no hunting through projects one at a time to work out what's still outstanding.

<figure><img src="/files/NWRofHxXtuuxi1y5EA99" alt=""><figcaption></figcaption></figure>

**Wherever You Work With Projects**

Expandable rows are available everywhere a projects table appears — `Projects`, your `Dashboard`, `Analytics`, `Groups`, `Portfolios` and `Streams`. Same rounds, same detail, wherever you happen to be working.

**View Scope and Retested Vulnerabilities**

🎯 Open the whole list as a full vulnerability table and make the view your own.

### Refreshed Test Suites and Writeups Libraries <a href="#refreshed-test-suites-and-writeups-libraries" id="refreshed-test-suites-and-writeups-libraries"></a>

Twelve frameworks. All current. All ready to import. 🚀

We've refreshed our public Test Suites and Writeups libraries so you're testing against the latest versions of the standards your clients, auditors and regulators actually care about — not the version that was current when you first set up your methodology. 🗓️

Every one of these is AttackForge-formatted JSON, ready to drop straight into your instance. No conversion. No cleanup. No "I'll do it next quarter." ✨

#### Test Suites <a href="#test-suites" id="test-suites"></a>

MITRE ATT\&CK v19.1 — the full set

The complete matrix trio, all on 19.1:

* 🏢 [ATT\&CK Enterprise 19.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ENTERPRISE/mitre_attack_enterprise_19_1.json)
* 📱 [ATT\&CK Mobile 19.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/MOBILE/mitre_attack_mobile_19_1.json)
* 🏭 [ATT\&CK ICS 19.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ICS/mitre_attack_ics_19_1.json)

Red team, purple team, adversary emulation, detection engineering, coverage reporting — all now running on current technique IDs and sub-techniques. 🧨

🤖 [MITRE ATLAS v2026.06](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATLAS/mitre_atlas_v2026.06.json)

Adversarial threat landscape for AI systems, freshly updated. If you're testing ML pipelines, model endpoints, or the agentic systems everyone is suddenly shipping to production — this is your methodology. Prompt injection, model evasion, data poisoning, supply chain attacks on AI. The demand for this one is going nowhere but up. 📈

🌐 [OWASP WSTG 4.2](https://localhost:3000/\(https://github.com/AttackForge/TestSuites/blob/main/OWASP/WSTG/owasp-wstg-4.2.json\))

The web app testing workhorse. Full methodology coverage, structured as test cases you can allocate, track and report against. 🕸️

✅ OWASP ASVS 5.0.0 — all three levels

* 1️⃣ [Level 1](https://github.com/AttackForge/TestSuites/blob/main/OWASP/ASVS/v5/owasp_asvs_5.0.0_level_1.json)
* 2️⃣ [Level 2](https://github.com/AttackForge/TestSuites/blob/main/OWASP/ASVS/v5/owasp_asvs_5.0.0_level_2.json)
* 3️⃣ [Level 3](https://github.com/AttackForge/TestSuites/blob/main/OWASP/ASVS/v5/owasp_asvs_5.0.0_level_3.json)

ASVS 5.0.0 is a significant rework from the 4.x line, and all three verification levels are packaged separately so you can scope an engagement to the assurance level the client is actually paying for. Sell L1, deliver L1, prove L1. 📐

📲 OWASP MASTG v2 (2026.01) — iOS & Android

* 🍎 [iOS](https://github.com/AttackForge/TestSuites/blob/main/OWASP/MASTG/v2/owasp-mastg-2.0.0-2026.01_ios.json)
* 🤖 [Android](https://github.com/AttackForge/TestSuites/blob/main/OWASP/MASTG/v2/owasp-mastg-2.0.0-2026.01_android.json)

Platform-split so your mobile testers get exactly the tests relevant to the build in front of them — no wading through the other platform's checks. 📱

#### Writeups Libraries <a href="#writeups-libraries" id="writeups-libraries"></a>

🐛 [MITRE CWE 4.20](https://github.com/AttackForge/Writeups/blob/main/MITRE/CWE/mitre_cwe_v4.20.json)

The full weakness enumeration as importable writeups. Consistent classification across every engagement, every tester, every report. 🏷️

⚔️ [MITRE CAPEC 3.9](https://github.com/AttackForge/Writeups/blob/main/MITRE/CAPEC/mitre_capec_v3.9.0.json)

Common attack patterns, ready to seed your vulnerability library with attack-centric context that maps cleanly alongside CWE. 🎯

### ReportGen Updates <a href="#report-gen-updates" id="report-gen-updates"></a>

We're always improving on our kick-ass reporting engine - ReportGen 🥋

You can now access [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details in the following places:

```
vulnerabilities[].custom_scoring
vulnerabilities[].affected_assets[].custom_scoring
```

### Self-Service API Updates <a href="#self-service-api-updates" id="self-service-api-updates"></a>

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

#### New REST APIs <a href="#new-rest-ap-is" id="new-rest-ap-is"></a>

* [**DeleteRemediationNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteRemediationNoteFile)
* [**DeleteWorkspaceFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteWorkspaceFile)
* [**DeleteWorkspaceFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteWorkspaceFile)
* [**DeleteProjectTestCaseFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectTestCaseFile)
* [**DeleteProjectTestCaseNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectTestCaseNoteFile)
* [**DeleteProjectTestCaseWorkspaceNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectTestCaseWorkspaceNoteFile)
* [**DeleteProjectNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectNoteFile)
* [**DeleteProjectReportingFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectReportingFile)
* [**DeleteProjectSummaryPageFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectSummaryPageFile)
* [**DeleteWorkspaceItemFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteWorkspaceItemFile)
* [**DeleteWorkspaceTestingLog**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteWorkspaceTestingLog)
* [**DeleteProjectRequestFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteProjectRequestFile)
* [**DeleteVulnerabilityLibraryFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteVulnerabilityLibraryFile)
* [**DeleteTestSuiteTestCaseFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/DeleteTestSuiteTestCaseFile)
* [**PatchRemediationNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchRemediationNoteFile)
* [**PatchVulnerabilityEvidence**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchVulnerabilityEvidence)
* [**PatchWorkspaceFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchWorkspaceFile)
* [**PatchProjectTestCaseFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectTestCaseFile)
* [**PatchProjectTestCaseNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectTestCaseNoteFile)
* [**PatchProjectTestCaseWorkspaceNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectTestCaseWorkspaceNoteFile)
* [**PatchProjectNoteFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectNoteFile)
* [**PatchProjectReportingFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectReportingFile)
* [**PatchProjectSummaryPageFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectSummaryPageFile)
* [**PatchWorkspaceItemFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchWorkspaceItemFile)
* [**PatchWorkspaceTestingLog**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchWorkspaceTestingLog)
* [**PatchProjectRequestFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchProjectRequestFile)
* [**PatchVulnerabilityLibraryFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchVulnerabilityLibraryFile)
* [**PatchTestSuiteTestCaseFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/PatchTestSuiteTestCaseFile)

#### Updates to REST APIs <a href="#updates-to-rest-ap-is" id="updates-to-rest-ap-is"></a>

* [**GetProjectsAndVulnerabilities**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectsandvulnerabilities) - now returns [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details
* [**GetProjectVulnerabilities**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectvulnerabilities) - now returns [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details
* [**GetVulnerability**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerability) - now returns [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details
* [**GetVulnerabilities**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilities) - now returns [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details
* [**GetVulnerabilitiesByAssetName**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilitiesbyassetname) - now returns [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details
* [**GetVulnerabilitiesByGroup**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilitiesbygroup) - now returns [Custom Vulnerability Scoring](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems) details

## 21 July 2026

### Review Notes, Reimagined <a href="#review-notes-reimagined" id="review-notes-reimagined"></a>

💬 A brand-new way to review, discuss, and resolve — right where the work happens.

We've completely rebuilt the Review Notes experience from the ground up. It now lives alongside every field you're reviewing — with note counts attached to the fields they're about, threaded conversations, rich text editor, resolve tracking, and one-click deep links on email notifications that take you straight to the note that needs your attention.

<figure><img src="/files/fsIR3qLFM01FgV3Is9o0" alt=""><figcaption></figcaption></figure>

#### What's New <a href="#whats-new" id="whats-new"></a>

📍 **Notes Live Next to Your Fields**

See unresolved review note counts posted right beside the fields they're about. Adding a note is just as easy: click the control next to any field and a note opens already scoped to it. Add note in context, and get straight back to work.

🧵 **Threaded Replies**

Replies now live in proper threads, presented as clean stacked screens you can drill into and step back out of.

<figure><img src="/files/JYADDohMm52uZiT8pmcr" alt=""><figcaption></figcaption></figure>

✍️ **A Better Editor — Rich Text in a Bottom Sheet**

Composing a note now happens in a spacious rich-text editor that slides up in a bottom resizable sheet, giving you room to write properly.

<figure><img src="/files/mIj6vTmHKuen6FSxOUXr" alt=""><figcaption></figcaption></figure>

✅ **Resolve & Track — Know What's Still Open at a Glance**

Mark notes as resolved (with a timestamp and a clear resolved badge), and filter your view by Open, Resolved, or All. Topic badges surface unresolved counts so reviewers can zero in on what still needs attention — and resolved threads stay tidily out of the way until you want them.

<figure><img src="/files/N9oJNI0XXgYXeW8ghWXF" alt=""><figcaption></figcaption></figure>

🔎 **Smart Filtering — See Only What Matters**

Filter notes by topic and by status, with a banner that always shows the summary.

🔗 **Deep Links — Zero Hunting**

Click on the email notification link and land directly on the exact note (even deep inside a reply thread), highlighted and ready to action.

Happy reviewing! 🎉

### User Interface Updated <a href="#groups-just-got-powered-up" id="groups-just-got-powered-up"></a>

✨ **A Fresh New Look**

We've given the interface a serious glow-up! 🎨

You'll notice a sleeker, more modern feel the moment you log in - refined visuals, refreshed touches, and a contemporary polish that makes every table, menu, icon, form and click - feel that little bit smoother.

Changes are rolling out across the entire application, so keep an eye out as you navigate around. It's the same platform you know and love - just wearing something a bit more stylish. 💫

We hope you love it as much as we loved building it! 🚀

### Groups Just Got Powered-Up <a href="#groups-just-got-powered-up" id="groups-just-got-powered-up"></a>

Managing your teams in AttackForge is now faster and easier. 💪

🔐 **Per-Member Permission Control**

Say goodbye to one-size-fits-all group settings! You can now dial in `Flow Sharing` and `Action Sharing` permissions on a per-group-member basis. Give your senior testers full reign, keep your interns on training wheels, or craft any custom access mix in between — the choice is yours. 🎯

⚡ **Bulk Actions Have Landed**

Managing team members one-by-one? That's so last release. With shiny new bulk actions, you can update permissions across your groups in a fraction of the clicks. Less admin, more adversary emulation. 🕵️

🚀 **The Bottom Line**&#x20;

Less time managing teams. More time hacking. That's what we call a win. Get in there and start bulk-configuring your way to team management nirvana. 🔥

### Bulk-Add SSO Mappings <a href="#bulk-add-sso-mappings" id="bulk-add-sso-mappings"></a>

Enterprise admins, this one's for you. We've just obliterated one of the most tedious jobs in identity management. 🎯

🚀 **Map at Warp Speed**

Configure and map hundreds — or even thousands — of Identity Provider groups in a single sweep. Whether you're onboarding a massive org, restructuring your IdP, or finally getting around to that SSO overhaul you've been dreading, bulk-add has your back. 💥

😮‍💨 **Kiss Mapping Mayhem Goodbye**

No more click-fatigue. No more losing your afternoon (or your sanity) to endless one-by-one entries. What used to take hours now takes minutes — freeing you up for the work that actually matters. 🔥

🎯 **Built for Scale**

Whether you're rolling with Entra ID, Okta, Ping, or any other IdP flavor, AttackForge's SSO mappings now scale as big as your enterprise demands. Growth-ready, admin-friendly, coffee-break-approved. ☕

### Self-Service API Updates <a href="#self-service-api-updates" id="self-service-api-updates"></a>

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

#### New REST APIs <a href="#new-rest-ap-is" id="new-rest-ap-is"></a>

* [**PatchGroupMember**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/patchgroupmember) - Patch a group member's access on a group.

## 25 June 2026

### Introducing System Actions, Actions Catalogue and Sharing Actions

⚡ **Actions Just Got a Massive Upgrade: User Actions, System Actions & Sharing!**

Get ready to supercharge your workflows! We've rolled out a powerful new way to create, manage, and share Actions across your organization — giving you more flexibility and control than ever before.

<figure><img src="/files/PUgd5coJUchNdeCW33Qc" alt=""><figcaption></figcaption></figure>

#### What's New

👤 **User Actions — Make It Personal**

Every user can now create, update, delete, and share their very own User Actions. Build the workflows that work for you, and bring your teammates along for the ride. Perfect for personal automations, team collaborations, and everything in between.

🏢 **System Actions — Roll It Out at Scale**

Administrators, this one's for you. System Actions let you configure powerful workflows and roll them out to your users — no acceptance or assignment required on their end. Deploy organisation-wide automations effortlessly, and rest easy knowing all admins automatically inherit Action Owner privileges on every System Action.

Need to pivot? Admins can switch any Action between System and User Action at any time. 🔄

<figure><img src="/files/Rj391T93lKMy2VNCBBte" alt=""><figcaption></figcaption></figure>

🤝 **Sharing Actions — Collaboration Unlocked**

Sharing is now smarter and more flexible:

Admins can share System Actions and their User Actions with Roles, Groups, or individual Users. Non-admins can share their User Actions with their own Groups and other Users.

Fine-tune access with three levels — None (a powerful denylist option), View (trigger the Action), and Edit (trigger and configure). Plus, the Override option guarantees your assigned access prevails, so users won't accidentally inherit access they shouldn't have through their Roles or Groups.

<figure><img src="/files/vFmY2aDd0R9XiMKUYSBC" alt=""><figcaption></figcaption></figure>

🔍 **Effective Access — Crystal Clear Visibility**

Want to know exactly who can access your Action? Effective Access gives Action Owners the definitive answer, factoring in every Role, Group, and User assignment. Available right from the Action settings page — just click Effective Access and see the full picture.

<figure><img src="/files/gyBj1tktwDVNdCMqSZgS" alt=""><figcaption></figcaption></figure>

More power. More flexibility. More collaboration. Go build something amazing! 🚀

### Custom Vulnerability Scoring Systems

🚀 **Score Vulnerabilities Your Way — Multi-Framework Scoring Is Now Available!**

Stop forcing every vulnerability through a single lens. AttackForge now ships with first-class support for every major vulnerability scoring framework — and lets you mix, match, and build your own on top.

✨ **What's Included Out-of-the-Box**

Spin up a fresh deployment and you'll find five battle-tested scoring systems already loaded:

* **CVSS 3.1** — the industry workhorse for technical severity
* **CVSS 4.0** — the latest evolution of the standard
* [**DREAD Threat Modelling**](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems#dread-threat-model) — Microsoft's classic threat model for fast triage
* [**OWASP Risk Rating Methodology**](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems#owasp-risk-rating-methodology) — likelihood × impact with business context baked in
* [**Custom 4x4 Risk Matrix**](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-scoring-systems#custom-4x4-risk-scoring) — board-friendly probability vs. impact scoring

Already have an existing AttackForge deployment? No problems! Click on any of the links above to access the scoring system configuration and add it to your own AttackForge instance!

<figure><img src="/files/n3NeeiTBwL9sIB84UfoC" alt=""><figcaption></figcaption></figure>

🎯 **Run Multiple Frameworks on the Same Vulnerability**

Here's the big idea: you don't have to pick just one. Stack as many scoring systems as you want on a single project and watch them work together.

You can now benefit from:

* **Complementary perspectives** — CVSS handles technical exploitability, DREAD adds discoverability and user reach, OWASP layers in business impact. Together, full picture.
* **Smarter prioritization** — a CVSS-critical bug that's hard to find and affects a non-revenue system? Multiple frameworks help you avoid both over-reacting and under-reacting.
* **Speak every dialect** — CVSS for compliance auditors, OWASP for executives, DREAD for the engineering huddle. One vulnerability, every audience.
* **Cancel out bias** — if all three frameworks scream "critical," confidence goes up.

<figure><img src="/files/8ocKH8aqVUGRaoXWvqNj" alt=""><figcaption></figcaption></figure>

🛠️ **Build Your Own Scoring System**

Need something custom? Head to `Administration → Vulnerabilities → Scoring` and click `Add Custom Scoring System`. You get:

* **Custom forms** — design your own sections and fields that appear when scoring.
* **A priority script engine** — write AFScript logic that returns Critical / High / Medium / Low / Info.
* **Cross-context awareness** — your script can pull data from other scoring systems, vulnerability fields, writeups, affected assets, and project metadata.

<figure><img src="/files/1RcR98t2kLBHdyFspBUr" alt=""><figcaption></figcaption></figure>

🔐 **Project-Level Control**

When you assign scoring systems to a project, you've got the steering wheel:

* **Required flag** — force testers to score using specific systems.
* **Ordered priority** — the top-ranked system wins when scores conflict.
* **Per-system access levels** — hide custom scoring rationale from lower-privilege roles when needed.

<figure><img src="/files/Y86UiU1W5FgBrl9J3O0U" alt=""><figcaption></figcaption></figure>

💡 **Smart Conflict Detection**

When a manually overridden priority disagrees with what the scoring systems calculated, AttackForge flags the mismatch — so you always know when human judgment has diverged from the math.

📜 **Ready-Made Priority Scripts**

The documentation ships with working AFScript examples for DREAD (1–50 score bands), OWASP (likelihood × impact matrix), and the Custom 4x4 — copy, paste, tweak, ship.

The bottom line: one vulnerability, many lenses, zero compromise. Pick your framework — or invent a new one — and get to triaging. 🎉

### Automated AI Attack Chains

🤖 **Build and Explore Attack Chains automatically for every project!**

<figure><img src="/files/T22B6Tq1YuXMaoMbY3Ea" alt=""><figcaption></figcaption></figure>

💥 Attack Chains turn a list of findings into a real story with impact. This shift unlocks many benefits which a usual vulnerability list cannot show:

* **Demonstrate real impact**. A report full of "medium" issues — a misconfigured share here, a weak service account there, an outdated Confluence page — tends to get triaged into oblivion. Chain them together into "anonymous foothold → credential harvest → lateral movement → domain admin → crown jewels," and suddenly the same findings represent an existential business risk. The attack chain proves the *so what?*, which is usually what executives and risk owners actually care about.
* **Forces realism**. Individual vulns are scored in isolation using CVSS or other, but attackers don't operate that way. Demonstrating the attack chain proves the path is actually walkable in the target environment, not just theoretically exploitable. Findings that looked scary on paper sometimes don't chain to anything meaningful, and findings that looked boring sometimes turn out to be the linchpin. That's much better signal for prioritization than severity scores alone.
* **Remediate faster with&#x20;*****break-chain*****&#x20;thinking rather than&#x20;*****fix-everything***. If five distinct findings are required to reach impact, the defender doesn't necessarily need to fix all five — breaking any one link disrupts the path. That's often significantly cheaper and faster than full remediation, and it gives the client a defensible interim posture while longer-term fixes land.
* **Map cleanly onto frameworks like MITRE ATT\&CK and Cyber Kill Chain**. Give the blue team a shared vocabulary. Each stage becomes a detection opportunity: if recon was caught, the chain stops at stage one; if not, where else could it have been broken? This feeds directly into purple-team exercises, SOC tuning, and tabletop scenarios.
* **Expose systemic weaknesses that single findings hide**. Over-permissioned service accounts, flat networks, missing egress controls, identity sprawl. Those root causes rarely show up as a single CVE but fall out clearly when you look at how an attacker actually moved from end-to-end.

In this release, we've included four (4) Attack Chain variants to help you:

#### [Interactive Attack Chain Explorer](https://support.attackforge.com/attackforge-enterprise/modules/ai-mcp-and-skills#interactive-attack-chain-explorer)

The Attack Chain Explorer generates an interactive web page from real AttackForge testing data — turning a project's findings into an animated, explorable kill-chain report. Pick from various distinct attack chains — different routes an attacker could realistically take through the engagement. Watch each chain animate stage-by-stage: attacker → recon → actionable stages → outcome. Hit `Best fix` to see the single highest-leverage remediation, or `Break chain` to see where any one fix collapses the path.

<figure><img src="/files/8UURvpRqATZI7VR8jvsi" alt=""><figcaption></figcaption></figure>

#### MITRE ATT\&CK

The MITRE ATT\&CK option will frame the project's findings against MITRE's tactics — Reconnaissance, Initial Access, Execution, Persistence, etc. It's built to land the attack chain narrative, the remediation plan, and the priority call - all on one page, so it suits board / leadership / engineering briefings.

There are two styles to choose from, and they produce visually and structurally different chains:

[**Style 1 — Executive landscape, one-pager**](https://support.attackforge.com/attackforge-enterprise/modules/ai-mcp-and-skills#mitre-att-and-ck-attack-chain-style-1)

<figure><img src="/files/6xyMwItDMLIdw2bQyFDE" alt=""><figcaption></figcaption></figure>

[**Style 2 — Portrait narrative, debrief leave-behind**](https://support.attackforge.com/attackforge-enterprise/modules/ai-mcp-and-skills#mitre-att-and-ck-attack-chain-style-2)

<figure><img src="/files/5ORzIdmHnuRexbhPpamv" alt=""><figcaption></figcaption></figure>

#### [Cyber Kill Chain](https://support.attackforge.com/attackforge-enterprise/modules/ai-mcp-and-skills#cyber-kill-chain)

The Cyber Kill Chain option maps every finding in an AttackForge project onto the seven stages of the Lockheed Martin Cyber Kill Chain — but with modernized offensive-security labels: Reconnaissance, Payload Development, Initial Access, Exploitation, Persistence, Command & Control, Mission Impact.

<figure><img src="/files/zPViUYCApCM1VQV4r0SA" alt=""><figcaption></figcaption></figure>

Try generating these Attack Chains on your next project! Your customers and internal teams are sure to be impressed 🤩

### New Tenable and Qualys Workflow Automations

🔄 **Click a button. Get a full Tenable or Qualys scan. Findings auto-import back into AttackForge.**

We've built new workflow automations giving you turnkey orchestration between AttackForge and Tenable and Qualys Web App Scanning (WAS) and Vulnerability Management (VM).

<figure><img src="/files/ZaxXncum8aMkNYZ2wORT" alt=""><figcaption></figcaption></figure>

🎯 **One Action Button, Full Scan Lifecycle**

A tester (or automation) clicks an Action on a Project in AttackForge → the project scope flies into Tenable → scans launch → status gets polled frequently → completed results flow straight back into AttackForge as vulnerabilities. No manual hand-offs, no babysitting.

💡 **Why This Matters**

* **Zero context switching** — testers never leave AttackForge to kick off a Tenable scan.
* **Self-healing orchestration** — retries, aborts, multi-target fan-out, and email alerts are all built in.
* **Bulk friendly** — fire off scans across many projects in one Action click.
* **Stateful tracking** — every scan lives on the project as a custom field, so the cron job always knows what to poll.
* **Hands-off ingestion** — findings appear in AttackForge automatically when Tenable or Qualys finishes.

These new workflow automations will help you with *Continuous Threat Exposure Management (CTEM)* and ensuring that your vulnerability scans are *easy to trigger, consistent, and automatically ingested, contextualized and prioritized!* 😎

Whether you're using **Vulnerability Management (VM)** or **Web Application Scanning (WAS)** - we've got you covered:

* [Tenable VM Workflow](https://support.attackforge.com/attackforge-enterprise/modules/flows/tenable)
* [Tenable WAS Workflow](https://support.attackforge.com/attackforge-enterprise/modules/flows/tenable)
* [Qualys VM Workflow](https://support.attackforge.com/attackforge-enterprise/modules/flows/qualys)
* [Qualys WAS Workflow](https://support.attackforge.com/attackforge-enterprise/modules/flows/qualys)

These new workflows will help you to:

* *Trigger immediate scans* directly from your projects.
* *Track the status* of each scan against the project.
* *Automatically ingest findings* as scans complete, with error reporting when there's issues.
* *Prioritize findings using your own rules* - focus on the vulnerabilities that matter!

Bottom line: AttackForge project scopes go in, Tenable and Qualys findings come out — and the whole loop runs itself on your frequency. Import the example Flows, set your secrets, and ship. 🎉

### WIZ Integration

Pentest findings, meet cloud security platform! 🥳

We've released new Flows to help you start streaming vulnerabilities straight into Wiz — automatically.

⚡ **Two Triggers, Same Destination** Pick your event, control what information goes into WIZ:

* [**Vuln Created Flow**](https://support.attackforge.com/attackforge-enterprise/modules/flows/wiz#export-vulnerability-to-wiz-on-vuln-created) — the instant a tester logs a finding, it's on its way to Wiz
* [**Vuln Updated Flow**](https://support.attackforge.com/attackforge-enterprise/modules/flows/wiz#export-vulnerability-to-wiz-on-vuln-updated) — catches vulnerabilities that weren't ready at creation but mature into export candidates later

🎯 **What Gets Exported**

Each AttackForge vulnerability lands in Wiz as a fully-formed attack surface finding, complete with:

* **Severity mapping** — AttackForge Critical/High/Medium/Low/Info translates cleanly to Wiz Critical/High/Medium/Low/None.
* **Affected asset endpoint** — hostname, port, and protocol pulled straight from asset custom fields.
* **Rich context** — description + attack scenario, steps to reproduce, vulnerability notes, and remediation guidance, all bundled into the assessment details.
* **Deep link back to AttackForge** — one click from the Wiz finding takes the analyst to the source vuln
* **Stable datasource ID** — keyed per project so findings group sensibly inside Wiz.

🧰 **Customization-Friendly**

Need to send more or less data? No problem! Adjust the Flows easily inside AttackForge to match your needs. Swap the vulnerability type (DAST, SAST, SCA, IaC, ContainerScan, HostScan, SecretDetection, Misconfiguration), tweak the severity mapping, or fan out to multiple payloads per vuln. Your tenant, your rules.

🛡️ **Pre-Flight Validation Built In**

The Flow doesn't fire blindly. Before anything touches the Wiz API, it performs validation first. Extend the validation to match your own unique integration requirements!

Bottom line: plug it in, set your secrets, and every qualifying finding in AttackForge shows up in Wiz with the full pentest context attached — no CSVs, no copy-paste, no missed handoffs. 🎉

### UX Improvements

We're constantly fine-tuning the user experience so every minute spent in AttackForge feels effortless 😎

#### New Design: Page View

We've given some of our most-used pages a fresh new look (and feel). Vulnerabilities, Writeups, Test Cases, Project Summaries, Reporting, and Project Requests now feature a redesigned layout, smarter navigation, and a cleaner, more focused experience.

**What's new:**

🧭 **Table of contents with built-in search and jump-to-section** — find what you need in seconds

↔️ **Adjustable panes** — expand your reading space whenever you need room to think

📂 **Collapsible sections** — show what matters, hide what doesn't

📎 **Faster access to files and history** — fewer clicks, more flow

<figure><img src="/files/N8aM1lduBSgW6QIomRvm" alt=""><figcaption></figcaption></figure>

#### Configure Default Tab on Module Load

🎯 **Land where you work most**

You can now choose which tab loads by default when you open a module. Spend most of your time in a specific view? Set it as your landing spot and skip the extra click every time.

Small change, big time-saver.

<figure><img src="/files/FgDSisKy0cLmZWkajIub" alt=""><figcaption></figcaption></figure>

#### Project Code - Disable or Optional

🛠️ **More control over your Project form**

The Project Code field is no longer one-size-fits-all. You can now make it optional — or remove it entirely — to match the way your team actually works.

Head to `Administration > Projects > Details Form` to tailor it your way.

#### Groups Contact Fields - Disable or Optional

👥 **Streamline your Group setup**

The Group Contact fields just got more flexible. Make them optional — or remove them entirely — so your Group records capture exactly what you need, and nothing you don't.

Head to `Administration > Groups > Details Form` to personalize your groups.

#### Copy Page Title or ID

📋 **One click, copied**

Need to grab a page title or system ID? Just click the page title and choose what you'd like to copy. No more highlighting, no more fiddly selections — it's right there when you need it.

<figure><img src="/files/GM91ztPegF418wIIO23T" alt=""><figcaption></figcaption></figure>

#### System Dropdowns Now Support Search

⚡ **Searchable dropdowns**

Dropdown lists now support search across all configured options — find what you need instantly, even in the longest lists.

<figure><img src="/files/KoOJLh3de40PC644YHxm" alt=""><figcaption></figcaption></figure>

#### Enforce Remediation Note on Retest

🔒 **Mandatory remediation notes for retests**

Optionally require a remediation note when vulnerabilities are marked as ready for retest — ensuring retesters have the context needed to validate fixes effectively.

Head over to `Administration > Vulnerabilities > Status > Status Update` to make the change.

#### Make Attack Scenario Mandatory

🛡️ **Mandatory Attack Scenario field**

Optionally require the Attack Scenario field on new Writeups — ensuring testers capture attack-specific context whenever a new issue type is defined.

Head over to `Administration > Writeups > Form` to make the change.

#### Improved User Lockout Experience for On-Premise Deployments

⚙️ **Configurable lockout policies**

You can now fine-tune the `max failed login attempts` and `account lockout timeout` policies to match your security requirements. Tighten things up, ease them off — your policy, your call.

#### All Project Request System Fields Now Accessible in Table

👀 We've loaded all the Project Request core/system fields and made them available in the `Pending Requests` and `Actioned Requests` tables!

You can now sort, filter, toggle and group all system and custom fields - and include them in your Custom Views!

### Additional Group Access - APIs, Events, Flows, Actions, AI MCP Tools

Your Groups just levelled up. Power your automations, your integrations, and your AI - using Group-driven access control.

<figure><img src="/files/ys6Z072hv8MMy8R1Nnem" alt=""><figcaption></figcaption></figure>

🎯 What's New

Say hello to `Other Access` — a brand-new tab on every Group page that lets administrators extend Group membership to a whole new set of capabilities. No more juggling individual user permissions. No more "wait, how do we get that team access to that?" Just one Group, one place, one click.

🔓 What Groups Can Now Access

* **Events via Flows** — Pipe internal events to the right team automatically
* **Events via Self-Service API** — Programmatic event access, gated by Group
* **Self-Service RESTful APIs** — Empower teams to build on AttackForge data
* **Flows** — Unlock ability for teams to build workflow automations and integrations
* **Actions** — Liberate teams to create Actions which matter to them
* **AI MCP** — Yes, even AI access is Group-managed now 🤖

💪 Why You'll Love It

🎛️ **Granular by design** — Administrators assign access per-Group, on a needs basis. Right people, right tools, right level.

⚡ **Scales with your org** — Onboard a new customer team, vendor, or internal squad and they instantly inherit access to the APIs, Flows, Actions, and AI surfaces you've already approved for their Group.

🧠 **One source of truth** — Group membership now drives access across projects, portfolios, vulnerabilities, AND your automation stack. Less drift, fewer gaps, way less audit headache.

🔐 **Enterprise-grade control** — Everything lives behind the Other Access panel on the Group page, so admins always know exactly who can do what.

🏁 How to Use It

1. Head to any Group in AttackForge
2. Click `Other Access`
3. Toggle on Events, APIs, Flows, Actions, or AI MCP — whatever this Group needs
4. Done. ✅ Members inherit the access automatically.

Bottom line: Groups are now the control plane for everything — not just data, but the Flows, Actions, APIs, and AI that make AttackForge sing. 🎶

### Flows Updates

We've made our Workflow Automation Engine [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) even better! 🤖

#### Sharing Roles and Groups Access to Flows

🚀 **Collaborate Like Never Before: Share Flows with Roles & Groups!**

You can now share your Flows with Roles, Groups, and individual Users — making collaboration smoother and access management a breeze.

🔐 **Smarter Sharing for Everyone**

* Administrators can now share Flows directly with Roles, Groups, or individual Users — perfect for rolling out access across your organization.
* Non-admins can now share their Flows with their own Groups and other Users, empowering teams to work together without waiting on admin approval.

<figure><img src="/files/r06e5WDdW34kYpX7u05T" alt=""><figcaption></figcaption></figure>

👀 **Introducing Effective Access**

Ever wondered exactly who can access a Flow? Wonder no more. Effective Access gives Flow Owners a single source of truth, showing precisely which users have access based on every Role, Group, and User assignment in play.

Just head to your Flow settings page and click Effective Access to see the complete picture.

<figure><img src="/files/FHIuBooStSPpfhaNlVwh" alt=""><figcaption></figcaption></figure>

Less guesswork, more teamwork. Happy Flowing! ✨

#### New Trigger Events

* [**asset-created**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/asset-created) - Handle event when an Asset in the Asset Module is created.
* [**asset-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/asset-updated) - Handle event when an Asset in the Asset Module is updated.
* [**asset-archived**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/asset-archived) - Handle event when an Asset in the Asset Module is archived.
* [**asset-restored**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/asset-restored) - Handle event when an Asset in the Asset Module is restored.
* [**user-created**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/user-created) - Handle event when a User is created.
* [**user-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/user-updated) - Handle event when a User is updated.
* [**project-request-file-uploaded**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-request-file-uploaded) - Handle event when a file on a Project Request is uploaded.
* [**project-on-hold**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-on-hold) - Handle event when a Project is on-hold.
* [**project-off-hold**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-off-hold) - Handle event when a Project is off-hold.
* [**vulnerability-remediation-plan-created**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-plan-created) - Handle event when a Remediation Plan is created on a Vulnerability.
* [**vulnerability-remediation-plan-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-plan-updated) - Handle event when a Remediation Plan is updated on a Vulnerability.
* [**workspace-testing-log-uploaded**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/workspace-testing-log-uploaded) - Handle event when a testing log file is uploaded to the Project Workspace.
* [**project-member-added**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-member-added) - Handle event when a user is added to the Project Team for a Project.
* [**project-member-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-member-updated) - Handle event when a user is updated on the Project Team for a Project.
* [**project-member-removed**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-member-removed) - Handle event when a user is removed from the Project Team for a Project.

#### Duplicate Actions

⚡ **Build Flows faster**

You can now duplicate actions in your Flows. Bootstrap new actions from existing ones, make a few tweaks, and you're off — no more rebuilding from scratch every time. A massive time-saver when you're building out complex automations.

#### HTML Response on HTTP Triggered Flows

📧 **One click. Clear feedback.**

Triggering automations from an email button click just got a whole lot friendlier. Add `?format=html` to the end of your [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) on any [HTTP Triggered Flow](https://support.attackforge.com/attackforge-enterprise/modules/flows#external-events), and users will land on a confirmation page acknowledging their request — including any helpful error details if something didn't go to plan.

No more guessing whether the click actually did anything.

<figure><img src="/files/TCbFqbPDHxXm0UFmDX9Z" alt=""><figcaption></figcaption></figure>

#### Secrets Now Support Multi-Lines

🔑 **Multi-line Secrets**

Secrets now support multi-line input — ideal for PEM keys and anything else that needs those line breaks preserved exactly as written. Paste with confidence.

### AFScript Updates

We've powered-up 🔋 our in-app scripting language [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) to make writing scripts even easier!

#### New Functions

* [**String.from()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - Use this function to return a string representing the primitive or object.
* [**String.sign()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - Use this function to cryptographically sign a message using a private key for signing.
* [**String.verify()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - Use this function to verify a cryptographically signed message with a public key.
* [**XML.parse()**](https://support.attackforge.com/attackforge-enterprise/afscript#xml) - Use this function to parse an XML string, constructing the JSON value or object described by the string.

#### Updated Functions

* [**Date.format()**](https://support.attackforge.com/attackforge-enterprise/afscript#dates) - This function was updated to now support passing a `timezone` parameter to include a timezone offset.

### ReportGen Updates

We're always improving on our kick-ass reporting engine - **ReportGen** 🥋

#### New Filters

* [**Substring**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#substring) - You can return a substring from a string.

### Self-Service API Updates

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

#### New REST APIs

* [**ParseCSVData**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/parsecsvdata) - Parse CSV data to JSON format.
* [**GetUserProfile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getuserprofile) - Get Profile for a User.
* [**UpdateUserProfile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateuserprofile) - Update Profile for a User.
* [**RequestInformationOnProjectRequest**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/requestinformationonprojectrequest) - Request Information on a Project Request.
* [**DownloadProjectRequestFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadprojectrequestfile) - Download a file on a Project Request.
* [**UploadProjectRequestFile**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadprojectrequestfile) - Upload a file to a Project Request.
* [**RichTextToConfluenceWiki**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/richtexttoconfluencewiki) - Convert data from rich-text to Confluence Wiki markup.
* [**UploadWorkspaceTestingLog**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadworkspacetestinglog) - Upload a testing log to a Project Workspace.
* [**GetReportTemplates**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getreporttemplates) - Get all Report Templates user has access to.
* [**DownloadReportTemplate**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadreporttemplate) - Download a Report Template user has access to.

#### Updates to REST APIs

* [**GetProjects**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojects) - now supports `?order=created:asc` and `?order=created:desc` to determine sort order on created timestamp.
* [**GetProjectRequests**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectrequest) - now supports the [Advanced Query Filter](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/advanced-query-filter).

## 5 March 2026

### Build Your Own Custom Workflows - In The UI!

The team at AttackForge are incredibly excited to bring you.. **Actions**! 🤩🤩

[Actions](https://support.attackforge.com/attackforge-enterprise/actions) enable you to *build your own custom workflows* into the AttackForge application user interface, and trigger those workflows with a *simple button click*.

With the newest addition of Actions, this completes our mission to empower our customers with ***Automation - Anytime, Anywhere, by Anyone!***

You can now create Workflow Automations in AttackForge from:

* **Internal Events** - e.g. when a vulnerability is created or updated
* **External Events** - e.g. when changes happen in other systems or scripts
* **Scheduled Events** - e.g. run automations hourly, daily, weekly, or a custom frequency
* ***NEW*** **Actions** - e.g. when a user clicks on an Action within the app

And best of all - these Workflow Automations are *built directly into AttackForge*! Meaning:

* No need for complex middleware, or scripts running on *someones* machine
* Full visibility and control into what your automations are doing at every step
* No additional assets or infrastructure to procure and manage
* Auditability and access controls across all automations, including secrets management!

<figure><img src="/files/RqJSeqn7CPFY0jdp4qFo" alt=""><figcaption></figcaption></figure>

You can create Actions to *help you*:

* **Build custom workflows for your teams**
* **Trigger a process automation**
* **Launch an integration with an external system**

Examples of Actions could include:

* *Request a QA review / Approve a QA review*
* *Launch scans within your security tooling*
* *Create bulk actions which do not currently exist*
* *Trigger a custom vulnerability risk acceptance workflow*
* *Enable low-privileged users to perform isolated privileged tasks*
* *Export data on-demand*
* *Run custom reports and email the results*
* *Anything you can imagine* 😄

<figure><img src="/files/czjpbDJdYwf7nZDGlFfO" alt=""><figcaption></figcaption></figure>

Actions can be created and accessed within any of the following entities:

1. [Project Request(s)](https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project)
2. [Project(s)](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects)
3. [Project Vulnerability(s)](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-vulnerabilities)
4. [Project Test Case(s)](https://support.attackforge.com/attackforge-enterprise/getting-started/test-cases)
5. [Portfolio(s)](https://support.attackforge.com/attackforge-enterprise/modules/portfolios)
6. [Portfolio Stream](https://support.attackforge.com/attackforge-enterprise/modules/portfolios)
7. [Group(s)](https://support.attackforge.com/attackforge-enterprise/modules/groups)
8. [Asset(s)](https://support.attackforge.com/attackforge-enterprise/modules/assets)
9. [Writeup(s)](https://support.attackforge.com/attackforge-enterprise/modules/vulnerability-library)
10. [User(s)](https://support.attackforge.com/attackforge-enterprise/modules/users)
11. [Application](https://support.attackforge.com/attackforge-enterprise/actions#application-actions)

When triggering an Action, a confirmation dialogue will appear. You can also access more information in the `README`&#x20;

<figure><img src="/files/6P8dE2oSpS7Jb4Ie9m40" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TU9jUqC4DMzOlqI2CX7i" alt=""><figcaption></figcaption></figure>

After you click on `Run` - the Action will be triggered and it will show in your `Action Runs Manager`.

From here, you can monitor the status of your triggered Action.

<figure><img src="/files/W2jFRGutllzXRdcXYUfk" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/E6CdGdoO7kGVVDo5YMoX" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/CdKWbgqvu70qIYhYOH2r" alt=""><figcaption></figcaption></figure>

You can click on the status to see more information:

<figure><img src="/files/M8GY29pd3chD6f2WRAO4" alt=""><figcaption></figcaption></figure>

You can also build `Application Actions`. These Actions relate to the application itself, they are not tied to any particular entity such as a Project, Vulnerability or Asset.

You can use Application Actions *at any time* - regardless of what access you have to other data and workflows in AttackForge.

This makes them ideal for ***personal workflows***, or ***delegating privileged workflows*** to lower-privileged users.

<figure><img src="/files/7lYbKXwTkHI1681uMZ5A" alt=""><figcaption></figcaption></figure>

Actions are powered by [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows). Flows is AttackForge's **powerful Workflow Automation engine**, allowing users to *create completely custom workflows, automations and integrations*.

Flows is powered by [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) - AttackForge's **easy-to-use scripting language**, creating *infinite possibilities* with your Actions and Flows!

Actions can be linked to multiple Flows - meaning many workflows can be triggered from one single Action.

> Coming Soon! You can share your Actions, and also create Custom Forms on Actions!

### Project Scope Assets Improvements

We've significantly improved on how you can interact with assets on a project! 🥳

We've improved the project scope assets interface to make it easier to work with data.

You can now add scope and import assets directly when creating or editing vulnerabilities - without leaving the page, reducing friction when working on vulnerabilities:

<figure><img src="/files/ZGaPq3BfOy0w9qqkE47w" alt=""><figcaption></figcaption></figure>

Importing assets now support list imports where you can copy/paste a delimited list of assets to import:

<figure><img src="/files/UsdstSVnLJI0Qi4d7FTl" alt=""><figcaption></figcaption></figure>

You can now view full asset information (including components) directly from the vulnerabilities table.

We've added advanced filtering on all asset related tables within a project.

You can also manage project scope assets anywhere within a project where scope assets can be selected.

We've also extended the *Quick Select* and *Table* options to all places for filtering and selecting assets.

### Review Notes In More Places

In recent updates, we've focused on making quality assurance reviews ✅ *faster and easier* in AttackForge.

In this update, we've extended [Review Notes](https://support.attackforge.com/attackforge-enterprise/getting-started/reviewing-and-qa-vulnerabilities) to **Writeups** and **Test Cases on Test Suites**:

<figure><img src="/files/8WPAUKGqPorl41HCPeM9" alt=""><figcaption></figcaption></figure>

### Table Custom Field Improvements

We've powered up Table Custom Fields! 💪

You can now use *Rich-Text fields in Table Custom Fields*.

This is ideal when you need a list of Rich-Text context, such as **Narratives, Timelines, Notes, and more**.

<figure><img src="/files/cTaFGVD6WFnmaMFl7bAY" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/98IaIIcyLM02dIEga2Pk" alt=""><figcaption></figcaption></figure>

We've also added support for ***List*** fields in Table Custom Fields too!

You can also control which fields need to be displayed when viewing the table.

We've also improved Table Custom Field features to support filtering for all columns, as well as `Export to CSV`.

We've also improved the user experience when viewing and editing rows of data within the table.

### Improvements In Analytics

We've enhanced all of the `Top 10 Analytics` to now include more details!

<figure><img src="/files/uspuR5NzcTKUoC6ZYxRr" alt=""><figcaption></figcaption></figure>

### New AI Testing Methodologies

If you're currently or planning to do `AI pentesting` - look no further! 👀

We've added two more AI pentesting methodologies, in addition to the existing [**MITRE ATLAS Framework**](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATLAS/mitre_atlas_4.8.0_testcases.json):

* [**OWASP AI Testing Guide**](https://github.com/AttackForge/TestSuites/blob/main/OWASP/AITG/owasp_ai_testing_guide_2026.json) - The OWASP AI Testing Guide (AITG) is an open-source, community-driven framework providing standardized methodologies to test the trustworthiness, security, and reliability of AI and LLM systems. It offers comprehensive, actionable test cases across four key layers—Application, Model, Data, and Infrastructure—to help developers and auditors manage AI-specific risks like prompt injection and bias.
* [**OWASP LLM Top 10**](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-LLM-Applications-Top-10-2025.json) - The OWASP Top 10 for Large Language Model (LLM) Applications is a comprehensive framework identifying the most critical security risks when integrating LLMs into applications. It focuses on unique vulnerabilities like prompt injection, insecure output handling, and training data poisoning, serving as a guide for developers and security professionals to build, deploy, and manage LLMs securely.

<figure><img src="/files/gW4bancyz15KuSeiK0pI" alt=""><figcaption></figcaption></figure>

### ReportGen Updates

We're always improving on our kick-ass reporting engine - **ReportGen** 🥋

#### New Filters

* [**Declare**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#declare) - You can declare variables in-line using values from tags directly.
* [**Assign**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#assign) - You can assign a new value to a variable in-line using values from tags directly.
* [**DateAdd**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateadd) - You can add units of time to a date.
* [**DateSubtract**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#datesubtract) - You can subtract units of time to a date.
* [**DateDiff**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#datediff) - You can diff the time between dates.
* [**Increment**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#datediff) - You can increment a number by 1 or a specified integer.
* [**Multiply**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#datediff) - You can multiply a number by a specified integer.
* [**Drop**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#drop) - You can use drop to prevent a value from showing.

#### New Functions

* [**$range**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdrange) - Use this function to create a range of data which you can iterate over.
* [**$dateRange**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdrange) - Use this function to create a range of dates which you can iterate over.
* [**$dateDiff**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdrange) - Use this function to perform a diff between two dates.

#### [dateFormat Filter](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateformat) Now Supports Timezone Offset

#### Sort Test Cases Using Custom Order

### UX Improvements

#### Export User Access Lists to CSV

You can now export all user access directly to CSV in `Users > (select user) > Access`.

#### Non-Admins Can Now Link/Re-Assign Vulns To Other Projects

#### Attack Chains Get New Tactics

We've added `Reconnaissance` and `Resource Development` to the Attack Chains.

### ServiceNow - Vulnerability Response Integration

We're committed to supporting our customers with integrating all of their offensive security testing into their enterprise ecosystem! 🎯

Previously we released an [integration with ServiceNow Incidents](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-servicenow-incident).

In this release, we've created a bi-directional integration with [ServiceNow Vulnerability Response](https://www.servicenow.com/au/products/vulnerability-response.html) to help you:

* Automatically create Vulnerability Response (VR) findings from new vulnerabilities
* Automatically sync statuses between VR findings and vulnerabilities

You can read more about these [Flows on our Support Portal](https://support.attackforge.com/attackforge-enterprise/modules/flows#examples). Or import these Flows into your AttackForge from our [Flows GitHub Repository](https://github.com/AttackForge/Flows).

<figure><img src="/files/OzcUPeJrtsI6MIAXX5PC" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/w1IGjJP2Vc3NG4UOxwIT" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/vGyep2A1lK2moqrM1MfN" alt=""><figcaption></figcaption></figure>

### AI MCP Updates

#### New Tools

We only recently introduced [AI MCP](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp) and we're already supercharging it to keep up with new ways our customers are *plugging in their own AI tools into AttackForge* to get real work done, fast! ✨

* [**get\_file**](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#get-file) - The Get File tool can be used to get the metadata and binary content of an AttackForge File by supplying its id. This is useful for retrieving evidence files attached to Vulnerabilities or files attached to Writeups.

> If you missed [our release on AI Model Context Protocol (MCP)](https://support.attackforge.com/release-notes/2025#id-19-december-2025) - make sure to check it out to see how you can work smarter, not harder!

### Flows Updates

We've made our Workflow Automation Engine [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) even better! 🤖

#### New Events

* [**project-reporting-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-reporting-updated) - Handle event when Project Reporting page is updated.
* [**project-reporting-file-uploaded**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-reporting-file-uploaded) - Handle event when a file is uploaded to the Project Reporting page.
* [**project-summary-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-summary-updated) - Handle event when Project Summary page is updated.
* [**project-summary-file-uploaded**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-summary-file-uploaded) - Handle event when a file is uploaded to the Project Summary page.
* [**vulnerability-remediation-note-file-uploaded**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-note-file-uploaded) - Handle event when a file is uploaded to a Vulnerability Remediation Note.

#### Updates to Events

* [**project\_created**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-created) - Added `"project_request_id"` to the payload.
* [**project\_updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-updated) - Added `"project_request_id"` to the payload.
* [**vulnerability\_created**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-created) - Added `"vulnerability_library_id"` to the payload.
* [**vulnerability\_updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-updated) - Added `"vulnerability_library_id"` to the payload.

### AFScript Updates

We've powered-up 🔋 our in-app scripting language [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) to make writing scripts even easier!

#### New Functions

* [**String.match()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - Use this function to perform a match against a regular expression.
* [**String.matchAll()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - Use this function to perform a match against a regular expression, and return all results.

### Self-Service API Updates

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

#### New REST APIs

* [**UpdateProjectRequestAccess**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateprojectrequestaccess) - Patch the ACL (Access Control List) on a Project Request.
* [**UpdateProjectAccess**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateprojectaccess) - Patch the ACL (Access Control List) on a Project.

#### Updates to REST APIs

* [**UpdateVulnerability**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updatevulnerability) - now supports `"reason"` on status changes.
* [**UpdateVulnerabilityWithLibrary**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updatevulnerabilitywithlibrary) - now supports `"reason"` on status changes.
* [**GetVulnerabilities**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilities) - now supports `"resolution"` in *Advanced Query Filter (Q filter)* and `"vulnerability_resolution_type"` in vulnerability response.
* [**GetVulnerability**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerability) - now supports `"vulnerability_resolution_type"` in vulnerability response.
* [**GetVulnerabilitiesByAssetName**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilitiesbyassetname) - now supports `"vulnerability_resolution_type"` in vulnerability response.
* [**GetVulnerabilitiesByGroup**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilitiesbygroup) - now supports `"vulnerability_resolution_type"` in vulnerability response.
* [**GetProjectVulnerabilities**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectvulnerabilities) - now supports `"vulnerability_resolution_type"` in vulnerability response.
* [**GetProjectsAndVulnerabilities**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectsandvulnerabilities) - now supports `"vulnerability_resolution_type"` in vulnerability response.


# 2025

## 19 December 2025

### Integrate AI Assistants with AttackForge!

AI is finally available in AttackForge! 🤩

We've made it *flexible, robust and secure* by allowing you - and your users - to **connect your own AI Assistants!**

Whether you're using AI frontier assistants like [Claude](http://claude.ai/), [ChatGPT](https://chatgpt.com/) or [Copilot](https://copilot.microsoft.com/); or you've developed your own model from open-source - so long as it supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) - you can plug into AttackForge 🔌 and start **improving productivity to get real work done, fast!!** 📈

<figure><img src="/files/WM5bDpjpcVzaVd0R8Oyj" alt=""><figcaption></figcaption></figure>

So what can you do with this new superpower? 🦸 Let's take a look at some examples!

#### [1. Generate executive summaries for your assessments and data](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#generate-pentest-executive-summary)

<figure><img src="/files/nIvrAsFH30fGBRjrdeTW" alt=""><figcaption></figcaption></figure>

#### [2. Generate vulnerability descriptions and recommendations](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#generate-vulnerability-descriptions-and-recommendations)

<figure><img src="/files/pJ93TStMSnJCND5qnBNA" alt=""><figcaption></figcaption></figure>

#### [3. Determine the single highest-risk vulnerability on a project](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#determine-single-highest-risk-vulnerability-on-project)

<figure><img src="/files/uE7dXz37LlU7lHoSETfI" alt=""><figcaption></figcaption></figure>

#### [4. Create vulnerability composition metrics dashboards](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#create-a-vulnerability-composition-metrics-dashboard)

<figure><img src="/files/k00XjDdqvVicYVYkSo3W" alt=""><figcaption></figcaption></figure>

#### [5. Create interactive vulnerability charts](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#create-interactive-vulnerabilities-chart)

<figure><img src="/files/qVpnqEn4PDM8u5NXVxsh" alt=""><figcaption></figcaption></figure>

#### [6. Review vulnerabilities in retest](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#review-of-vulnerabilities-in-retest)

<figure><img src="/files/dANFp7TbFvBU1FneNBdK" alt=""><figcaption></figcaption></figure>

#### [7. Generate a report on all vulnerabilities assigned to you](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#show-vulnerabilities-assigned-to-me)

<figure><img src="/files/PZgyRHdWQZBSulagyXef" alt=""><figcaption></figcaption></figure>

#### [8. Generate a report on the top 10 vulnerabilities](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#top-10-vulnerabilities-report)

<figure><img src="/files/U9kxgroXb0w3KoySV1JX" alt=""><figcaption></figcaption></figure>

#### [9. Create a top 10 vulnerabilities dashboard](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#top-10-vulnerabilities-dashboard)

<figure><img src="/files/9J3j6AO0G5oHlFQYrRGB" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/z3tMoSpQR773qIOVEyeT" alt=""><figcaption></figcaption></figure>

#### [10. Create a top 10 affected assets dashboard](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#top-10-affected-assets-dashboard)

<figure><img src="/files/5caAJ9RltPp6PyN1ocYO" alt=""><figcaption></figcaption></figure>

#### [11. Create a CVSS dashboard](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#cvss-dashboard)

<figure><img src="/files/8jHlYD5Xt9muf6gDMb5N" alt=""><figcaption></figcaption></figure>

#### [12. Generate a report on unique OWASP Top 10 vulnerabilities](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#unique-owasp-top-10-vulnerabilities-report)

<figure><img src="/files/X4QW7G4u2Z9EXptDXGOt" alt=""><figcaption></figcaption></figure>

#### [13. Create an interactive executive project closeout scorecard](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#interactive-executive-project-closeout-scorecard)

<figure><img src="/files/AUDver4sAvlVPJmeWQ2l" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/50qO9r989B6Aspx2uATU" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/FqM0u15AmTByseJgnpe7" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/fDKH006ZTip8KGE2gCgY" alt=""><figcaption></figcaption></figure>

These are just some examples to get you started. You're only limited by your prompting creativity!

We will continue to add more examples over the coming months.

Integrating your AI Assistants with AttackForge has a number of significant benefits:

> More Helpful and Accurate Responses

***Access to Current AttackForge Information***

Instead of being limited to training data, AI Assistants using MCP can pull real-time information from AttackForge - for example access to your latest vulnerabilities and projects. This provides context and answers based on your latest data, not outdated information.

***Personalized Assistance***

MCP enables AI to access your specific context - your AttackForge vulnerabilities, writeups, assets, projects - making responses tailored to your actual situation rather than generic advice.

> Greater Productivity

***Unified Interface***

Instead of switching between different APIs and creating complex scripts, you can interact with your AttackForge through a single conversational interface. Ask questions about your data, retrieve records, check statuses, all in one place.

***Automated Workflows***

The AI can perform multi-step tasks, like pulling data from AttackForge, analyzing it, and updating a spreadsheet or creating a presentation - all from a simple request.

> Better Privacy and Control

***Data Stays Where It Belongs***

With MCP, your sensitive vulnerability data doesn't need to be sent to AI providers for training. The AI accesses your data when needed and only for your specific requests.

***Granular Permissions***

You control exactly what data and capabilities the AI can access on behalf of any AttackForge user you authorize to use MCP, ensuring appropriate boundaries and compliance with your security requirements.

> Future-Proof Investment

***Vendor Independence***

If you build workflows using MCP, you're not locked into a specific AI provider. You can switch AI Assistants while keeping all your integrations working.

***Growing Ecosystem***

As AttackForge continues to build more MCP tools and services, you'll automatically gain access to new capabilities without needing custom development work.

> Connecting your AI Assistants to AttackForge is super easy! 😎

We've created guides to help you with:

* [Enabling MCP](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#enabling-mcp)
* [Configuring Remote MCP](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#configuring-remote-mcp)
* [Configuring Self-Registration](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#self-registration)
* [Configuring Assisted Registration](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#assisted-registration)
* [Configuring Local MCP](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#configuring-local-mcp)
* [Available MCP Tools](https://support.attackforge.com/attackforge-enterprise/modules/ai-model-context-protocol-mcp#tools)

If you're experimenting with AI in AttackForge - we would love to hear from you! We're already planning for the next set of MCP tools, and we would love to incorporate your feedback!

### CVSS Version 4 Vulnerability Scoring

We've added support for CVSS Version 4! 🥳

This major change comes with a ton of new improvements:

* Option to configure multiple scoring systems on a project, including CVSS v4
* Re-order preferences for each scoring system
* Determine required and optional scoring systems

<figure><img src="/files/PCvL2rygfkkDEMGhSxQt" alt=""><figcaption></figcaption></figure>

* Ability to filter vulnerabilities by CVSS Score and CVSS Vector

<figure><img src="/files/uz3tF5BjdwI5tHTyHBVz" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/KzyGW78W3QU00pn6XNPc" alt=""><figcaption></figcaption></figure>

* View and score CVSS v3 and CVSS v4 independently
* Priority and Exploitability can be independently adjusted from the CVSS Score
* Recommendation on Priority if the current value selected is different to CVSS determined priority
* CVSS Vectors will show shorthand syntax i.e. ignore metrics which are *Not Defined (X)*

<figure><img src="/files/Dcd1ZAMAoQJ7w0mG1vb9" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MdtTwQXm5dt88LC0yGff" alt=""><figcaption></figcaption></figure>

> **Coming soon!** Create your own custom scoring systems! DREAD, STRIDE, PASTA, or completely custom in-house solution

### File Uploads and Downloads Supported In Flows

We've added support for uploading and downloading files in Flows! 💪

With this new addition, you can now:

* Attach vulnerability evidence (e.g. screenshots, videos, scripts) to your external tickets and systems in real-time.
* Ingest vulnerability files from external sources like JIRA, ServiceNow, Bug Bounty platforms, and others - in real-time.
* Programmatically save Excel files, PDFs, diagrams, ZIP archives, etc. in your project workspace.
* plus much more..

Every [Flow Action](https://support.attackforge.com/attackforge-enterprise/modules/flows#actions) now supports `Options` which also includes `Download Response`. This instructs the Flow to download the response from the HTTP request, and returns a `fileId` which can then be referenced later on to upload the file to wherever it needs to go.

[Request Scripts](https://support.attackforge.com/attackforge-enterprise/modules/flows#request-script) now also support passing a `multipart` body in the request configuration, which can be used to reference one or more previously downloaded files through their *fileIds*. This is used to perform the file upload action.

For more information on how to upload and download files using Flows, check out the following links:

* [Download Files using Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#downloading-files-using-flows)
* [Upload Files using Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#uploading-files-using-flows)

### Fetch Flow Run Outcome on HTTP Triggered Flow

We previously released [HTTP Triggered Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#external-events) which empowers you to create your own custom APIs in AttackForge.

You can use these custom APIs the same way you would use any other REST APIs. The key difference is *you are in control* of:

* who can call those APIs
* the input accepted
* the actions performed
* the resulting output

In this release, we've added support for retrieving the outcome of the HTTP Triggered Flow after it has been triggered.

Due to the asynchronous nature of a [Flow Run](https://support.attackforge.com/attackforge-enterprise/modules/flows#runs), you will immediately receive a `Flow Run Id` in the response:

<figure><img src="/files/PPktVZmKwVUpCufYzh8v" alt=""><figcaption></figcaption></figure>

The Flow Run Id can then be used to poll and fetch the results of the Flow Run once it has reached its terminal state.

To fetch the results of the Flow Run, you would need to make the following API request:

```bash
curl --request GET \
	--url 'https://{{tenant}}/api/flows/runs/{{flowRunId}}' \
	--header 'content-type: application/json' \
	--header 'x-user-key: {{api-key}}'
```

The result of the Flow Run will then be returned:

<figure><img src="/files/U3uDo1QZonDa2Z8f8CkW" alt=""><figcaption></figcaption></figure>

### Bugcrowd Integration

We're committed to supporting our customers with integrating all of their offensive security testing into AttackForge - this includes Bug Bounty! 🐛🎯

Previously we released an [integration with HackerOne](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-vuln-from-hackerone-report).

In this release, we've created a bi-directional integration with [Bugcrowd](https://www.bugcrowd.com/) to help you:

* Automatically ingest submissions discovered in the Bugcrowd portal
* Automatically receive updates when submissions have been updated in the Bugcrowd portal
* Automatically create a comment in Bugcrowd when a remediation note is created/updated in AttackForge
* Automatically create/update a remediation note in AttackForge when a comment is created/updated in Bugcrowd

You can read more about these [Flows on our Support Portal](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-vulnerability-on-bugcrowd-submission). Or import these Flows into your AttackForge from our [Flows GitHub Repository](https://github.com/AttackForge/Flows).

<figure><img src="/files/9S3dG3kmDvxbP2x3uSD2" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/N4NI8OiCN1KGZo9aj1lw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oaqvnGmc7NDphSSr6IKU" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/tae9GmlLVjFgY7BNNddX" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/3WAcospGCjJaccNTClBU" alt=""><figcaption></figcaption></figure>

> Not using Bugcrowd or Hackerone? Import and configure these Flows for your own Bug Bounty provider!

### UX Improvements

We've improved the `Info Panel` across the application so that you can *lock* and *unlock* the data shown in the panel. This can help you to persist vital information on the screen at all times.

<figure><img src="/files/jf7Nubt92bU9mHtiqqFd" alt=""><figcaption></figcaption></figure>

We've also made it possible to switch between the current information and the Writeup when working on a vulnerability:

<figure><img src="/files/RmpXzMuhZWxE7a14hMoi" alt=""><figcaption></figcaption></figure>

We've also improved the Vulnerabilities module to now include options to select from **Pending, Draft and All Vulnerabilities** in addition to *Visible* vulnerabilities.

<figure><img src="/files/IFf3HTgw69D135pH4RB8" alt=""><figcaption></figcaption></figure>

We've also added bulk actions, filtering and sorting to the access applied to an individual user:

<figure><img src="/files/UFUBz3mjlNZ55UonWG5d" alt=""><figcaption></figcaption></figure>

### Improved BURP and Nessus Imports

We've improved the way AttackForge handles evidence from [Burp](https://portswigger.net/burp) and [Nessus](https://www.tenable.com/products/nessus) during vulnerability imports. This means less manual changes and better looking findings!

* HTTP Request and HTTP Response is now wrapped in code blocks
* URL Path now shows as Affected Components
* For BURP - Likelihood of Exploitation is now set using *Confidence*
* IP address now shows in tags

<figure><img src="/files/MLmodsS8i0xnMcfDxBWB" alt=""><figcaption></figcaption></figure>

### Updates to AFScript

We've powered-up 🔋 our in-app programming language [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) to make writing scripts even easier!

* **New** functions:
  * [**String.split()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - takes a pattern and divides the string into an ordered list of substrings by searching for the pattern, puts these substrings into an array, and returns the array.
  * [**Datetime.format()**](https://support.attackforge.com/attackforge-enterprise/afscript#dates) - can be used to convert a date and time to a specified mask.

### Enhancements to Self-Service APIs & Events

We're always improving our Self-Service APIs and Events to make automations and integrations even easier! 💪

* **New** Events:
  * [**vulnerability-remediation-note-file-uploaded**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-note-file-uploaded) - event triggered when a file is uploaded to a remediation note.
* **Updates** to Events:
  * [**vulnerability-remediation-note-created**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-note-created) - now includes "remediation\_note\_files" and "remediation\_note\_details\_html".
  * [**vulnerability-remediation-note-updated**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-note-updated) - now includes "remediation\_note\_files" and "remediation\_note\_details\_html".
* **New** RESTful endpoints:
  * [**Upload Remediation Note File**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadremediationnotefile) - upload a file to a remediation note.
  * [**Download Remediation Note File**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadremediationnotefile) - download a file on a remediation note.
  * [**Update Remediation Note**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/createremediationnote-1) - update a remediation note on a vulnerability.
  * [**Upload Test Suite Test Case File**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadtestsuitetestcasefile) - upload a file to a test case on a test suite.
  * [**Download Test Suite Test Case File**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadtestsuitetestcasefile) - download a file on a test case on a test suite.
* **Updates** to REST endpoints:
  * [**Get Project Report Data**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectreportdata) - now supports *allowlists* (`cf_key_allowlist`) and *blocklists* (`cf_key_blocklist`) to control which custom fields are returned.
  * [**Get Test Suite**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/gettestsuite) - now returns "files".

## 30 October 2025

### Enhancements to Emails!

*...Damn. That's a good looking email!* 🤩 is what people will be saying when they receive your newly styled AttackForge emails! 🥳

We've overhauled the email notifications to provide even more flexibility, predictability and flare to your emails.

#### Script Editor With Live Preview

Gone are the days of having script in a text box, making it almost unusable to change your email templates.

Now you have the full power of a code editor with syntax highlighting and code auto-formatting - making it a breeze to whip up new emails or modify your emails on the fly!

You will get **instant feedback** in the `Preview` tab. *No more guesswork* on how the emails will look when they land in the inbox. You will have *peace of mind* even before the emails are sent.

You can also click on the `HTML` tab to inspect the code to see exactly what will be sent in the email.

<figure><img src="/files/Kr3Oxkus2Ri94RTPrMF8" alt=""><figcaption></figcaption></figure>

#### New Global Email Template Settings

We've removed all barriers - you now have *FULL CONTROL* over your email template 💪

Want to include `<meta>` tags? What about your own custom CSS stylesheets? How about links to external files and images?

Now, you're **in control** over everything which goes in to your emails.

Start making changes by visiting `Administration > Notifications > Templates > Header and Footer`

<figure><img src="/files/fACSxADPMEIa0kFGgIwF" alt=""><figcaption></figcaption></figure>

#### Configurable Daily/Weekly Emails

The Daily/Weekly Summary Emails have a lot of really useful information. The only problem before was that these emails could not be modified.

That's no longer a problem. You can now include the *exact data you need*, and style them exactly *how you need them to look*.

Start making changes by visiting `Administration > Notifications > Users > Email User on Daily Project Update` and `Administration > Notifications > Users > Email User on Weekly Project Update`

<figure><img src="/files/WfO2EZJuRf0vrBCcx2dc" alt=""><figcaption></figcaption></figure>

#### New Email Tags

We've added a whole heap of [new email tags](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags), to ensure you can always include the information you need in your emails.

### Updates to Flows

We've made our in-house [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) engine *even more powerful and easier to use*! 😍🤟

Workflow automation and integrations have never been so flexible and easy to build!

> Keep an eye out 👀 for our upcoming releases which are going to *turn Flows on its head*! 😉🤫

#### New Action Type - Script

Introducing [Script Actions](https://support.attackforge.com/attackforge-enterprise/modules/flows#script-action) - the latest addition to our arsenal of [Flow Actions](https://support.attackforge.com/attackforge-enterprise/modules/flows#actions)!

> Script Actions make it easy to *separate your business logic from your HTTP Request/Response logic*.

Script Actions can execute user-defined logic.

Script Actions can receive input from [Data](https://support.attackforge.com/attackforge-enterprise/modules/flows#data) and output Data into the proceeding Action.

In the example below:

* **Action 1 (HTTP)**: Retrieves vulnerabilities from the AttackForge Self-Serviced APIs.
* **Action 2 (Script)**: Takes the vulnerabilities from Action 1; groups them into the required format, then passes them into Action 3.
* **Action 3 (HTTP)**: Sends the formatted vulnerabilities to an external security platform.

<figure><img src="/files/K1z9ZVh1QFIqMCXMKNOs" alt=""><figcaption></figcaption></figure>

The Script Action contains a single code editor where the script can be entered in and modified.

<figure><img src="/files/qGBL4X9TjU1G5p2unKj0" alt=""><figcaption></figcaption></figure>

#### New Flows Events

We've added even more [Events](https://support.attackforge.com/attackforge-enterprise/modules/flows#internal-events) to trigger more automations and integrations when you need them!

* **Writeup Created**
* **Writeup Updated**

These events are super handy for triggering a workflow to start a QA review for a new or modified [Writeup](https://support.attackforge.com/attackforge-enterprise/modules/vulnerability-library).

They're also useful for keeping tabs on when new content is created, in case you would need to perform automated analysis or integration with *your own AI tools*, for example to improve the quality of descriptions and recommendations.

You can also leverage these events to hook into your own version control!

* **Workspace File Uploaded**

This event is perfect for notifying you as soon as a client uploads a file to the project workspace! You no longer need to anxiously keep checking for new updates 😎

* **Project Test Case Updated**

This event is great for programmatically monitoring progress on tests, to ensure testing is on track and even providing *progress updates in real time* to project stakeholders and external systems!

#### User Id in Events

[Events](https://support.attackforge.com/attackforge-enterprise/modules/flows#internal-events) now include the user id for the user which triggered the event.

This is useful if you are needing to attribute the creation or modification of a record to a person, for example:

* Notify people for who just approved or created a new project
* Let people know who just created or modified a Writeup
* Know if a pentester or a client just uploaded a file

#### Terminate a Running Flow

We all make mistakes. Nobody is perfect.

But when you make a mistake building an automation or integration - the outcome can be spoooOOOoooky 🎃 (hey, it's almost Halloween - cut us some slack).

You know what we mean - setting up an automation, your loop becomes a bit too loopy, and now someone just received 10,000 emails.

Or you're creating vulnerabilities - forgot to add an important piece of data - and now 3,000 unattributable vulnerabilities just surfaced.

Don't worry - now you can terminate a running Flow! Stop the flow dead in its tracks 🦸‍♂️ and prevent the damage from building up.

#### Download Large HTTP Responses

Are you processing vast amounts of data in your Flows?

When the data in your [Request](https://support.attackforge.com/attackforge-enterprise/modules/flows#request) or [Response](https://support.attackforge.com/attackforge-enterprise/modules/flows#response) is too big - you will now see an option to download the data directly as a file.

This makes it far easier to use the data, for searching or manual inspection.

#### Action Modals Now Display Titles

We've added the titles for all [Actions](https://support.attackforge.com/attackforge-enterprise/modules/flows#actions) to the modal when you're working on an Action. This makes it easier to keep track of which scripts you are working on - to avoid mistakes!

#### All Messages Show on Flow Run Page

We've made it possible to view the message on the outcome of a [Flow Run](https://support.attackforge.com/attackforge-enterprise/modules/flows#run-overview) - regardless of whether it was successful or not.

<figure><img src="/files/UdGIxAUG8cYT92GtWnRx" alt=""><figcaption></figcaption></figure>

#### Synack Integration

Are you working with Pentest-as-a-Service (PTaaS) providers? Do you wish you could collaborate effortlessly on vulnerabilities and retesting? 🤔

We've created fully bi-directional [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#import-synack-vulns) with [Synack](https://www.synack.com/) to help you:

* Automatically ingest vulnerabilities discovered in the Synack portal
* Automatically inform Synack when vulnerabilities are Ready for Retesting
* Automatically receive updates when vulnerabilities have been updated in the Synack portal
* Automatically close vulnerabilities when they have been closed in the Synack portal
* Automatically create Remediation Notes when new comments are posted on the vulnerability in the Synack portal

You can read more about these [Flows on our Support Portal](https://support.attackforge.com/attackforge-enterprise/modules/flows#import-synack-vulns). Or import these Flows into your AttackForge from our [Flows GitHub Repository](https://github.com/AttackForge/Flows).

<figure><img src="/files/twn7xCknPyTi4J1X6MDN" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/ahu0ghQD2Bq1bx3SRZDn" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/14XefxLuk48JLywS5Qg2" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/u1lKJsHBnKravMKSNExX" alt=""><figcaption></figcaption></figure>

> Not using Synack? Import and configure these Flows for your own PTaaS provider!

### Review Notes on Project Test Cases and Project Summary

In recent updates, we've focused on making quality assurance reviews ✅ *faster and easier* in AttackForge.

In this update, we've extended [Review Notes](https://support.attackforge.com/attackforge-enterprise/getting-started/reviewing-and-qa-vulnerabilities) to **Project Test Cases** and **Project Summary**:

<figure><img src="/files/LW2FfzftcdIAgn9U9rPb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/YwzppsvzVQe3Tkkd4qeM" alt=""><figcaption></figcaption></figure>

> Review Notes are coming soon to Writeups! 👀

### UX Improvements

#### Improved Experience When Scoring Vulnerabilities

As part of our mission to bring you the *best vulnerability scoring experience* - we've started with enhancing how CVSS Version 3 scores are captured.

You now have a modal experience which helps to:

* Paste CVSS Vector strings to automatically score the vulnerability
* Easily score, with support for metrics sections and no scrolling needed!
* Better validation and user feedback when scoring
* Access multiple scoring tabs (coming soon!)

<figure><img src="/files/XyvUe64Cwwk77ftUg8ax" alt=""><figcaption></figcaption></figure>

> Watch out for CVSS Version 4 and Build-Your-Own-Scoring-Systems coming soon!

#### Rich-Text Editor Now Supports Strikethrough and Blockquote

We've (yet again) extended the rich-text editor! This time we've added support for strikethrough and blockquote.

<figure><img src="/files/t1QVT3A4ttvJ7OaOXmfe" alt=""><figcaption></figcaption></figure>

> Strikethrough and Blockquote are also now supported in reports!

#### Project Notes Now Support Files and Inline Images

We've added support for file uploads and image previews to Project Notes! 🤩 This provides even more options on capturing and storing the right information in the right places on your projects.

<figure><img src="/files/KqmL58XBnhbgZkzfIYN1" alt=""><figcaption></figcaption></figure>

#### Workspace Items Now Support Inline Images

You can now include inline images in your workspace items!

<figure><img src="/files/3KnW5PdWrM9i86eL9kYi" alt=""><figcaption></figcaption></figure>

#### Drag and Drop Files Into Workspace Files and Testing Logs

We've extended the file upload feature to support drag and drop ability for Workspace Files and Testing Logs.

#### Workspace Files, Testing Logs and Workspace Item Files Now Support Captions

You can now add captions to files which have been uploaded to:

* Workspace Files
* Testing Logs
* Workspace Item Files

#### Copy Ids Directly From Page Titles and Page Breadcrumbs

We've made it possible to now copy ids directly from page titles and page breadcrumbs. This saves time and effort if you are working with APIs, [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) and scripts.

#### Hide Expression Support for `project.core.linked_portfolio_streams`

We've extended [Hide Expressions](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions) on Vulnerabilities to support [Portfolios and Streams](https://support.attackforge.com/attackforge-enterprise/modules/portfolios) assigned to the Project.

This means you can have custom sections and custom fields either hide or show on your vulnerability form, based on which portfolios and streams are assigned to the project you are working on.

### Asset and Portfolio Forms Upgraded

We've powered-up the [Asset](https://support.attackforge.com/attackforge-enterprise/modules/assets) and [Portfolio](https://support.attackforge.com/attackforge-enterprise/modules/portfolios) forms! 🚀

* Re-order the entire forms - have full control over where the fields are shown
* Create custom sections - group your fields into relevant sections
* Rename the default sections, or remove them entirely
* Apply [Hide Expressions](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions) against sections to hide entire sets of fields when they are not relevant, or show them when they are!

<figure><img src="/files/hoJI0xzdJDqJrCPhScgQ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Xpv7mkxljauJ3LaCVbCN" alt=""><figcaption></figcaption></figure>

### Affected Components Now Track Actioned Status

You can now use `Actioned` status for individual Affected Components on every Affected Asset on a Vulnerability!

This is useful to track exactly which components of the affected asset (API endpoint, TCP port, line of code, etc.) has been fixed, and which ones haven't.

<figure><img src="/files/VhF5rGhsphlJqxLSoa1D" alt=""><figcaption></figcaption></figure>

### Updates to ReportGen

We're always improving on our kick-ass reporting engine - **ReportGen** 🥋

#### New Stacked Bar Chart

We've added support for stacked bar charts! 📊

These charts are useful when you have multiple sources of data to represent, for example when you have vulnerabilities with different ratings (Critical, High, Medium, Low) categorized by an identifier (OWASP Top 10, CWE, CAPEC, etc.).

<figure><img src="/files/SNmTrpTQ6mW4QqEBk2H2" alt=""><figcaption></figcaption></figure>

#### Updated Resize Filter

The [Resize filter](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#resize) now supports up to 1500 pixels, for even larger in-line WYSIWYG images in reports!

#### Format Dates in Report File Names

We've added support for the `{now}` tag which can be placed in the report name configuration. This tag will be replaced when the report is generated, with an ISO 8601 date representing the current time.

You can also add a [dateFormat](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateformat) filter to adjust the representation of the date, for example `{now | dateFormat:["mm/dd/yyyy"]}`.

#### Increased Support For `"$(variable)"`

We've extended support for [**$increment**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdincrement), [**$decrement**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usddecrement), [**$includes**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdincludes), [**$assign**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdassign), and [**$equals**](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdequals) to reference dynamic variable names.

### Updates to AFScript

We've powered-up 🔋 our in-app programming language [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) to make writing scripts even easier!

* **New** functions:
  * [**String.includes()**](https://support.attackforge.com/attackforge-enterprise/afscript#strings) - performs a case-sensitive search to determine whether a given string may be found within this string, returning `true` or `false` as appropriate.
  * [**Object.entries()**](https://support.attackforge.com/attackforge-enterprise/afscript#objects) - returns an array of a given object's own enumerable string-keyed property key-value pairs.
  * [**Object.keys()**](https://support.attackforge.com/attackforge-enterprise/afscript#objects) - returns an array of a given object's own enumerable string-keyed property names.
  * [**Object.values()**](https://support.attackforge.com/attackforge-enterprise/afscript#objects) - returns an array of a given object's own enumerable string-keyed property values.

### Enhancements to Self-Service APIs

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

* **New** RESTful endpoints:
  * [**Markdown To Rich-Text**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/markdowntorichtext) - convert Markdown data to HTML data supported in the AttackForge rich-text fields.
  * [**Archive Group**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/archivegroup) - Archive a group.
  * [**Restore Group**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/restoregroup) - Restore a previously archived group.
* **Updates** to REST endpoints:
  * All GET endpoints which support custom fields now also support *allowlists* (`cf_key_allowlist`) and *blocklists* (`cf_key_blocklist`) to control which custom fields are returned.
  * [**Create Group**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/creategroup) and [**Update Group**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updategroup) - now supports configuring SSO and Group Member access controls; auto add project requests; and enabling project team notifications.
  * [**Get Group**](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getgroup) - now returns all group data, including SSO and Group Member access controls; auto add project requests config; and enable project team notifications config.

## 27 August 2025

### Scheduled Flows - For Automated Daily Tasks, Polling-Based Integrations and Cron Jobs

We're super excited to release [**Scheduled Flows**](https://support.attackforge.com/attackforge-enterprise/modules/flows#scheduled-events) which means you can now ***schedule repetitive or one-time tasks*** within AttackForge! ⏰🦾

Need a Flow to run every morning? How about the end of the month? We've got you covered! 😌

*Scheduled Flows can help you to:*

* Create polling-based integrations and data syncing with external APIs and tools
* Create batch-jobs, like daily/weekly/monthly reports on progress of your security testing program
* Schedule automated tests based on configurable policies such as criticality of assets
* Clean-up old testing data based on configurable policies such as expiration dates

<figure><img src="/files/oNtXC9Kx2VdSrCDQvENj" alt=""><figcaption></figcaption></figure>

To make Scheduled Flows super flexible, we support [cron](https://en.wikipedia.org/wiki/Cron).

Cron is the *golden standard* 🌟 when it comes to scheduling repetitive or one-time tasks.

We've built a **crontab helper** 🤖 which makes building your schedule even easier:

<figure><img src="/files/fv3XpKtGCgPC2LCLmJgK" alt=""><figcaption></figcaption></figure>

We've also added a *next run* indicator - for peace of mind that your schedule will run exactly when you need it:

<figure><img src="/files/bZspSMAfBeSlJmtecPaW" alt=""><figcaption></figcaption></figure>

You can also set which timezone to use 🌏

<figure><img src="/files/bIWxRhY3aIRyykNExLjX" alt=""><figcaption></figcaption></figure>

After your Flow is created, you can always check to see when the next run is scheduled:

<figure><img src="/files/3dPgFQacwCC23P08ghds" alt=""><figcaption></figcaption></figure>

And view the details of a specific [Flow Run](https://support.attackforge.com/attackforge-enterprise/modules/flows#runs)

<figure><img src="/files/RBJkjrOgVxdpj6AGVagQ" alt=""><figcaption></figcaption></figure>

### Delay and Repeat Flow Actions

We've added options to `delay` and `repeat` [Flow Actions](https://support.attackforge.com/attackforge-enterprise/modules/flows#actions)!

This opens *a world of possibilities* in how you can use your Flow Actions 💪

#### [Repeat Action](https://support.attackforge.com/attackforge-enterprise/modules/flows#decision)

* Create a 'for loop' Action over a list of data - for example create a new vulnerability for each record in a long list.
* Interact with paginated endpoints to retrieve the full list of results.
* Request failed; update the payload and try again.

When you repeat an action, you can also modify its context. This means you can pass in new data to the Action.

#### Action (Request)

<figure><img src="/files/SSqUHgi0qc6dwRKVfsc1" alt=""><figcaption></figcaption></figure>

#### Action (Response)

<figure><img src="/files/pPlQndmsgsvKQlq8MOGN" alt=""><figcaption></figcaption></figure>

***Example 1: Process list of data***

Say you have 100 vulnerabilities in a list. The purpose of your Action is to create a new vulnerability.

On the *first iteration* of the Action, you create vulnerability `1 out of 100` - leaving 99 more to go.

You can then repeat the Action, updating the list to remove the vulnerability which was just created.

On the *second iteration* of the Action, you create vulnerability `2 out of 100` - leaving 98 more to go.

This process repeats until you have 0 vulnerabilities left in the list to process, then you call *next* to move on to the next Action in your Flow, or *finish* to gracefully end your Flow.

***Example 2: Interact with paginated endpoints***

Say you have 1000 vulnerabilities you need to access, however the page length of the API endpoint only returns 50 at a time. The purpose of your Action is to fetch a page of vulnerabilities.

On the *first iteration* of the Action, you fetch vulnerabilities `1 to 50 out of 1000` - leaving 950 more to go.

You can then repeat the Action, updating the page marker to fetch the next page.

On the *second iteration* of the Action, you fetch vulnerabilities `51 to 100 out of 1000` - leaving 900 more to go.

This process repeats until you have 0 pages left to fetch, then you call *next* to move to the next Action in your Flow, or *finish* to gracefully end your Flow.

When an Action repeats, the logs for each iteration will be visible to you in the [Flow Run](https://support.attackforge.com/attackforge-enterprise/modules/flows#run-overview) logs.

<figure><img src="/files/V3c3esUvh5HE9vThKK8s" alt=""><figcaption></figcaption></figure>

#### [Delay Action](https://support.attackforge.com/attackforge-enterprise/modules/flows#decision)

* Safely interact with request-throttled endpoints - for example APIs with restricted number of requests per minute.
* Reduce likelihood of issues interacting with asynchronous APIs where data is not ready on time.
* External endpoint is down; retry the request with a delay until service comes back online.

Delaying an Action is *easy to do*, you just pass in a number (in milliseconds) to the [Return Statement](https://support.attackforge.com/attackforge-enterprise/modules/flows#the-return-statement).

You can delay anywhere `from 1 millisecond to 24-hours`.

***Example 1: Throttle requests to API endpoint***

Say you are working with an API which accepts 100 requests per minute. However you have 200 requests you need to make.

You can add a `1-second delay` between each repeated Action:

<figure><img src="/files/Ixqj0EnG5NXJ852cgC0t" alt=""><figcaption></figcaption></figure>

***Example 2: Delay requests until system is back online***

Say you are working with an API, however that API returns an error indicating it's currently not available.

You can add a `30-second delay` between each repeated Action until (hopefully) the service is back up and running again:

<figure><img src="/files/AkR6nevzO8nuYJNck3Wb" alt=""><figcaption></figcaption></figure>

> Keep an eye out 👀 *even more amazing new features coming to Flows soon*!

### HTML Tables in Rich-Text Fields and Reports

We've finally added support for HTML tables in rich-text fields! 🥳

You can now `create or paste tables` in your rich-text fields, and make them look beautiful in your reports 😍

You can even adjust column widths and row heights, or create lists and inline code in your tables!

<figure><img src="/files/G7GAgkgyFH8a2O4HEdrb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oz4I7nQVmR1qUcIdnFaf" alt=""><figcaption></figcaption></figure>

You can also scroll and expand the tables when viewing the data, in case you have lots of columns or rows.

And best of all - the tables will show in your reports, including the custom column widths and row heights!

You can even add [Custom Styling](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-styles#table-styling) for your HTML tables in your reports 😎

### Text Highlighting in Rich-Text Fields and Reports

We didn't just stop at HTML tables - we've also added text highlighting for rich-text fields! ✍️

You can pick from different colors to really make your text pop! 🟡🔵🔴

<figure><img src="/files/BWE9KR0yOl9izOx7h6Js" alt=""><figcaption></figcaption></figure>

The text highlights will show when you view the data in the portal and within the reports.

<figure><img src="/files/3rsHYmKMEHljJAsoiiv1" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/71rX6HmgLiBP4b3S30tU" alt=""><figcaption></figcaption></figure>

### Review Notes Improvements

In the persuit of making *QA reviews faster and easier using AttackForge* - we've made major user experience improvements to Review Notes:

* Review Notes in the *Reporting tab* on the project have been extended to include all custom fields
* Threads are now displayed as an overlay
* A preview of a note being replied to is now shown above the input
* Clicking on a reply note will scroll the note in to view and show a highlight
* Note actions (reply and delete) only appear on hover
* Notes which are a reply to another note will only show a one line preview of the reference note with a tooltip

<figure><img src="/files/ZBnSks9ethS8ac3QOsNK" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/ZfbYfbj8dBAryMJrGOQQ" alt=""><figcaption></figcaption></figure>

> Stay tuned for many more improvements coming to review features and workflows in AttackForge!

### New OWASP Test Suites

AttackForge Test Suites play an *important role* in measuring the **success**, **transparency** and **impact** for any security audit.

If *you're a pentester* - you can measure and improve the performance of your testing methodology; demonstrate the depth of manual testing coverage; and collaborate efficiently with others to save you time and effort - whilst avoiding night terrors thinking you forgot to test something!

If *you're a customer* - knowing what, when, where, who, why and how your assets were tested is crucial to measuring visibility, impact, standardization and return of investment in your security program.

In this release, we have included even more security checklists and benchmarks from OWASP:

* [**OWASP Desktop App Security Top 10 2021**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#desktop-applications)
* [**OWASP Operational Technology (OT) Top 10 2025**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#operational-technology-ot)
* [**OWASP Web Application Security Top 10 2021**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#web-application--api)
* [**OWASP API Security Top 10 2023**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#web-application--api)
* [**OWASP Mobile Application Security Testing Guide (MASTG) Version 2 2025**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#mobile-application)
* [**OWASP Mobile Top 10 2024**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#mobile-application)
* [**OWASP Top 10 CI/CD Security Risks 2023**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#cicd)
* [**OWASP Low Code/No Code Top 10 2024**](https://github.com/AttackForge/TestSuites?tab=readme-ov-file#low-codeno-code)

You can import these in to your existing or new test suites.

> You can also create your own custom testing methodologies in the Test Suites module!

<figure><img src="/files/fuWchlyTLBJvxjd4bkmv" alt=""><figcaption></figcaption></figure>

### New JIRA and ServiceNow Flows

We've added new Flows to help you with **bi-directional custom integrations and workflows** in Atlassian JIRA and ServiceNow.

You can use our [Flows Templates](https://github.com/AttackForge/Flows) to get up and running fast!

[***JIRA Issue Retest -> Update Vuln to Ready for Retest***](https://support.attackforge.com/attackforge-enterprise/modules/flows#jira-issue-retest-greater-than-update-vuln-to-ready-for-retest)

<figure><img src="/files/4GkwkiBQfvD7dg5WfCE8" alt=""><figcaption></figcaption></figure>

[***Close JIRA Issue***](https://support.attackforge.com/attackforge-enterprise/modules/flows#close-jira-issue)

<figure><img src="/files/eK79yVOlhGmpF0Kxuyd3" alt=""><figcaption></figcaption></figure>

[***Re-Open JIRA Issue***](https://support.attackforge.com/attackforge-enterprise/modules/flows#re-open-jira-issue)

<figure><img src="/files/zYYrvQJxYMFVUPlviLND" alt=""><figcaption></figcaption></figure>

[***ServiceNow Incident Retest -> Update Vuln to Ready for Retest***](https://support.attackforge.com/attackforge-enterprise/modules/flows#servicenow-incident-retest-greater-than-update-vuln-to-ready-for-retest)

<figure><img src="/files/5XTenP0VUrraDRIcjeEK" alt=""><figcaption></figcaption></figure>

[***Close ServiceNow Incident***](https://support.attackforge.com/attackforge-enterprise/modules/flows#close-servicenow-incident)

<figure><img src="/files/nWwt2V1JPdDTPEBI2UyB" alt=""><figcaption></figcaption></figure>

[***Re-Open ServiceNow Incident***](https://support.attackforge.com/attackforge-enterprise/modules/flows#re-open-servicenow-incident)

<figure><img src="/files/mXnz939TIJgDWpDbjhsI" alt=""><figcaption></figcaption></figure>

### Updates to Tenable Security Center and Invicti Netsparker Vulnerability Import Parsers

Importing vulnerabilities from Tenable and Invicti is now even better!

We've updated the `Tenable Security Center` vulnerability import parser to support even more fields from Tenable.

We've also updated the `Invicti Netsparker` vulnerability import parser to support Invicti Netsparker Enterprise.

### Advanced Filtering Now Available In More Places!

Previously we introduced [Advanced Filtering](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering) - a way to quickly and easily find the *exact information that you need*.

You can combine Advanced Filtering with [Custom Views](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#custom-views) to save your search filters and effortlessly repeat your custom search time-and-time again.

In this release, we rolled out Advanced Filtering even wider:

* Groups
* Portfolios
* Test Suites and Test Cases
* Project Test Cases
* Project Retesting
* Project Scope
* Import Vulnerabilities
* Users

We've also added a *copy* button on the table row to make it easier to copy the system id for a record, without having to view it first.

<figure><img src="/files/U9Gpwz3dSXFncqJYd5CH" alt=""><figcaption></figcaption></figure>

### Updates to ReportGen

We're always improving on our kick-ass reporting engine - **ReportGen** 🥋

#### Configure Image Downscaling in Reports

AttackForge utilizes image downscaling for dynamic reporting images to help improve performance and speed for report generation. Dynamic images are images that are uploaded to AttackForge, not statically input into your report template.

Images are downscaled by default to `975 pixels`. This is an ideal balance for most images for reporting purposes.

However, you can increase or decrease this threshold.

Increasing the Max Image Width will ***improve image quality***, however may increase report generation time.

Decreasing the Max Image Width may ***speed up report generation time***, however image quality will also be decreased.

Images can be downscaled to any value between `100 pixels and 2000 pixels`.

<figure><img src="/files/EUieRnvM0RDYTD7NHNeA" alt=""><figcaption></figcaption></figure>

#### Table Styling Options

With this release now supporting HTML tables in rich-text fields, we've also added the ability to override the style of your tables, per-field, in your report templates.

This gives you full control over how your tables look and feel in your reports!

{% code overflow="wrap" %}

```
{@<RICH-TEXT-TAG>_styled(“table_alignment”:”center”,”table_columns_width”:”5000”,”table_rows_split_across_pages”:”true”,”table_borders_thickness”:”30”,”table_borders_color”:”ff5733”,”table_cells_margin_top”:”1”,”table_cells_margin_bottom”:”1”,”table_cells_margin_left”:”1”,”table_cells_margin_right”:”1”,”table_cells_borders_thickness”:”10”,”table_cells_borders_color”:”6c3483”,”table_rows_header_style”:”CustomTableHeader”,”table_rows_header_background_color”:”00B050”,”table_rows_header_height”:”600”,”table_rows_header_vertical_alignment”:”center”,”table_rows_body_style”:”CustomTableBody”,”table_rows_body_vertical_alignment”:”center”,”table_rows_body_background_color”:”c7fcc5”,”table_rows_body_height”:”1000”)}
```

{% endcode %}

* table\_alignment - left/center/right
* table\_columns\_width - pixels e.g. 5000
* table\_rows\_split\_across\_pages - true/false
* table\_borders\_thickness - pixels e.g. 30
* table\_borders\_color - hex color code e.g. 000000
* table\_cells\_margin\_top - pixels e.g. 1
* table\_cells\_margin\_bottom - pixels e.g. 1
* table\_cells\_margin\_left - pixels e.g. 1
* table\_cells\_margin\_right - pixels e.g. 1
* table\_cells\_borders\_thickness - pixels e.g. 10
* table\_cells\_borders\_color - hex color code e.g. 000000
* table\_rows\_header\_style - maps to a Word style (don't put whitespace in the style name)
* table\_rows\_header\_background\_color - hex color code e.g. 000000
* table\_rows\_header\_height - pixels e.g. 600
* table\_rows\_header\_vertical\_alignment - top/center/bottom
* table\_rows\_body\_style - maps to a Word style (don't put whitespace in the style name)
* table\_rows\_body\_vertical\_alignment - top/center/bottom
* table\_rows\_body\_background\_color - hex color code e.g. 000000
* table\_rows\_body\_height - pixels e.g. 1000

#### New Filter: Array\_Chunk

If you've ever found yourself adding a list of data in your report, and thinking *there's way too much whitespace. It would look so much better in a table with multiple columns... but how to make the columns adjust to the length of my data?*

Well now you're in luck 🍀

We've added a new filter [Array\_Chunk](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#array_chunk) which can be used to create an array of elements split into groups the length of `size`. If the array can't be split evenly, the final chunk will be the remaining elements. Each array is then accessible inside the parent loop via `chunk[index]`.

```
array_chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
 
array_chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]

{#tag | array_chunk:<size>}{chunk[0]}{chunk[1]}{chunk[...]}{/}
```

For example, say you wanted to distribute your project scope assets across three (3) columns to help save whitespace in the report - you could do the following:

<figure><img src="/files/cF0zWG4jnArPjhZqpWtG" alt=""><figcaption></figcaption></figure>

The result will be the distribution of each asset across three columns:

<figure><img src="/files/TFkaAVRAZIDrze8ddkcS" alt=""><figcaption></figcaption></figure>

#### Ignore Labels with 0 Value

We've added a new chart option which helps to remove labels from charts which have a `0` value.

For example - the following chart has 0 Critical, 0 High and 0 Medium findings:

<figure><img src="/files/pTcg2xM9suKVjgBO2mL5" alt=""><figcaption></figcaption></figure>

By specifying `"ignore_falsy_values": true,` inside the `"data": {...}"` object of your chart settings (e.g. `"data": {"ignore_falsy_values": true,...}"`) the chart will then ignore labels with a 0 value.

<figure><img src="/files/s0N0W7KeOMnfjv9Kbg8G" alt=""><figcaption></figcaption></figure>

### Enhancements to Self-Service APIs

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

* **New** RESTful endpoints:
  * [Update Linked Projects on Vulnerabilities](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updatelinkedprojectsonvulnerabilities) - link or unlink projects to vulnerabilities.
  * [Get Asset Library Assets](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getassetsinlibrary) - search assets in the library using POST in case you have hundreds/thousands of assets in your custom filter
  * [Destroy Projects](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/destroyproject) - destroy all data on a project. Spare logs - or don't - i'm a release note, not a cop.
* **Updates** to REST endpoints:
  * [Get Test Suites](http://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/gettestsuites) - we've added support for the test suite code and sort order.
  * [Get Test Suite](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/gettestsuite) - we've added support for the test suite code and sort order, as well as for every individual test case.

## 11 July 2025

### Build Your Own Custom APIs!

We're super pumped to release **HTTP Triggers** for [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) which means you can now ***build your own custom APIs*** in AttackForge! 🤩💪

Why would you want to build your own custom APIs you might ask? 🤔

Well, for starters:

* Your external systems can push data directly to AttackForge in real-time, whenever you need it!
* You can run an automation in AttackForge on your own terms
* You can create custom integrations which are triggered on-demand
* You can create modularized Flows (trigger them from other Flows!)

Still not quite sure where to start with this superpower? 🦸 We've got you covered!

We created an example video demonstrating how *a security researcher can submit a new vulnerability in* [*HackerOne*](https://www.hackerone.com/) *and it's in AttackForge instantly!*

> No delays = No unnecessary risk exposure!

{% embed url="<https://youtu.be/lLhXPshWe1g?si=YUr1qW3Eu4knAE1n>" %}

You can create your own custom workflows, for example:

* Fetch information about the affected asset from an external CMDB or from within the AttackForge [Assets Library](https://support.attackforge.com/attackforge-enterprise/modules/assets), and use this information for *contextualized prioritization and risk scoring*
* Custom score the vulnerability using *threat and vulnerability intelligence data* from external databases like Flashpoint's [VulnDB](https://flashpoint.io/ignite/vulnerability-intelligence/)
* *Enrich the vulnerability* with [CWE](https://cwe.mitre.org/) data using the [MITRE CWE API](https://github.com/CWE-CAPEC/REST-API-wg) - descriptions, remediation advice, references, etc.
* *Create tickets in external tools* like [Atlassian JIRA](https://www.atlassian.com/software/jira), [ServiceNow](https://www.servicenow.com/), [Azure DevOps](https://azure.microsoft.com/en-us/products/devops) and others
* Trigger conditional email notifications for *automatic escalations*
* *Post a message* to [Slack](https://slack.com/), [Teams](https://teams.live.com/) or other collaboration tools

> This is just a taste for what powered-up Flows can now do!

<figure><img src="/files/uCmcSDtJcYKY1KLhf9kR" alt=""><figcaption></figcaption></figure>

Every [HTTP Triggered Flow](https://support.attackforge.com/attackforge-enterprise/modules/flows#external-events) comes with the following:

* A [dedicated URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) to receive your instructions and launch your Flow (you can even rotate it too!)
* Configurable [HTTP Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods)
* Option to [enable and control Authentication and Authorization](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-authentication)
* Ability to control input into the Flow, and output from the Flow
* Unlimited number of [Actions](https://support.attackforge.com/attackforge-enterprise/modules/flows#actions) performed by your Flow
* Support for custom scripting using [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript)!
* Support for [Secrets](https://support.attackforge.com/attackforge-enterprise/modules/flows#secrets)
* Whitelist or blacklist [HTTP Headers](https://support.attackforge.com/attackforge-enterprise/modules/flows#trigger-configuration)

We've also added a README section for each Flow, so now you can document how your Flow works - and make that documentation available to others!

<figure><img src="/files/nQl89Ztonw1J4PM6hzpx" alt=""><figcaption></figcaption></figure>

We've also added [new Flows access controls](https://support.attackforge.com/attackforge-enterprise/modules/flows#getting-access-to-flows) for even more granular access on how your users can use different types of Flows.

<figure><img src="/files/3qDWKZOq6sIYiFsvwBfh" alt=""><figcaption></figcaption></figure>

And we didn't stop there - we extended the [Flow permissions](https://support.attackforge.com/attackforge-enterprise/modules/flows#sharing-flows-with-teams) so you have even more granular control for how each individual Flow can be used by every authorized user.

<figure><img src="/files/qPWEempigskr8nzF90RX" alt=""><figcaption></figcaption></figure>

We also improved the user experience with [Secrets](https://support.attackforge.com/attackforge-enterprise/modules/flows#secrets) to make them even easier to use, and more accessible where you need them!

And we are not done with Flows just yet 😉 stay tuned for even more exciting Flows updates coming in the next release!

### Portfolios and Stream Analytics Now Available

[Portfolios](https://support.attackforge.com/attackforge-enterprise/modules/portfolios) has been instrumental for many security teams and organizations - helping them to track and monitor security for their:

* Business Units, Subsidiaries and Functional Teams
* Applications, Systems and Technologies
* Geographies
* Compliance obligations
* Security Programs
* Mergers and Acquisitions

We've now added Analytics for every Portfolio and Stream - providing *even better tracking, monitoring, reporting and coverage!*

Answer the tough questions easily:

* *What are the most common vulnerabilities?*
* *Which assets need the most attention?*
* *What are the root causes for these vulnerabilities?*
* *Are we getter better, or are we getting worse?*

<figure><img src="/files/sDCsfJmI63SWsVugf1W3" alt=""><figcaption></figcaption></figure>

### DRAFTS Now Available for Writeups, Project Requests, Assets and Test Cases

In the last release, we introduced DRAFTS - a game-changer for being more *effective and efficient* with how you create data, putting you *in control* of when to publish.

DRAFTS ensure you *never lose your data* with the real-time autosave feature - so you always have peace of mind knowing that we got you covered. Stash your data, come back to it when you need it (or dont!). Work on multiple concurrent DRAFTS.

In this release, we rolled out DRAFTS even wider:

* Writeups
* Assets
* Test Cases
* Project Requests

<figure><img src="/files/pRNTkRxsXtpMYfAd607s" alt=""><figcaption></figcaption></figure>

DRAFTS are coming to even more places soon, including a central location where you can find all of your DRAFTS for everything in one place!

### Review Notes Improvements

We've improved [Review Notes](https://support.attackforge.com/attackforge-enterprise/getting-started/reviewing-and-qa-vulnerabilities)! They now support *every system and custom field* - you can even start a thread on multiple topics!

<figure><img src="/files/IJdEy2MFHBs1Ti6ZlBck" alt=""><figcaption></figcaption></figure>

Stay tuned for other exciting updates coming soon for QA and Reviews!

### Advanced Filtering Now Available for Writeups, Assets and Project Requests

In the last release, we introduced [Advanced Filtering](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering) - a way to quickly and easily find the *exact information that you need*.

You can combine Advanced Filtering with [Custom Views](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#custom-views) to save your search filters and effortlessly repeat your custom search time-and-time again.

In this release, we rolled out Advanced Filtering even wider:

* Writeups
* Assets
* Projects
* Project Requests

We also extended Advanced Filtering to even more field types!

<figure><img src="/files/OJN7DazA9r1fq91scWQI" alt=""><figcaption></figcaption></figure>

### User Session Improvements

We know this is long overdue, but it's *finally* here!

We've overhauled how we manage user interactivity - to give you the best possible user experience when it comes to your active session!

We now *detect mouse clicks and keyboard strokes* to help keep your session alive. This means you will no longer timeout when writing detailed vulnerability writeups, or interacting with any part of the application!

We've also included a configurable session inactivity timeout warning. By default, this message will show when you have 1 minute left on your session.

<figure><img src="/files/N3xVjLNXmbByZV657Gk1" alt=""><figcaption></figcaption></figure>

When your session times out, you will see a pop up message in the center of the screen.

<figure><img src="/files/YnQI6MQTwlxEEPXIa3WH" alt=""><figcaption></figcaption></figure>

If you click on the `X` - you will be able to copy any data already loaded or entered into your page.

<figure><img src="/files/YBsMK2IOdXYNgsVuhJcY" alt=""><figcaption></figcaption></figure>

You can configure when the timeout warning pops up, and also whether you want users to be automatically redirected to login after session timeout or not.

<figure><img src="/files/6xGBHV3OOy6opfM4s1Gz" alt=""><figcaption></figcaption></figure>

### User Invitation Improvements

Now when you invite a new user to AttackForge, you can include their first name and last name to make the invitation more personal, and save them the hassle filling it in later!

### AFScript Improvements

We've added support for [HMAC](https://en.wikipedia.org/wiki/HMAC) signature verification in [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript)!

HMAC can provide authentication using a shared secret. This can be used to help verify both the data integrity and authenticity of a message.

This is ideal for use cases where you might want to verify a payload in [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) and cannot rely on using [HTTP Trigger Authentication](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-authentication).

### Enhancements to Self-Service APIs

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

* **New** RESTful endpoints:
  * [Delete Vulnerability Evidence](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadvulnerabilityevidence) - delete a file uploaded to a vulnerability
* **Updates** to REST endpoints:
  * [Update Project](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateproject) - we've added support for updating [Reporting Custom Fields](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#reporting-custom-fields) and [Summary Page Custom Fields](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#summary-page-custom-fields)

## 28 May 2025

### Advanced Filtering in Tables

You can now do [Advanced Filtering](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering) in tables! 🤩

Advanced filtering can help you *quickly and easily find the information you need*.

You can combine Advanced Filtering with [Custom Views](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#custom-views) to save your search filters and effortlessly repeat your custom search time-and-time again.

<figure><img src="/files/sxQWNKg5nkYElJ5dD7ht" alt=""><figcaption></figcaption></figure>

So what makes it **advanced**? 🧐 Let's take a look:

* [Contains](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#contains)
  * Does not contain
  * Match whole word
  * Match case
* [Starts With](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#starts-with)
  * Does not contain
  * Match whole word
  * Match case
* [Ends With](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#ends-with)
  * Does not contain
  * Match whole word
  * Match case
* [Wildcard / Globbing](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#wildcard) 💪
  * Does not
  * Match case
* [Regular Expressions (RegEx)](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#regex) 🚀
  * Does not
  * Match case

> Advanced Filtering is available in the *Filters* section of your table, or when selecting an individual column filter.

You can now apply a filter which will return the **exact data** you need in your table! Advanced filters can be applied across multiple fields. You can also set multiple filters on the same field!

When you need the extra power or flexibility - [Wildcards / Globbing](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#wildcard) and [Regular Expressions](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#regex) come to the rescue!

The Wildcard filter is used to perform Glob matching or [globbing](https://en.wikipedia.org/wiki/Glob_\(programming\)#Syntax) using wildcards (like `*` and `?`) to match values based on patterns.

The following globs are supported:

* Wildcards (`**`, `*.js`)
* Negation (`'!a/*.js'`, `'*!(b).js'`)
* [extglobs](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#extglobs) (`+(x|y)`, `!(a|b)`)
* POSIX character classes (`[[:alpha:][:digit:]]`)
* [brace expansion](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#brace-expansion) (`foo/{1..5}.md`, `bar/{a,b,c}.js`)
* [regex character classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class) (`foo-[1-5].js`)
* regex logical "or" (`foo/(abc|xyz).js`)

Examples using Wildcards:

```
1. Filtering for xss OR cross site scripting anywhere in the Vulnerability title
```

`*xss*|*cross site scripting*`

```
2. Filtering for NOT 'HTTP' AND NOT 'MIME' anywhere in the Vulnerability title
```

`!(*HTTP*|*MIME*)`

When a Wildcard just isn't enough, Regular Expressions (RegEx) unlock a new level of possibilities!

<figure><img src="/files/f0j0UfxO7CQxP0Zv4XnC" alt=""><figcaption></figcaption></figure>

Regular Expressions are widely used due to their ability to perform powerful text manipulation and pattern matching. When using Regular Expressions, you're only limited by your creativity!

If you're unfamiliar with RegEx, there's hundreds of online tools to help you build and test your regular expressions to ensure they work exactly how you expect them to.

> Advanced Filters can be saved in your [Custom Views](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#custom-views).

Advanced Filtering is now supported on Vulnerabilities and is coming to more tables very soon!

### Vulnerability Drafts

Sick and tired of working long and hard on writing vulnerabilities, only to lose them because your session timed out?

What about when you need to stash a vulnerability and come back to it later?

Or you're simply *not ready to share it* for others to see?

Vulnerability drafts solve these painful problems - plus more!

When working on a new vulnerability, **every change you make is autosaved**. Any field you enter in data or change, any file you upload - is now saved against your draft.

<figure><img src="/files/FNzH7Xep4u02DNxx1Bo0" alt=""><figcaption></figcaption></figure>

You can see all of your drafts by clicking on *Draft Vulnerabilities* when filtering your vulnerability views.

You can click on any of your drafts to resume from exactly where you left off.

<figure><img src="/files/8SKVi0dVkK8iaMvr6vnb" alt=""><figcaption></figcaption></figure>

Drafts are currently only on Vulnerabilities, however will be supported wider very soon!

### Groups Now Support Custom Fields

We've added Custom Fields to Groups! 🥳

This means you can now capture information about your **clients, teams, business units, platforms, technologies and more** - store that information in a centralized location with *field-level access controls*, and use that information in:

* [Reports](https://support.attackforge.com/attackforge-enterprise/modules/reporting)
* [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows)
* [APIs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api)
* [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript)
* [Advanced Filters](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering)
* [Notifications](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications)
* [SLAs](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-slas)

<figure><img src="/files/o63IBJ1dlArb9l5qsGKC" alt=""><figcaption></figcaption></figure>

> You can also create custom sections and re-order your form to exactly how you want it!

### Writeups Now Support Form Configuration

We've powered-up the Writeups form! 🔋

* Re-order the entire form - have full control over where the fields are shown
* Create custom sections - group your fields in to relevant sections
* Rename the default sections, or remove them entirely
* Apply [Hide Expressions](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions) against sections to hide entire sets of fields when they are not relevant, or show them when they are!

<figure><img src="/files/DoTBaUkIa0alsEDXK87U" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/s7YYm8BcQ1RO9CaroTD4" alt=""><figcaption></figcaption></figure>

### Custom Views Now Supported Across Application

Previously we brought you [Custom Views](https://support.attackforge.com/attackforge-enterprise/getting-started/advanced-filtering#custom-views) - a way to make it easy to save your favourite and frequently used table views.

Custom Views help to *easily configure and switch between different views of your data*, providing flexibility in how you want to see and use your data.

We've extended Custom Views across the application, so you can benefit from them in even more places!

* Vulnerability tables
* Project tables
* Scheduling calendar and tables
* Project Request tables
* Project Test Cases table
* Project Scope tables
* Assets tables
* Portfolio and Stream tables
* Groups table
* Writeups tables
* Test Suites tables
* Test Cases tables
* Users table

<figure><img src="/files/yMM3JJK2ZMUYtPBpUkK5" alt=""><figcaption></figcaption></figure>

### New Red Team Reporting Template

We've released a new **Red Team Reporting Template** to help you bootstrap a red-team report, fast! 🏃‍♂️‍➡️

Red Team reports != Pentest reports. They're structured differently. They tell a different story. This is why they need to be built to handle the unique needs of red teams.

To get started - download the *AttackForge Red Team Report* [Example](https://attackforge.com/attackforge-core-enterprise/report-templates/af-red-team-report-v3-0-example.docx), [Template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-red-team-report-v3-0-template.docx) and [Test Data](https://attackforge.com/attackforge-core-enterprise/report-templates/af-red-team-report-test-data.json).

You can build the report and experiment with the template in [ReportGen](https://www.attackforge.com/reportgen.html).

> You can download ReportGen locally from **Report Templates > ReportGen > Offline Browser Tool**

<figure><img src="/files/aUN6ZfGueYRSZDwa6mis" alt=""><figcaption></figcaption></figure>

### New Powerpoint-Style Template

We've also released a new **Powerpoint-styled Executive Summary Presentation Template** to help you create a slick presentation to wow your clients! 😎

To get started - download the *AttackForge Exec Summary Slide Deck* [Example](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-exec-summary-slide-deck-example.pdf), [Template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-exec-summary-slide-deck-template.docx) and [Test Data](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-exec-summary-slide-deck-test-data.json).

You can build the presentation and experiment with the template in [ReportGen](https://www.attackforge.com/reportgen.html).

> You can download ReportGen locally from **Report Templates > ReportGen > Offline Browser Tool**

<figure><img src="/files/SpdS5kOEx3hSRuCgT7pM" alt=""><figcaption></figcaption></figure>

### MITRE ATLAS Framework Now Available

If you're testing AI Systems, you can now import the [MITRE ATLAS](https://atlas.mitre.org/) framework into your AttackForge as either [Test Suites](https://github.com/AttackForge/TestSuites) or [Writeups](https://github.com/AttackForge/Writeups)!

This gives you an out-of-the-box testing methodology and vulnerability language for assessing AI Systems.

> Download the MITRE ATLAS framework from our [GitHub](https://github.com/AttackForge) and conveniently import what you need via the UI

<figure><img src="/files/tAr4iYc5xztQu3NIcGN8" alt=""><figcaption></figcaption></figure>

### New Flows Videos

[Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) has been a game changer 🚀🌝 since its release in February.

Flows opened up a **world of automation and integration possibilities!**

Some examples we have seen implemented so far:

* Exporting vulnerabilities into enterprise ticketing systems such as ServiceNow, JIRA, Azure DevOps
* Real-time AttackForge data feeds into visualization tools like PowerBi
* Custom prioritization of vulnerabilities
* Leverage threat and vulnerability intelligence feeds to augment vulnerability data
* Integrations to schedule and kick-off automated scanning activities Tenable and Qualys
* Posting chat messages in Slack and Teams
* Email notifications on custom QA reviews and workflows
* Allow selected clients to self risk-accept their vulnerabilities
* Email notifications on custom criteria for vulnerabilities
* Create custom webhooks

We've added tutorial videos for some of the most common Flows to help you get started fast!

{% embed url="<https://youtu.be/onvSZShtEB4?si=AOrjg-ohvEkS3-4T>" %}

{% embed url="<https://youtu.be/-BfrTnCIoi0?si=Cdg8yWI4KEikuM5U>" %}

{% embed url="<https://youtu.be/2bps7vEcmVA?si=kQSZr0CjqCSgsaox>" %}

{% embed url="<https://youtu.be/eV1qxzcJ2Do?si=gKYxSsETi7NYTUia>" %}

{% embed url="<https://youtu.be/HHEZNcKFOKA?si=S_EDFMy6cMltJq_P>" %}

{% embed url="<https://youtu.be/atT4u8HHp4s?si=qfEQUBSS417Gxowq>" %}

{% embed url="<https://youtu.be/UxByLi-wNKI?si=2lsRy2_9A1D_Yvhl>" %}

{% embed url="<https://youtu.be/vrplMhqzLNk?si=gk9htwNGZGo17uu0>" %}

{% embed url="<https://youtu.be/15KbJKPVrtM?si=42Ym4J4fbpa1r0G->" %}

{% embed url="<https://youtu.be/TKUJrk5WVvo?si=ZSDbv6mKKOr1YfVQ>" %}

{% embed url="<https://youtu.be/qnlhLAT41f4?si=rteLRvLZ9FXT7Uoj>" %}

### Flows - User Secrets Now Available

If you're building Flows and thinking *...geez it would be awesome to have a centralized secret, that way I can update it in one place and all my flows will be updated*, or you might be thinking *...geez it would be awesome if I had a way to prevent other people from seeing my secrets even if they have access to my flow* - then you're in luck 😉

We just added [User Secrets](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#secrets) - a place where you can create secrets which belong to you and not your individual Flow. That way you can share your secret across multiple Flows, and you can protect it from other users seeing your secrets' value.

You can also choose to share your secret with other users if you would need to.

<figure><img src="/files/hm2Jg6YR50GD1dpc8dLI" alt=""><figcaption></figcaption></figure>

### Flows - Transfer of Ownership Now Available

If you've just went through all of the effort for building a Flow, and now you need to hand it over to someone else so it runs under their context (not yours) - you can now [Transfer Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows).

Transferring is easy. Open the Flow settings page and click on *Transfer*. Enter in the user to recieve your Flow then approve. That's it!

The user will receive ownership of your Flow immediately, however it will be *disabled* until they have reviewed the Flow (to make sure it's not dodgy 😉) and decide to enable it themselves.

### Project Request Scope Now Supports Asset Selection

When a user requests a new project, previously the only option was to allow them to enter in any arbitrary scope they can think of. This makes it hard for security teams to link that information to *real assets* in your [Assets](https://support.attackforge.com/attackforge-enterprise/modules/assets) module.

You can now switch the Scope field on the Project Request to an Asset selector.

This can be toggled from `Administration > Project Requests > Form > Scope > Field Type (Text/Asset)`

The user will only see assets which they already have access to via the [Assets](https://support.attackforge.com/attackforge-enterprise/modules/assets) module.

This means that any [Asset Libraries](https://support.attackforge.com/attackforge-enterprise/modules/assets#grouping-and-managing-access-to-assets) and access controls on those libraries will be enforced.

Users can only select from *their assets*, or create a new asset (if permitted to do so).

<figure><img src="/files/6KfdXjxS5wgSk3lWmcsm" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/B0D1g7SLpdBj8L2gutVl" alt=""><figcaption></figcaption></figure>

We've also improved the user experience when approving the project request, so that it's easier to reconcile requested project scope against similarly named assets in different libraries. We also now highlight any differences and have an easy option to create new assets when needed.

### Custom Email Distribution Lists

The [Custom Time-Based Email Notification Engine](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-time-based-emails) is a god-send for use cases where you might need AttackForge to:

* Remind people about vulnerabilities about to breach their Remediation SLA and/or Remediation Plan
* Escalate to people about vulnerabilities which have breached their Remediation SLA and/or Remediation Plan
* Remind people about overrunning projects
* Remind people about unactioned project requests
* Remind people their account will lock out if they don't log in soon
* Get a digest of all vulnerabilities or writeups that need to be reviewed
* Plus many more use cases you can think of!

We've now extended these emails to also support custom distribution lists! 🤩

This means you can now use any **user(s) or group(s)** [Custom Fields](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms) as distribution lists for these emails.

For example, if you have **Vulnerability Owner** or **Teams Responsible** custom fields configured on your vulnerabilities, you can now include those recipients within these emails.

<figure><img src="/files/YOnsmaMShO9iisZVMXwL" alt=""><figcaption></figcaption></figure>

> You can leverage **any** *User Select*, *User Multi-Select*, *Group Select* or *Group Multi-Select* custom fields

### Skip Access Control in Custom Time-Based Emails

We've also added the option to *Skip Access Control Checks* in [Custom Time-Based Emails](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-time-based-emails). This means that if the user doesn't have access to the data record, for example the project or the vulnerability, then it will still include that record in the email.

This is ideal for scenarios where you have generic mailboxes which need to have all information in one place; or you need users to be made aware of authorized information in AttackForge for which they would not normally have access to within the application.

### Pending Vulns Now Supported in Custom Time-Based Emails

The [Custom Time-Based Emails](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-time-based-emails) also now support **Pending** vulnerabilities.

You can now create emails which contain a list of pending vulnerabilities which need to be reviewed or subjected to quality assurance reviews - *making it way easier to know when you need to do QA*!

### Create Projects Without Scope or Test Suites

We've made it possible to now create projects *without any scope or test suites* selected.

This makes it easier to set up new projects and complete the information later on once it's known.

### Retest Rounds Now Support All Vulns

When [Requesting a Retest Round](https://support.attackforge.com/attackforge-enterprise/getting-started/retesting#request-a-retest), you can now select *any* of the vulnerabilities on the project to be considered in-scope for the retest, not just the vulnerabilities which are marked as *Ready for Retest*.

This makes it far easier for your teams to request retests, and saves on the extra steps needed to first mark those vulnerabilities as Ready for Retest.

### Linked Project Keys Now Supports All Project Fields

When building your custom project request and intake form, you can now easily map all of your desired *Project Request fields* to *Project fields*.

We've extended the **Linked Project Key** feature to now show all fields which you can map to. This includes *System* and *Custom* fields.

<figure><img src="/files/u9BMo6zXPH01dUZsR1uo" alt=""><figcaption></figcaption></figure>

### Updates to ReportGen

Charts now support modifying the base chart color, for example you can make the lines and labels white if you have a dark background in your reporting template.

### Enhancements to Self-Service APIs

We're always improving our Self-Service APIs to make automations and integrations even easier! 💪

* **New** RESTful endpoints:
  * [Request New Project Retest](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/requestnewprojectretest) - new & improved API to request a retest on a project
  * [Complete Project Retest](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/completeprojectretestround) - new & improved API to complete a retest on a project
  * [Cancel Project Retest](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/cancelprojectretestround) - new & improved API to cancel a retest on a project
  * [Update Project Retest Round](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateprojectretestround) - new API to update a retest round on a project
  * [Get Project Membership Administrators](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectmembershipadministrators) - new API to get all project membership administrators on a project
  * [Update Project Membership Administrators](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateprojectmembershipadministrators-1) - new API to update project membership administrators on a project
  * [Add Project Membership Administrators](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/addprojectmembershipadministrators) - new API to add project membership administrators on a project
  * [Remove Project Membership Administrators](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateprojectmembershipadministrators) - new API to remove project membership administrators on a project
  * [Download Project Test Case File](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadprojecttestcasefile) - new API to download a file on a project test case
  * [Download Project Test Case Note File](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadprojecttestcasenotefile) - new API to download a file on note created on a project test case
  * [Download Project Test Case Workspace Note File](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadprojecttestcaseworkspacenotefile) - new API to download a file on workspace note created on a project test case
* **Updates** to REST endpoints:
  * [Get Groups](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getgroups) - now supports custom fields
  * [Get Group](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getgroup) - now supports custom fields
  * [Get Assets In Library](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getassetsinlibrary) - now supports `archived=true` query parameter to search archived assets
  * [Update Asset In Library](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateassetinlibrary) - now supports `is_archived: true` body parameter to archive/unarchive assets
  * [Get Project Test Cases](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojecttestcases) - now returns "uploaded\_files" on notes, workspace notes and the test case itself.

## 28 February 2025

### Introducing Flows - Automation Module for AttackForge

We're super excited 🤩 to finally reveal [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) - AttackForge's comprehensive, end-to-end **automation engine** - powered by [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript).

{% embed url="<https://www.youtube.com/watch?v=tTbq4Dbh-A8&t=1s>" %}

Flows can help you to automate AttackForge with *nearly unlimited systems*. You can streamline processes across your organization to save time and focus on what's most important.

Some examples you can do with Flows:

* **Integrate your vulnerability data** with ticketing tools like [Atlassian JIRA](https://www.atlassian.com/software/jira), [ServiceNow](https://www.servicenow.com/), [Azure DevOps](https://azure.microsoft.com/en-us/products/devops), [BMC Helix](https://www.bmc.com/it-solutions/bmc-helix.html) and others.
* **Visualize your pentesting data** in powerful tools like [Power BI](https://www.microsoft.com/en-us/power-platform/products/power-bi) and [Tableau](https://www.tableau.com/)
* Help make **better risk decisions** by sending your vulnerability data to GRC platforms like [RSA Archer](https://www.archerirm.com/), [MetricStream](https://www.metricstream.com/), [OneTrust](https://www.onetrust.com/) and [LogicGate](https://www.logicgate.com/)
* Create **workflow automations** by chaining together [AttackForge Self-Service APIs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api)
* Trigger **automated scanning** activities in your security toolset like [Rapid7](https://www.rapid7.com/), [Tenable](https://www.tenable.com/) and [Qualys](https://www.qualys.com/)
* Create **messages** on collaboration platforms like [Slack](https://slack.com/intl/en-au/) and [Teams](https://www.microsoft.com/en-au/microsoft-teams/group-chat-software)
* **Prioritize vulnerabilities with threat-intelligence** like [VulnDB](https://flashpoint.io/ignite/vulnerability-intelligence/)
* Create **custom webhooks**
* Send **custom email notifications** on events

> Flows can interact with any HTTP interface, including your own tools as well as the [AttackForge Self-Service APIs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api)

<figure><img src="/files/dUZfOCCm4Ts5sGVFH24m" alt=""><figcaption></figcaption></figure>

Flows is included in all AttackForge Enterprise plans and in the AttackForge Core SME plan. For all others plans, Flows can be added-on from the `Administration -> Subscriptions` page.

We've made some Flows available on [our GitHub](https://github.com/AttackForge/Flows) which you can [import into your AttackForge](https://support.attackforge.com/attackforge-enterprise/modules/flows#importing-exporting-flows) to get started fast! Examples include:

* [Create JIRA Issue](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-jira-issue)
* [Update JIRA Issue](https://support.attackforge.com/attackforge-enterprise/modules/flows#update-jira-issue)
* [Create ServiceNow Incident](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-servicenow-incident)
* [Create Azure DevOps Work Item](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-azure-devops-work-item)
* [Prioritize Vulnerability with Threat Intelligence from VulnDB](https://support.attackforge.com/attackforge-enterprise/modules/flows#prioritize-vulnerability-with-threat-intelligence-from-vulndb)
* [Trigger an Automated Scan in Tenable](https://support.attackforge.com/attackforge-enterprise/modules/flows#trigger-an-automated-scan-in-tenable)
* [Create Slack Message](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-slack-message)
* [Create Teams Message](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-teams-message)
* [Send Vulnerability to PowerBI](https://support.attackforge.com/attackforge-enterprise/modules/flows#send-vulnerability-to-powerbi)
* [Create a Salesforce Opportunity](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-salesforce-opportunity)
* [Create a WebHook](https://support.attackforge.com/attackforge-enterprise/modules/flows#create-a-webhook)
* [Send a Custom Email](https://support.attackforge.com/attackforge-enterprise/modules/flows#send-custom-email)

<figure><img src="/files/eq1GMI7HURxxiFb1b294" alt=""><figcaption></figcaption></figure>

Some of the great features of Flows include:

* [Share your Flows with multiple team members](https://support.attackforge.com/attackforge-enterprise/modules/flows#sharing-flows-with-teams) to help collaborate on your Flows together
* [Import and Export Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#importing-exporting-flows) to get started fast! You can also to share your Flows with other people using AttackForge
* Various [Triggers](https://support.attackforge.com/attackforge-enterprise/modules/flows#triggers) to handle a variety of use cases and bespoke needs
* Support for [Secrets](https://support.attackforge.com/attackforge-enterprise/modules/flows#secrets) to protect your passwords, API tokens and keys
* Powerful [Actions](https://support.attackforge.com/attackforge-enterprise/modules/flows#actions). Create your own custom logic and decision paths using [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript)
* [Run History](https://support.attackforge.com/attackforge-enterprise/modules/flows#runs) for robust testing. Tracking and monitoring for peace of mind. Access detailed logs to know exactly what your Flows are doing at any time.
* Unlimited Flows and Flow Actions! Build as many automations and integrations as you need, start with simple Flows and level up to complex sequences and chains.

<figure><img src="/files/rYS3vDorlJ4frwPPEUS6" alt=""><figcaption></figcaption></figure>

We will be releasing even more capabilities and examples for Flows in the coming months so stay tuned!

### Custom Domains for AttackForge Core

We have released fully custom domain names for AttackForge Core!

You can now have your own personal domain for hosting and accessing your AttackForge tenant.

> Currently only available on AttackForge Core SME plans

### Updates to AFScript

We've extended [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) yet again!

You can now suggest values across all of your custom fields, everywhere, using AFScript.

We've also added `String.replace()` and `String.replaceAll()` functions.

### Updates to Groups

We've updated [Groups](https://support.attackforge.com/attackforge-enterprise/modules/groups) to now support [Custom Fields and Forms](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms).

This makes to possible to have a custom set of fields and forms for different Groups such as customers, security teams, technology and engineering teams, risk teams, subsidiaries and divisions, platforms and other ways in which you use groups.

You can configure Group custom fields and forms from `Administration -> Groups`.

In addition, we added support for mapping AttackForge Groups to SSO Identity Provider Groups to include support for assigning no access to the groups projects, as well as support for assigning access to [Project Requests](https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project).

### Updates to Custom Time-Based Notifications

We've added support for ability to use the [dateFormat](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateformat) filter to adjust the way the date and time is displayed in your custom time-based emails.

For example, you can now do this:

`{project.start_date | dateFormat:["fullDate"]}`

### Updates to ReportGen

We added new [Filters](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters) including [isArray](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#isarray), [isBoolean](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#isboolean), [isInteger](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#isinteger), and [isString](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#isstring).

We also updated the styling and layout for the out-of-the-box [Pentest Report Template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-report-v3-4-example.docx) to showcase even more possibilities for custom reports in ReportGen!

We also added a `Retry` button in the [Offline Browser Tool](https://www.attackforge.com/reportgen.html) from Chrome users which makes building reports even faster! 🥳 You no longer need to re-select the template file or JSON data.

<figure><img src="/files/vd8c8IF4Tej34RGFiNU6" alt=""><figcaption></figcaption></figure>

### Updates to Writeups Libraries and Test Suites

We've added MITRE ATT\&CK Framework for [Writeups](https://support.attackforge.com/attackforge-enterprise/modules/vulnerability-library)! Including [Enterprise v16.1](https://github.com/AttackForge/Writeups/blob/main/MITRE/ATT\&CK/Enterprise/mitre_attack_enterprise_16_1.json), [ICS v16.1](https://github.com/AttackForge/Writeups/blob/main/MITRE/ATT\&CK/ICS/mitre_attack_ics_16_1.json), [Mobile v16.1](https://github.com/AttackForge/Writeups/blob/main/MITRE/ATT\&CK/Mobile/mitre_attack_mobile_16_1.json). You can now leverage MITRE ATT\&CK directly in your Writeups.

We've also updated to [MITRE CWE v4.16](https://github.com/AttackForge/Writeups/blob/main/MITRE/CWE/mitre_cwe_v4.16.json) and [MITRE CAPEC v3.9](https://github.com/AttackForge/Writeups/blob/main/MITRE/CAPEC/mitre_capec_v3.9.json) including new tags to cross-reference between CWE and CAPEC.

We've also updated MITRE ATT\&CK Framework for [Test Suites](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder) to the latest versions - [Enterprise v16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ENTERPRISE/mitre_attack_enterprise_16_1.json), [ICS v16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/MOBILE/mitre_attack_mobile_16_1.json), [Mobile v16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ICS/mitre_attack_ics_16_1.json).

### UX Improvements

Now when creating a new Writeup in the Project Library from the Vulnerability form, the current project will be auto-selected.

Also after completing an Abuse Case, there is now a button to *Create Another*.

### Enhancements to Self-Service APIs

We're always improving our Self-Service APIs to make automations and integrations even easier!

* **New** RESTful endpoints:
  * [Upload Writeup File](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadvulnerabilitylibraryfile)
  * [Send Email](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/sendemail)
  * [Update Project Retest Round](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateprojectretestround)
* **Updates** to REST endpoints:
  * [Get Application Audit Logs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getapplicationauditlogs) - now supports new query filters
  * [Get Project Audit Logs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectauditlogs) - now supports new query filters
  * [Get User Audit Logs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getuserauditlogs) - now supports new query filters


# 2024

## 15 November 2024

### Powered-up Project Forms!

#### Suggested Values

We've extended [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) to Project Form Fields!

This means you can now suggest values (and soon automatically set values) using AFScript.

For example, you can:

* Suggest a project or vulnerability code based on your own algorithm
* Suggest a project budget based on the testing types
* Apply logic to your custom fields

{% embed url="<https://youtu.be/VUmFWhYWtGo>" %}

To get started, head over to `Administration -> Projects` and click on `Configure` for the Suggested Value on the field:

<figure><img src="/files/CsA9sCA74JysTrVNcs0B" alt=""><figcaption></figcaption></figure>

Then add your code based on how you want your field to suggest its value.

![Set Suggested Value](https://demo.attackforge.dev/resources/docs/whats-new/resources/15-11-24/set-suggested-value.png)

> TIP: You can modify the test context and run your code to make sure it's working as expected 😊

#### Configure Sections, Re-order Your Form!

You can now configure sections and re-order all fields on the project form.

This provides the ultimate flexibility 🧙 with how you want and need your forms to look.

> TIP: Use [Hide Expressions](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions) to make your fields and sections conditionally display or hide based on your needs

![Sections and Reordering](https://demo.attackforge.dev/resources/docs/whats-new/resources/15-11-24/sections-and-reorder.png)

#### Vulnerability Code Limits Removed

We have removed the character limitations on the vulnerability code field, providing you with freedom to create however long or short vulnerability codes you need!

### Exciting UX Improvements

#### New Modal Experience for Writeups

At AttackForge, we're on a mission to make your daily tasks as easy as they can get.

Creating and editing Writeups on Vulnerabilities should be effortless. And we've made it so!

Now when interacting with a Writeup on a Vulnerability, you will get a new modal experience. This makes it easier to make your changes and go straight back to finishing your vulnerability.

We've also made it possible to duplicate a Writeup and select the destination library in one action - saving time and effort!

{% embed url="<https://youtu.be/qL-FOVSRMko>" %}

#### Inline Code Snippets

We've added support for inline code snippets in the rich-text fields.

The inline code will automatically show in your reports, however you can set a [custom style](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-styles) if preferred.

![Inline Code Snippets](https://demo.attackforge.dev/resources/docs/whats-new/resources/15-11-24/inline-code.png)

> COMING SOON! More options to style your data in rich-text fields

#### Sessions No Longer Timeout When Actively Entering Data

As the title says ☝️😉

#### Better Management of Select Field Options

We've re-designed the way options are created and managed in `Select` and `Multi-Select` custom fields.

We've also removed the limitation in the user interface for how many options can be set and configured.

#### Asset ID Now In Tables and CSV Export

You can now copy and filter `Asset Ids` in the table, and also export Asset Ids in the CSV.

&#x20;

<figure><img src="https://demo.attackforge.dev/resources/docs/whats-new/resources/15-11-24/filter-asset-id.png" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/3VIGuhTvZIEdhy6O5MTR" alt=""><figcaption></figcaption></figure>

### Assets Module - Uncatalogued Assets

If you're using the [Assets Module](https://support.attackforge.com/attackforge-enterprise/modules/assets) then you're going to enjoy this.

Previously you could set custom access controls for your `Catalogued Assets` - Assets which belong to libraries.

We've now extended the ability to set access controls for `Uncatalogued Assets` too!

You have full control over who can, and cannot, view, create and edit uncatalogued assets.

![Uncatalogued Assets](https://demo.attackforge.dev/resources/docs/whats-new/resources/15-11-24/uncatalogued-assets.png)

### Improvements to AFScript

Not only did we extend the use of [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript), but we also added support for even more built-in libraries and functions! 💪

This creates even more possibilities for how you might want to create your own logic in AttackForge!

* [String decode](https://support.attackforge.com/attackforge-enterprise/afscript#strings)
* [String encode](https://support.attackforge.com/attackforge-enterprise/afscript#strings)
* [String digest](https://support.attackforge.com/attackforge-enterprise/afscript#strings)
* [String length](https://support.attackforge.com/attackforge-enterprise/afscript#strings)
* [String toLowerCase](https://support.attackforge.com/attackforge-enterprise/afscript#strings)
* [String toUpperCase](https://support.attackforge.com/attackforge-enterprise/afscript#strings)
* [Util base64](https://support.attackforge.com/attackforge-enterprise/afscript#util)
* [Util base64url](https://support.attackforge.com/attackforge-enterprise/afscript#util)
* [Util base32](https://support.attackforge.com/attackforge-enterprise/afscript#util)
* [Util base32hex](https://support.attackforge.com/attackforge-enterprise/afscript#util)
* [Util base16](https://support.attackforge.com/attackforge-enterprise/afscript#util)
* [Util Z85](https://support.attackforge.com/attackforge-enterprise/afscript#util)
* [Util uuidv4](https://support.attackforge.com/attackforge-enterprise/afscript#util)

### Enhancements to Self-Service APIs

We're always improving our Self-Service APIs to make automations and integrations even easier!

* **New** Events:
  * `Project Retest Requested`
  * `Project Retest Completed`
  * `Project Retest Cancelled`
* **New** RESTful endpoints:
  * `Create Portfolio`
  * `Update Portfolio`
  * `Archive Portfolio`
  * `Download Vulnerability Library (Writeup) File`
* **Updates** to REST endpoints:
  * `All Get Vulnerability Related Endpoints` - now return `vulnerability_library_files`
  * `All Writeup Related Endpoints` - now return `files`

> Have you tried our Events API yet? It's perfect for real-time integrations with your scanners, ticketing tools, analytics and data lakes.

### Improvements to ReportGen

We've improved pie charts in reports to better handle labels.

Labels will now automatically resize to make sure they are always visible and legible.

![Improved Pie Charts](https://demo.attackforge.dev/resources/docs/whats-new/resources/15-11-24/improved-pie-charts.png)

We've also added new support for [custom inline code styles](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-styles).

## 24 September 2024

### Introducing AFScript - Your Custom-Code Powerhouse!

We're excited to announce [**AFScript**](https://support.attackforge.com/attackforge-enterprise/afscript) - an interpreted programming language built by the engineering wizards 🧙 at AttackForge, to help you customize and personalize *YOUR* AttackForge even more! 💪

{% embed url="<https://youtu.be/DlTl4o5J2qY>" %}

AFScript can help you in many different ways:

* Changing application logic to better align with your preferred workflows.
* Driving new behaviour in forms and their respective sections and fields.
* Creating in-app automations for your projects, vulnerabilities, assets and more.
* Applying pre-and-post data transformations when saving, updating or exporting data.
* Building bespoke dashboards and analytics that matter to you.

<figure><img src="/files/vxNYFce6BdGeQNnh7m2x" alt=""><figcaption></figcaption></figure>

AFScript is the next generation in empowerment for AttackForge customers. It comes off the back of the successes we’ve had with *Hide Expressions* ([custom fields and sections](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions), [vulnerability SLAs](https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-slas#configuring-sla-rules), [custom vulnerability parsing](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-vulnerabilities#custom-import-mapping)) and *Filter Expressions* ([custom emails](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#filter), [APIs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/advanced-query-filter)) which have provided customers with ways to make AttackForge their own.

We built AFScript to provide a *safe and secure* path to apply your own code to your AttackForge application, in a performant manner and without creating any security holes.

> Importantly, the language itself is not executable - for peace of mind.

The language was built to look-and-feel like [JavaScript](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/First_steps/What_is_JavaScript) to make it familiar and easy to use for security teams, pentesters and software engineers.

It comes with a modern and optimized code editor similar to VS Code, making it familiar and effortless to write powerful scripts.

In this release, you can use AFScript to change the logic for how project status is calculated.

{% embed url="<https://youtu.be/uN1Nzr5nfrY>" %}

Some examples you can consider:

* [Project Completed based on Project End Date](https://support.attackforge.com/attackforge-enterprise/afscript#example-1-project-completed-based-on-project-end-date)
* [Project Completed based on Custom Field](https://support.attackforge.com/attackforge-enterprise/afscript#example-2-project-completed-based-on-custom-field)
* [Project Retest based on Retesting Rounds](https://support.attackforge.com/attackforge-enterprise/afscript#example-3-project-retest-based-on-retesting-rounds)

For more information on AFScript, how it works and how to use it - please visit our [Support Centre](https://support.attackforge.com/attackforge-enterprise/afscript)

### Custom Views for Tables

We've added support for our tables to allow you to save your favourite and frequently used table views!

You can now **easily configure and switch between different views** providing flexibility in how you want to see your data.

Each view is saved against your own personal user settings, so you can tailor each view to *exactly how you want it*.

> And best of all - you can create as many views as you need!

{% embed url="<https://youtu.be/tcsXc68_6Gw>" %}

Each view is unique. You can save the following information and table state for each view:

* Table filters
* Column visibility
* Column filters
* Column ordering
* Column sorting
* Column locking
* Column features
* Table scroll

Every view gets its own name. You can also duplicate views to create similar ways of seeing your data.

We also support re-ordering your views, making it easy to switch between frequently accessed data.

{% embed url="<https://youtu.be/ViLzeIQD8tE>" %}

Custom views has already made its way to Projects, and will be rolling out across the app to other tables in the coming weeks.

### UX Improvements

We've added support for indented lists! Yes, we should have had it a long time ago. We know. Be glad it's here now and supported in your reports too! 😊

<figure><img src="/files/TajNqQ51SxH6v6C8dMLd" alt=""><figcaption></figcaption></figure>

Infinite scroll was causing you pain? Yeah we didn't really like it much easier. We've now changed it so you have a better experience scrolling those longggg lists 😎

We also combined the selection of the Writeups library and Writeup itself into a single form field, making it easier to search and select your Writeups in one place!

We've added a new email tag to help admins know who invited a user to AttackForge. For more info visit [Notifications](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags).

We've also added support for custom informational message to show on `Visibility` field when creating or updating vulnerabilities. This helps to provide your own guidance to your testers on when to set vulnerabilities as visible or as pending.

You can now also set a default value for the project code field on project requests.

### ReportGen Improvements

ReportGen v2.10.7 is out! 🚀

Along with this update, you now get:

* Support for indented ordered and unordered lists.
* Support for [Dynamic Variable Names](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usddeclare) when using the *$declare*, *$push* and *$value* functions.
* We've added *Assigned Assets* and *Assigned User* as additional data points you can now access on all Test Cases.
* We fixed an annoying bug that may have caused your report to show a 'Broken XML' error message in Word when opened under certain conditions.

### Improved Logging

Let's face it. Logs are important. So we improved our logging to give you even more information!

Now you can extract the following information for *every API request*!

* HTTP method
* URL
* Request path
* Request query parameters
* Request body
* API endpoint name
* HTTP status code
* User Id
* Project Id
* Source IP address
* Event details
* Timestamp

Logs are available in-app and via the [Self-Service APIs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api).

## 21 August 2024

### Enhanced Image Support and File Management

We've introduced better file management and image support! You can now:

* Copy and Paste images and files directly from your clipboard into rich-text fields
* Drag and Drop images and files directly into rich-text fields or within drop-zones
* Upload files at any time - even before saving the form
* Preview images and add captions directly within rich-text fields
* Expand rich-text fields to full-screen for maximum efficiency
* File manager accessible within rich-text fields

{% embed url="<https://youtu.be/DygDjakxBi0>" %}

Adding images and files from your clipboard or via drag & drop is now a piece of 🍰

> You can also upload files now at any time! You no longer need to wait until you have saved the form.

{% embed url="<https://youtu.be/G0tUK-tYb-g>" %}

Adding captions is even easier now that you can add them in-line whilst you work.

{% embed url="<https://youtu.be/3ZKWSLoCpjM>" %}

Need more space to work? You can now expand your rich-text fields to full-width, giving you more space to create or review information.

{% embed url="<https://youtu.be/ffRqAgV3Flo>" %}

You now have access to the file manager directly within your rich-text fields. This makes it easy to manage files and images, and insert the ones you need with one-click.

{% embed url="<https://youtu.be/EEz1GeqAIE0>" %}

### New Pentest Report Template is Out!

We've been working hard behind the scenes improving on the most powerful 💪 reporting engine - [ReportGen](https://github.com/AttackForge/ReportGen).

We've just released `Pentest Report Template v3.4` ([example](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-report-v3-4-example.docx) | [template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-report-v3-4-template.docx)) to help share examples on how you can improve your own reports!

> This template takes advantage of many of the new features and capabilities introduced into AttackForge and ReportGen over the past several months.

Pentest Report Template v3.4 includes the following:

* **New Section**: Document Control
* **New Section**: Version Control
* **Updated**: Project Team
* **Re-designed**: Executive Summary
* **New Section**: Background
* **New Section**: Approach
* **New Section**: Methodology
* **New Section**: Out-of-Scope
* **New Section**: Customer Goals
* **New Section**: Testing Team Goals
* **New Section**: Assumptions and Constraints
* **New Section**: Summary of Recommendations
* **New Section**: Positive Security Observations
* **Re-designed**: Retesting History
* **Updated**: Summary of Findings
* **Re-designed**: Vulnerabilities
* **New Section**: Unique Vulnerability Details

<figure><img src="/files/KA5jHlU9Y9WHGTIr2BJJ" alt=""><figcaption></figcaption></figure>

### Cloud Testing Methodologies Now Available

We have just added five (5) new Cloud Configuration Testing Methodologies.

You can now import any of these methodologies into your `Test Suites`

* [**CIS Amazon Web Services Foundation**](https://github.com/AttackForge/TestSuites/blob/main/AWS/CIS-Amazon-Web-Services-Foundation-v1.2.0.json)
* [**CIS Microsoft Azure Foundation**](https://github.com/AttackForge/TestSuites/blob/main/AZURE/CIS-Microsoft-Azure-Foundation-v1.2.0.json)
* [**CIS Google Cloud Platform Foundation**](https://github.com/AttackForge/TestSuites/blob/main/GCP/CIS-Google-Cloud-Platform-Foundation-v1.1.0.json)
* [**Kubernetes Infrastructure**](https://github.com/AttackForge/TestSuites/blob/main/KUBERNETES/Kubernetes-Infrastructure.json)
* [**Oracle Cloud Infrastructure**](https://github.com/AttackForge/TestSuites/blob/main/OCI/Oracle-Cloud-Infrastructure.json)

You can download these methodologies from the [AttackForge GitHub](https://github.com/AttackForge/TestSuites).

<figure><img src="/files/HLvatHtJ2CCAg8EjlDyC" alt=""><figcaption></figcaption></figure>

### New Custom Time-Based Emails: Writeups

You can now configure `Writeups` in your [custom time-based emails](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-time-based-emails).

This means you can now:

* Get an email update daily/weekly on newly created or modified Writeups
* Notify maintainers on changed Writeups in a custom library
* Get updates on Writeups that meet your specific criteria, even custom fields!

<figure><img src="/files/m5eFTTQXTWP89MxOkawM" alt=""><figcaption></figcaption></figure>

### Invite User by Email Address

You can now invite users to AttackForge directly by email address within the Users module. Invited users will receive an email with an activation link which takes them to a page to finish setting up their profile.

<figure><img src="/files/jzQYQo5slUUgvDxdijzx" alt=""><figcaption></figcaption></figure>

### Column Freeze on Table Scroll

You can now lock columns as you scroll your tables. This makes it easier to keep the important information always within view. You can also lock multiple columns giving you more control over your tables and data.

{% embed url="<https://youtu.be/tXl8K59RgS0>" %}

### Improved Table Custom Fields

We have made significant improvements to table custom fields. You can now:

* Click on a table row to view or edit the full data in a modal window
* Filter and sort on individual columns
* Expand table into a modal window, for advanced filtering and search
* Bulk actions when editing table rows
* Support for pagination

{% embed url="<https://youtu.be/mgPGu3tSxyo>" %}

### Exploitability and Custom Tags now Optional

We have made `Exploitability` and `Custom Tags` optional fields. You can now switch off these vulnerability fields within `Administration -> Vulnerabilities`.

<figure><img src="/files/ipoNH9bKyCLltaplQKlj" alt=""><figcaption></figcaption></figure>

### View Visible, Pending and All Vulnerabilities

You can now view visible, pending and all vulnerabilities on your project. This makes it easier to view and action all vulnerabilities in a single table.

<figure><img src="/files/i6hPCjRglyU8IR4iOYMP" alt=""><figcaption></figcaption></figure>

### Access Detailed Asset Information from Vulnerabilities

You can now open the Asset page to access detailed information on assets, directly from your vulnerabilities.

{% embed url="<https://youtu.be/UGtJzJGalRs>" %}

### Self-Service API Improvements

We made a bunch of improvements to our Self-Service APIs namely:

* **Major updates** to REST endpoints:
  * `Create Project & Update Project` - we updated these APIs to support configuration for [Features](https://support.attackforge.com/release-notes/2024#project-features) and [Pages](https://support.attackforge.com/release-notes/2024#new-feature-project-pages) on projects.
  * `Create Writeup & Update Writeup` - we added support for creating and editing Writeups in the `Project Library`.
  * `All Get Vulnerability APIs` - we added support for querying on `Pending` vulnerabilities.

> Have you tried our Events API yet? It's perfect for real-time integrations with your scanners, ticketing tools, analytics and data lakes.

## 5 July 2024 <a href="#top" id="top"></a>

### New Feature: Project Pages

We've introduced a new feature on projects - **Pages** - which can be used to create, group and manage custom content on your projects.

Pages are a dynamic way to interact with your project teams. Their content can also be used in reports and in the Self-Service APIs.

Here's some examples for how you can use Pages:

* Create dedicated places for project teams to enter and track information
* Provide additional options for teams to collaborate
* Group content into topics

> Each page comes with its own access controls to manage who can View, Upload or Edit content on the page.

{% embed url="<https://youtu.be/y014VbpeG5A>" %}

For this first release, we have made a **Summary** page available. You can control which sections and fields can be created on this page. Some examples might include:

* Customer Goals for the testing team, and Testing Team goals for the customer
* Assumptions, Constraints and Limitations
* Scope-related notes such as Out-of-Scope

<figure><img src="/files/4sEZp1ZSb3tw4M2tOf9x" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/rWWq7Kt6CGeevHb6eNoZ" alt=""><figcaption></figcaption></figure>

We will be adding more pages in the future to provide even more flexibility on how you can use your projects.

### Reporting Enhancements

We have extended the capabilities of the **Reporting** section on your project.

You can now create custom sections and custom fields to personalize your Reporting data to the project.

Some ideas for sections and fields you can now create:

* Project Summary - including Executive Summary, Summary of Recommendations and Positive Security Observations
* Testing Overview - including Background, Approach and Methodology
* Document Control - including Author(s), Reviewer, Approver and Version History

> Dont forgot to try Hide Expressions on Custom Fields to make your Reporting tailored to your project.

{% embed url="<https://youtu.be/woVftzsyV5Y>" %}

> Check out GitHub AttackForge ReportGen site for details on how to add these fields to your reports

### Project Features

You can now personalize your project experience even further with **Project Features**.

Project Features are a new way to help you:

* Control when Test Cases are viewable on a project, and by whom.
* Determine if Retesting is needed, and who can have access.
* Determine if Attack Chains are relevant, and who can have access.
* Determine if Reporting is needed, and when Reporting section and downloadable reports are made available.

> Every feature comes with access controls for better management of information and workflows

{% embed url="<https://youtu.be/-fyT_f-pT3M>" %}

### Self-Service API Improvements

We made a bunch of improvements to our Self-Service APIs namely:

* **New REST endpoint**: Create Project Test Case
  * This endpoint can be used to create dynamic test cases on the project, for example you could import all of the checks that your scanner performed to provide better and programmatic testing coverage
* **Major updates** to REST endpoints:
  * Update Test Case - we updated this API to provide the full suite of options when it comes to managing your project test cases. You can even insert custom fields on your test cases via the API!
  * Get Project Test Cases - we updated this API to provide more comprehensive information about the test cases, such as linked vulnerabilities, assignment to project scope, and assignment to tester.
* **Minor updates** to REST endpoints:
  * Get Project, Get Projects, Get Projects and Vulnerabilities & Get Projects by Group - now supports *project\_scope\_details*
  * Create Vulnerability, Create Vulnerability Bulk, Create Vulnerability With Library - now supports *linked\_testcases*
  * Update Vulnerability & Update Vulnerability With Library - now supports *project\_id* and *linked\_testcases*
  * Get Vulnerability, Get Vulnerabilities, Get Vulnerabilities by Group, Get Vulnerabilities by Asset, Get Project Vulnerabilities & Get Projects and Vulnerabilities - now supports *vulnerability\_testcases*

> Have you tried our Events API yet? It's perfect for real-time integrations with your scanners, ticketing tools, analytics and data lakes.

### UX Improvements

We've added new email notification settings which can be configured to determine who gets notified when `Project Requests` are either *created* or *updated*. You can configure this from `Administration -> Notifications -> Project Requests`.

We've improved the experience after creating a `Vulnerability` or a `Writeup`. You now have options to create another, create and view or go straight to evidence upload.

We also improved the `Administration` module navigation experience to make it easier to find the configuration settings you need.

## 18 June 2024 <a href="#top" id="top"></a>

### Customize Vulnerability and Project Colors

You can now customize your vulnerability and project colors!

Personalize your AttackForge and make it your own. Tailor your color scheme to match your identity and messaging.

You can now change the following color options in the **Administration** module:

* Vulnerability priorities
* Vulnerability statuses
* Project statuses
* Project Request statuses

{% embed url="<https://youtu.be/1rQi51xUDf0>" %}

We've also lightened the status colors to blend in better with the rest of the application, and to keep the eyes focused on the most important messages on the page.

### Assign Users and Groups to Manage Project Teams

You can now assign users and groups to be responsible for managing access to individual projects.

This can help you to:

* Empower customers and engineering teams to provide access to the project and vulnerabilities on a needs basis.
* Delegate project team management to persons better suited for the role.
* Save time and effort on project team management.

When assigning a user or a group to manage access to the project, you can configure the following:

* **Access Level Limit**: Set the highest level of access the user or group is allowed to assign to project team members.
* **Add User Method**: Configure how project team members can be added - either by selecting an existing user from a drop-down list, or by entering in an email address.
* **Allow User Invite**: Allow user to invite new persons to your AttackForge tenant.

{% embed url="<https://youtu.be/dwwnf3keDc0>" %}

### Delegate User Management of Groups

You can now delegate management of your groups to other users and groups.

This can help you to:

* Empower customers and engineering teams to manage stakeholder access to relevant projects and project requests.
* Delegate security teams to manage groups on a needs basis.
* Reduce the burden on application administrators.

When assigning a user or a group to manage access to the group, you can configure the following:

* **Project Access Level Limit**: Set the highest level of access the user or group is allowed to assign for access to the groups' projects.
* **Project Request Access Level Limit**: Set the highest level of access the user or group is allowed to assign for access to the groups' project requests.
* **Add User Method**: Configure how group members can be added - either by selecting an existing user from a drop-down list, or by entering in an email address.
* **Allow User Invite**: Allow user to invite new persons to your AttackForge tenant.

> You can now also set `None` for group members access to Projects and Project Requests.

{% embed url="<https://youtu.be/93JswhvIy4s>" %}

### Invite Users to Projects, Groups and AttackForge

You can now allow specific users or groups to invite other users to your AttackForge.

This makes it convenient to get access to the right people, quickly and without the need to involve application administrators.

This can help you to:

* Empower customers to invite their engineering teams directly to the relevant projects or groups.
* Delegate account managers to better manage their customer accounts.
* Share vulnerability information faster, to help remediate vulnerabilities sooner.

> When combined with the new project team and group membership administration options (above) - this feature provides full autonomy for user management!

<figure><img src="/files/f0VsyNico4lJ1wfZFvnr" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/lB7Drrw3nQbyPP1SbKjN" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/H3pcHoojF5LWmTPWPOHG" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oiTKEHpFqpR5CyoEgJiT" alt=""><figcaption></figcaption></figure>

### SSO Improvements

We have now enabled support for two (2) independant identity providers (IdPs) which each can be supported with their own unique configurations, including SSO User Role Mappings.

We have also now added the option to disable Just-In-Time SSO user creation, helping to restrict access to AttackForge via SSO to users whom have been invited only.

### ReportGen Enhancements

We have added support for additional project team member data in reports, namely `About Me`. See example below:

```
{#data.testing_summary.project_team_details}
{first_name} {last_name} ({role}) – {about_me}
{/}
```

We also added an extra option for Charts which can be configured to [increase the y-axis on vertical bar charts](https://github.com/AttackForge/ReportGen/issues/57). This can help to add some padding above the bars to improve the aesthetic of the chart.

## 21 May 2024 <a href="#top" id="top"></a>

### Secure Code Learning with SecDim

We're excited to be the first Pentest Management Platform to release a Secure Code Learning collaboration with [SecDim - Dev-Native Attack & Defence Wargames](https://secdim.com/).

With the power 💪 of SecDim and AttackForge, you can:

* Explore a real vulnerability in a cloud native app. Debug and verify if your security patch can remediate the vulnerability
* Train developers on how to fix vulnerabilities identified in their applications, during a pentest
* Collaborate between engineers and security teams on how to best remediate vulnerabilities
* Improve retesting pass rates for discovered vulnerabilities
* Test your knowledge on how to fix common vulnerabilities and measure your effectiveness

Every Sandbox comes with a security test suite to simulate the exploitation of the vulnerability.

Sandboxes are integrated with `git` so you can save your progress and pick it up again where you left off.

SecDim's catalogue is extensive, covering everything from AI, GraphQL, React, Kubernetes, to Web3. You can test your skills against modern security vulnerabilities inspired by real-world issues.

> Each sandbox is deployed in a secure isolated Cloud Development Environment directly in your browser. You can debug, patch and test your code as if you were building an app.

{% embed url="<https://youtu.be/FycgBdGKA5w>" %}

Start by learning more about this integration, and when ready - switch the integration on.

![Switch SecDim On](https://demo.attackforge.dev/resources/docs/whats-new/resources/21-5-24/secdim-switch-on.png)

Search SecDim's catalogue of vulnerable sandboxes which you can link to your Writeups.

{% embed url="<https://youtu.be/E8X9017GXI8>" %}

When you create a vulnerability on a project, users will be able to see the linked SecDim sandboxes and launch a sandbox to get started.

{% embed url="<https://youtu.be/mnsjLeV_gRc>" %}

### Auto-Add Groups to Project Requests

You can now automatically assign Groups to Project Requests when they get created by group members.

This means any custom access controls you have created on the group will take effect immediately.

This makes it easy to have dedicated teams of people who can work together to view, edit and approve project requests - without the involvement of Administrators or Project Coordinators.

<figure><img src="https://demo.attackforge.dev/resources/docs/whats-new/resources/21-5-24/auto-add-group-to-project-request-1.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://demo.attackforge.dev/resources/docs/whats-new/resources/21-5-24/auto-add-group-to-project-request-2.png" alt=""><figcaption></figcaption></figure>

### UX Improvements

Now when you open and close the Info panels on Vulnerabilities, Project Test Cases and Reporting - this action will be remembered for the duration of your session. This means you can easily switch between pages and see the information you want to see more easily and with less clicks.

We added new Core Fields for [Hide Expressions](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions) to provide more flexibility when it comes to controlling your forms.

We also improved the behaviour of how CVSSv3.1 scoring is used on vulnerabilities, including better support for Nessus.

### SSAPI Enhancements

We have made the following enhancements to the Self-Service APIs:

* `Get Assets In Library` now supports the [Advanced Query filter](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/advanced-query-filter)
* `Create Vulnerability` and `Create Vulnerability With Library` now supports passing in Asset Library Ids.
* `Update Vulnerability` and `Update Vulnerability With Library` now supports updating the Affected Asset(s).
* `Get Project Report Data` endpoint was created to allow programmatic creation of reports for selected vulnerabilities only.

### ReportGen Improvements

We have improved ReportGen to make your reports even better!

* `$declare`, `$push` and `$assign` [Functions](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions) now support 'this' and 'this\[number]'
* `$includes` now supports Dictionaries

## 3 May 2024 <a href="#top" id="top"></a>

### New Project Request Access Controls

You can now configure custom access controls on Project Requests.

This means you can now:

* Create multi-stage review and approval workflows for Project Requests, particularly when combined with custom field access controls.
* Delegate additional persons or group members to View, Edit or Action selective Project Requests.
* Improve efficiency when it comes to project scoping workflows.

To get started, as an Administrator or Project Coordinator - access the Settings on the Project Request:

{% embed url="<https://youtu.be/sJVJhy7uG34>" %}

You can assign access to application user Roles, Groups or individual Users.

Each access control can be assigned with View, Edit or Action:

* **View** means the user will be able to view the Project Request, but not make any changes.
* **Edit** means the user will be able to view and edit the Project Request, and upload any supporting files.
* **Action** means the user will be able to view and edit the Project Request, upload any files, request more information, and reject and approve the Project Request. Approving the Project Request will result in creation of a new Project.

When assigning Group access to the Project Request, the group members can be assigned with View, Edit and Action. This will apply to the Project Requests linked to the Group.

{% embed url="<https://youtu.be/Ak_wbUZFK60>" %}

Project Coordinators and Administrators will continue to have access to all Project Requests, along with any additional Roles or Users who have been delegated global privileges to Action all Project Requests.

### Vulnerability Imports Now Support Multiple Files

You can now import multiple scan files in one import. This means you can now take advantage of Grouped Assets on vulnerabilities across multiple scans - making it easier to identify and track unique vulnerabilities on the project, and associate affected assets more easily.

Simply select multiple scan files when prompted to select a file.

We also made improvements to user feedback during parsing of vulnerabilities.

{% embed url="<https://youtu.be/oY-US-h0mlw>" %}

### Bulk Action Retest Vulnerabilities

We have now made it easier to see all vulnerabilities associated with a retest, and perform bulk actions.

{% embed url="<https://youtu.be/LlHVVBrECmY>" %}

### Create Reports on Pending Vulnerabilities

You can now create reports on pending vulnerabilities. This makes it easier to review vulnerabilities in your custom reports, before releasing them to customers.

![Pending Vulnerabilities Report](https://demo.attackforge.dev/resources/docs/whats-new/resources/3-5-24/pending-vulnerabilities-report.png)

### Filterable Project Team Details

You can now view the entire project team and filter on team members more easily:

<figure><img src="https://demo.attackforge.dev/resources/docs/whats-new/resources/3-5-24/filterable-project-team-dashboard.png" alt=""><figcaption></figcaption></figure>

<figure><img src="https://demo.attackforge.dev/resources/docs/whats-new/resources/3-5-24/filterable-project-team-settings.png" alt=""><figcaption></figcaption></figure>

### Email Updates

Vulnerability-related events on projects, for example New Vulnerability Discovered email, now support **{vulnerability.\<tag>}** [Custom Email Meta Tags](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags)

Daily Start/Stop Testing Notifications on projects now support **{project.\<tag>}** [Custom Email Meta Tags](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags)

All emails which support the {link} tag now also support {link.url} which returns only the URL. This means you can use {link.url} inside custom buttons and achors in your emails.

### Updates to ReportGen

We have added a new function [$percentage](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdpercentage) that can be used to calculate the percentage of two values.

We also updated [$keys](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdkeys) to support $keys\[this] which can be used to iterate on any object and return each key/value pair in the object as an array.

## 15 April 2024 <a href="#top" id="top"></a>

### Project Test Case custom fields

Project test cases now support custom fields.

This opens up many possibilities, for example:

* Capturing Red-Team and Blue-Team information on Purple-Team assessments
* Filtering on additional test case sub-status
* Persisting additional testing details

You can also now re-order your project test case view to personalize how you want your test cases to appear on different projects.

Soon you will be able to use the Self-Service APIs to import custom project test cases for dynamic and reactive testing, for example importing scanner policies for scans performed.

### Defining a Purple-Team project

<figure><img src="/files/4L3lWVb3BNisn6igMUpx" alt=""><figcaption></figcaption></figure>

Start by adding a custom field to your project which will be referenced by your test cases and vulnerabilities.

This will show purple-team fields on test cases and vulnerabilities for only purple-team projects, without creating additional overheads for other assessments.

<figure><img src="/files/JcibWqyYLTrEiqjlf9MZ" alt=""><figcaption></figcaption></figure>

#### Defining your Red-Team custom fields on Test Cases

> Define your red-team project test case custom fields in **Administration → Projects → Test Cases → Form**

1. Create a Section for your red-team fields, this makes it easier to group your fields.
2. Add a Hide Expression to ensure your red-team section and its related fields only show on project test cases for purple-team projects.
3. Add Custom Field Access Controls to ensure that:
   * Red Teamers can View and Edit red-team fields;
   * Blue Teamers can only View red-team fields;
   * All others have no access to these fields.

{% embed url="<https://youtu.be/REfbmQfepBU?si=4HofJooY22oLZpnc>" %}

#### Defining your Blue-Team custom fields on Test Cases

> Define your blue-team project test case custom fields in **Administration → Projects → Test Cases → Form**

1. Create a Section for your blue-team fields, this makes it easier to group your fields.
2. Add a Hide Expression to ensure your blue-team section and its related fields only show on project test cases for purple-team projects.
3. Add Custom Field Access Controls to ensure that:
   * Blue Teamers can View and Edit blue-team fields;
   * Red Teamers can only View blue-team fields;
   * All others have no access to these fields.

{% embed url="<https://youtu.be/vtwD_NHGJHM>" %}

#### Defining your Red-Team custom fields on Vulnerabilities

> Define your red-team vulnerability custom fields in **Administration → Projects → Vulnerabilities → Form**

Create a Section for your red-team fields, this makes it easier to group your fields.

Add a Hide Expression to ensure your red-team section and its related fields only show on vulnerabilities for purple-team projects.

{% embed url="<https://youtu.be/viAf7U0P6Mo>" %}

#### Set your project as a Purple-Team project

When creating or editing a project, select `Purple Team` from the Testing Types custom field.

{% embed url="<https://youtu.be/q8ZhMzDzDiQ>" %}

#### Complete Purple-Team project test case fields on purple-team projects

When working on a purple team assessment, you can now complete the purple-team fields on the project test cases.

{% embed url="<https://youtu.be/PJMYzpQEoSM>" %}

#### Complete Purple-Team vulnerability fields on purple-team projects

When working on a purple team assessment, you can now complete the purple-team fields on the vulnerabilities.

{% embed url="<https://youtu.be/xXgfSAOlZhI>" %}

### Configuring sub-status on project test cases

#### Defining a sub-status on project test cases

Start by adding a custom field to your project test cases which will be used to capture the sub-status of a test case.

<figure><img src="/files/ijLhuAH6bnB5KZPcZTlV" alt=""><figcaption></figcaption></figure>

#### Track and manage sub-status on project test cases

When working on a project, you can now complete the relevant sub-status on project test cases.

You can also filter and sort your project test cases on sub-status.

<figure><img src="/files/r7rUEU8m0Zx1E67qo18x" alt=""><figcaption></figcaption></figure>

### Capturing additional information on project test cases <a href="#top" id="top"></a>

#### Defining additional information on project test cases

Start by adding custom fields to your project test cases which will be used to capture additional information relevant for your test cases.

<figure><img src="/files/IO4OhT4I5DtBCiygkQzz" alt=""><figcaption></figcaption></figure>

#### Entering additional information on project test cases

When working on a project, you can now complete the relevant fields when working on project test cases.

<figure><img src="/files/uEqh2mIpznHwAFJundUo" alt=""><figcaption></figcaption></figure>

### New time-based custom email options

You can now configure custom time-based emails for Projects, Project Requests and Users - in addition to previously supported Vulnerabilities.

Some examples of custom time-based emails could include:

* Notify vulnerability owners when vulnerabilities are 7-days from breaching SLAs
* Notify remediation teams when vulnerabilities are 10-days from reaching Target Remediation Date
* Notify security teams when vulnerabilities exceed SLAs
* Notify project teams when projects have overrun
* Notify project coordinators when project requests have not been actioned for some time
* Notify users when their account will be locked out due to inactivity

Check [Custom Time-Based Emails](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-time-based-emails) for more details.

<figure><img src="/files/MxmUnX1NjOv5PmGCDnFq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Vwtog6xwgz6uuolntnae" alt=""><figcaption></figcaption></figure>

### Import vulnerabilities via JSON file

You can now import vulnerabilities directly from a JSON file.

This makes it easy to import vulnerabilities from any source, where the data can be formatted into JSON format.

A template is provided to help make this process easy, as well as details for required fields.

<figure><img src="/files/LlC1zvOwZUi7WPWZ0klX" alt=""><figcaption></figcaption></figure>

### Rich-text custom fields now support images

The following rich-text custom fields now support display of images in-app and in-reports:

* Project Requests
* Vulnerabilities
* Writeups
* Project Test Cases
* Test Suite Test Cases

{% embed url="<https://youtu.be/5LT3A7twJ68>" %}

### Config change detection

Changes to configuration will now trigger all users to reload their app to retrieve the new configuration, at a time suitable for the user.

{% embed url="<https://youtu.be/7rfeGR-0_8E>" %}

### Custom field dates now support time picker

You can now enable capturing and display of time for your Datepicker custom fields.

This can be useful if you need to capture time, for example **Execution Start Time** and **Execution End Time** for Red-Team activities or for captured logs.

This option can be configured using the `Display Time` option configured on the the Datepicker custom field.

### Project Request information now available in reports

You can now add information from your linked Project Request into your reports.

> First, start by enabling this option in **Administration → Projects → Export Project as JSON → Project Request**

Now in your reports, you can access the Project Request information using `{projectRequest}` for example:

```
{projectRequest.name} - {projectRequest.created}
```

### Vulnerability imports now support Qualys Guard

You can now import vulnerabilities from Qualys Guard.

### Reports now support email addresses for project team

You can now include email addresses for your project team members in your reports.

> Start by enabling this option in **Administration → Projects → Export Project as JSON → Team Member Email**

You can print the emails of the team members in your report as follows:

```
{#data.testing_summary.project_team_details}
{first_name} {last_name} - {email}
{/}{/}
```

### ReportGen - $help now supports \[scope] and \[var]

You can now use `{$help[scope]}` and `{$help[var]}` in your report templates to show debugging and help information in your ReportGen browser console.

{% embed url="<https://youtu.be/EFdMEZA5Xwo>" %}

## 31 January 2024

### Asset Libraries

You can now use Asset Libraries to manage your assets better!

Asset libraries will help you to:

* Group and manage assets across different teams, technologies, products, customers, business units, networks or compliance;
* Control who can see assets, and which assets they can see;
* Manage who can create and modify assets;
* Assign assets to many groups for better tracking and de-duplication;

<figure><img src="/files/1obrV1elgD5QXjSmDqMD" alt=""><figcaption></figcaption></figure>

You can view your asset libraries directly from the Assets module:

<figure><img src="/files/iqQlEoxsAPpnCinXrQJn" alt=""><figcaption></figcaption></figure>

Take advantage of libraries when adding scope to a project:

<figure><img src="/files/mBNwNhLUBgYyQtjPCn6w" alt=""><figcaption></figcaption></figure>

Control which libraries get used when importing vulnerabilities:

<figure><img src="/files/GOqBPZvWL5UTHP78p7Cx" alt=""><figcaption></figcaption></figure>

Assign libraries when importing assets:

<figure><img src="/files/qfZnWF7H6kVsTTmmZQd2" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/7vLyMQDOgiFdseVddY7u" alt=""><figcaption></figcaption></figure>

Asset libraries can be configured from the Administration module.

<figure><img src="/files/rg8lRzMX1F2M3Z1S2ou1" alt=""><figcaption></figcaption></figure>

Assets can belong to one or more libraries, allowing to share access or ownership of assets and reducing asset duplication.

<figure><img src="/files/AVbfWZngiInVYqzOua0o" alt=""><figcaption></figcaption></figure>

Assets are unique to each library.

Asset libraries can have access controls to manage who can view or edit the assets.

Access controls can be applied to:&#x20;

* Application Roles
* Groups
* Users

Access to libraries can be assigned with either View or Edit privileges.

Users with View access to an asset library will be able to see the asset and any vulnerabilities for which that user has access to via their projects.

Users with Edit access to an asset library will be able to create assets in that library; and make changes to any asset in that library, including ability to archive the asset or link additional asset libraries.

Bulk actions can be used to bulk (re)assign assets to libraries.

You can now also import assets from CSV and JSON formats – making it even easier to bring existing assets into AttackForge!

For more details, please visit <https://support.attackforge.com/attackforge-enterprise/modules/assets>

### Portfolios Enhancements

You can now provide access to Portfolios and Streams for:

* Your customers;
* Your engineering teams and product owners;
* Your security, risk and compliance teams;
* Your business management and senior leadership;
* Your external auditors and 3rd parties;

This makes it possible to have custom dashboards which are relevant and tailored to your stakeholders, which they can now access.

<figure><img src="/files/p6xueKORlpJidAylzcwO" alt=""><figcaption></figcaption></figure>

Access can be granted to the entire Portfolio, or individuals Streams. This enforces *need-to-know*.

<figure><img src="/files/5yjTyy4X4XJeuaJ6NYGM" alt=""><figcaption></figcaption></figure>

Users will only see project, vulnerability and asset related data which is relevant to their existing projects.

Users will not be able to see project, vulnerability and asset data for which they do not already have access.

We have also enriched the data available for projects, vulnerabilities and assets.

Users can also configure their own table preferences to consume the data the way they prefer.

<figure><img src="/files/JgYHgyykA5h9hJvSHZoI" alt=""><figcaption></figcaption></figure>

### ReportGen v2.9

We have just released another massive update for AttackForge ReportGen: The ultimate pentest reporting tool!

This release includes three (3) new functions; one (1) new style; and access to user profile data in reports!

<figure><img src="/files/KfEIjr4svkvOxcWJfXiK" alt=""><figcaption></figcaption></figure>

#### New Function: $hyperlink

You can use this new function to construct hyperlinks in your reports.&#x20;

Hyperlinks can be built using data from your project (scope), manual creation or based on values from other variables.

For more information on how to use this function, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions%22%20%5Cl%20%22usdhyperlink).

#### New Style: hyperlink\_style

This release introduces support for hyperlinks for rich-text fields. We have also released a new style which allows you to independently set the style for hyperlinks contained within the styled tags. You can apply this style to any of the {@...\_styled} fields.&#x20;

For more information on how to use this style, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-styles%22%20%5Cl%20%22custom-styles-for-rich-text-fields).

#### New Function: $comment

You can use this new function to include comments in your template which do not get shown in the report.&#x20;

This can be useful to help with adding explanations and also debugging.

For more information on how to use this function, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions%22%20%5Cl%20%22usdcomment).

#### New Function: $multiply

You can use this new function to multiply a variable which has a numeric value.

For more information on how to use this function, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions%22%20%5Cl%20%22usdmultiply).

#### User Profiles Now Available

You can now include user profile information in your reports for each team member on the project.

For examples how to include this information in your reports, visit this [link](https://github.com/AttackForge/ReportGen/issues/54).

### Updated Testing Methodologies and Vulnerability Libraries

We have updated to the latest version of MITRE ATT\&CK framework, which you can now easily import into your Test Suites.

* MITRE ATT\&CK Enterprise Version 14.1
* MITRE ATT\&CK Mobile Version 14.1
* MITRE ATT\&CK ICS Version 14.1

To get started, head over to <https://github.com/AttackForge/TestSuites>

Download the relevant methodologies and follow the guide to import them into your Test Suites.

<figure><img src="/files/Sf7kaefWEnSqkdaEpyxX" alt=""><figcaption></figcaption></figure>

We have also updated to the latest version of MITRE CWE and MITRE CAPEC, which you can now easily import into your Writeups.

* MITRE CWE Version 4.13
* MITRE CAPEC Version 3.9

To get started, head over to <https://github.com/AttackForge/Writeups>

Download the relevant vulnerability libraries and follow the guide to import them into your Writeups.

<figure><img src="/files/ZhBFKr8aOpijQTFaYL7x" alt=""><figcaption></figcaption></figure>

### Report Locking

You can now control when reports are available for download on any given project.

This is particularly useful if you want to restrict your customers from generating reports until a point in time on the project, for example when testing is completed or when QA has finished.

When creating or updating a project, you can now configure the minimum Access Level required on the project in order to generate reports.

<figure><img src="/files/2m0EqaleBJRCjsCJxc06" alt=""><figcaption></figcaption></figure>

### Custom Project Roles

You can now configure custom project roles which can be assigned to any project team member.

This is particularly useful if you want to create roles which align with your internal operating processes.

These roles can be reflected in emails, reports and in automations and integrations.

These roles are not used for access control.

You can manage the project roles from Administration -> Projects -> Fields -> Team Members.

<figure><img src="/files/7zX9RQ4jjGrJ7cR7LccI" alt=""><figcaption></figcaption></figure>

### Custom System Email Notifications

You can now configure and personalize every system email!&#x20;

You can independently for each system email notification:

* Enable or Disable the email notification;
* Configure a custom Subject with HTML and {metatags} support
* Configure a custom Body with HTML and {metatags} support

You can manage the email notifications from Administration -> Notifications.

For a full list of {metatags} supported – please visit this [link](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications%22%20%5Cl%20%22custom-email-meta-tags).

<figure><img src="/files/oW1bv6s7pEH7TdKShGly" alt=""><figcaption></figcaption></figure>

### Custom Report Names

You can now configure the custom report name for all of your downloaded reports.

This configuration option also supports (metatags}.

You can update your report name from Administration -> Reporting -> Report Name.

### Manage User Roles via SSO Groups

For SSO users – you can now opt into managing application user roles via SSO groups.

This ensures that every time an SSO user logs in, their application user role will be automatically updated to match their expected role via the mappings.

This makes it easier for AttackForge Administrators managing tenants with hundreds or thousands of users.

It also helps to comply with internal policies for privileged access management.

You can opt into this setting from Administration -> Users -> Manage Application User Roles via SSO Groups.

<figure><img src="/files/9y8tBOo5xAYuxGaWGX33" alt=""><figcaption></figcaption></figure>

### UX Enhancements

#### Inline vulnerability view on tables

When viewing vulnerabilities in a table, you can now preview the vulnerability without having to navigate away.

To do so, click on the eye icon next to the vulnerability name.

<figure><img src="/files/pIpBZP3c6Gqj57QJh8R8" alt=""><figcaption></figcaption></figure>

#### Retest rounds now should on schedules

Retest rounds now have an optional end date and will now also show on your global and project schedules.

#### Rich-text fields now support hyperlinks

Rich-text fields now support option to include hyperlinks. These hyperlinks will also automatically show in reports for any ‘\_styled’ fields.

#### Warnings to help prevent data loss

We have added warnings when you have data entered into a form and try navigating away, or when you try to close a popup window with data.

This UX improvement will help to prevent any accidental data loss on common user actions.

#### Wider, taller and draggable form fields

We have reviewed all forms within AttackForge and where needed we have made fields wider and taller.

We have also enabled ability for rich-text fields to have draggable adjustable height.

#### Set custom error message for blocked accounts

You can now configure a custom error message for blocked accounts.&#x20;

This is useful if you have company-specific instructions on account reactivation process that you want to show blocked users.

#### Bulk add tags on grouped assets

You can now bulk add tags to grouped assets when working on vulnerabilities.

#### Bulk overwrite on vulnerabilities now supports mixed asset selections&#x20;

You can now perform bulk overwrite actions on vulnerability selections with mixed asset types i.e. individual or grouped assets.

#### Hovering on project name shows full name

We made adjustments to show more information when hovering on data, including on the project name.

### Linking Vulnerabilities to Test Cases

You can now link vulnerabilities to test cases directly from the test cases.

This makes is easier to fail test cases in bulk, and to show direct correlation between testing and findings.

<figure><img src="/files/hjerVnmDvpDsX1hYuoe7" alt=""><figcaption></figcaption></figure>

### Importing Vulnerabilities

We added support for Nuclei Scanner and Acunetix 360 when importing vulnerabilities on a project.

### Updates to Self-Service API

We made updates to the *Invite User To Project* and *Invite Users To Project Team* RESTful API endpoints to include additional fields.

We also added support for Asset Libraries for all of the relevant RESTful APIs.

### Video Tutorials

#### Introduction to AttackForge and On-Demand Trial Environments

We recently released a new trial service for AttackForge – <https://try.attackforge.io> – which allows people to deploy a dedicated, on-demand private tenant of AttackForge with just their email address. The entire process takes less than two minutes from signup to deployment!

We created a short video to demonstrate this process for deploying new trial environments, as well as a brief introduction to AttackForge.

This video might be useful to you if:

* You need access to on-demand non-production AttackForge tenants for testing configuration, integrations or scripts;
* You want to access the newest features in an environment that is already configured with optimal settings for the latest features;
* You want your customers or users to get a preview for what the AttackForge application is about and key workflows, prior to interacting with your own AttackForge.

The video is available on YouTube:&#x20;

{% embed url="<https://www.youtube.com/watch?v=yTBrkovVTYg>" %}

### Pentest Report Automation with AttackForge

We created a short video to demonstrate how to set up an automation for PDF report generation and delivery to customers.

This video might be useful to you if:

* You need to send encrypted PDF reports to your customers via email and programmatically;
* You are interested in exploring automations in AttackForge;
* You are interested in how custom fields can be used to enforce custom workflows.

The video is available on YouTube:&#x20;

{% embed url="<https://www.youtube.com/watch?v=_aBsBwbX1S0>" %}

#### Red Teaming with AttackForge

We created a short video to demonstrate how to do Red Teaming in AttackForge.

This video might be useful to you if:

* You are considering starting to perform Red Team assessments in AttackForge;
* You are already performing Red Teaming in AttackForge.

The video is available on YouTube:

{% embed url="<https://www.youtube.com/watch?v=VAczGVdiLYk>" %}


# 2023

## 2023-10-31

### Vulnerability Form Builder

One of the most requested and highly anticipated features has arrived – Vulnerability Form Builder!

You now have *full control* over how your vulnerability form can be built:

* Re-arrange all of your existing fields into your preferred display order
* Create custom sections, then group and order your fields into their relevant sections
* Re-name and re-order existing sections
* Use logic to show relevant vulnerability sections and fields based on the testing types assigned to the project
* Improved vulnerability page view to match your preferred vulnerability user experience

Vulnerability Form Builder provides unprecedented levels of customization when it comes to how you want to create and view your vulnerabilities.

You can create custom sections and fields for different types of security testing, for example red team assessments, pentests, configuration reviews, code reviews, social engineering, etc.

You can combine sections and fields and choose when you want them to be displayed.&#x20;

For example - your project might include web application, infrastructure and mobile application testing. You can show the relevant sections and fields based on the types of testing assigned to the project.

We have also improved the way vulnerability fields are presented when viewing the vulnerability. Your sections and display order are now fully supported, helping you to group and highlight the most important information for your remediation teams and customers.

#### **Re-arrange your vulnerability fields the way in which you want**

You can now completely re-arrange all of your fields and sections! Don’t want to follow the standard layout? No worries, adjust it to how YOU want it.

<figure><img src="/files/yF5ZikuFQKhYFFiggvE0" alt=""><figcaption></figcaption></figure>

#### **Create custom sections, then group and order your fields into their relevant sections**

You can now create custom sections to group your vulnerability fields into where they belong, according to you.

<figure><img src="/files/m4HJi5JogCFnvluca7vw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/fDcBAxv4Y1IXU16uODQX" alt=""><figcaption></figcaption></figure>

#### **Re-name and re-order existing sections**

Existing sections can be re-named, re-ordered or even dismantled. When we say you have full control – we really mean it!

#### **Use logic to show relevant vulnerability sections and fields based on the testing types assigned to the project**

Hide Expressions are now supported on sections.&#x20;

Project fields are now also supported in vulnerability hide expressions.

This means you can create vulnerability forms which are relevant to your projects – creating a more personalized vulnerability experience!

<figure><img src="/files/ZFILGdGlvJLBZC0GxdL4" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oX1wfmr7bF3TyEiRv6yJ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/EJeuM5sKApYOhre6lXyE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/o92y6nS0p9UsX0AJcZRO" alt=""><figcaption></figcaption></figure>

#### **Improved vulnerability page view to match your preferred vulnerability user experience**

When viewing a vulnerability, you will now see the relevant sections and fields for that vulnerability.

You can also control the order in which the sections and fields are displayed, so you can view the data the way you and your customers need it!

<figure><img src="/files/Q73ksGUcn3XMXIhMko5f" alt=""><figcaption></figcaption></figure>

### ReportGen v2.8

We have just released another massive update for AttackForge ReportGen: The ultimate pentest reporting tool!

This release includes the much-anticipated ReportGen CLI tool; ReportGen NodeJS library; support for testing your Combined Report templates; one (1) new option; two (2) new styles; and support for charts in loops - adding even more power to your reports!.

#### Introducing ReportGen CLI and NodeJS library

<figure><img src="/files/WD9FQrpdaMsLfOi8I5sT" alt=""><figcaption></figcaption></figure>

This release comes with two new ways you can build or program your reports.

The **ReportGen Command Line Interface (CLI)** tool is ideal for people who prefer to build pentest reports on the command line; and combine ReportGen into an existing automation or pipeline.&#x20;

You can create automations combining ReportGen CLI with Self-Service API Events.

For example, you can create *real-time automated PDF reports* and have them securely emailed to your customers, posted to a Slack/Teams channel, or uploaded to a ticket.

For more information on how to do this, check out our [Blog](https://blog.attackforge.com/blog/reportgencli).

The **ReportGen NodeJS library** is ideal for people who want to simply "import" ReportGen into their existing codebase or scripts and build custom penetration testing reports easily and effortlessly natively in your own code.

You can download ReportGen CLI and NodeJS library directly from [NPM](https://www.npmjs.com/search?q=attackforge).

<figure><img src="/files/ZelF43r3ngAkHc2KAZqM" alt=""><figcaption></figcaption></figure>

#### Combined Reports now supported

Building Combined Reports has never been easier – now you can use the ReportGen browser tool to create and test your Combined Report templates.

These reports combine multiple project JSON files, to create a single report using data from multiple projects.

To do this, simply select multiple JSON files on the *'Select Your JSON File'* step when using the ReportGen browser tool.

For more information on Combined Reports, visit the [Support Centre](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-tags%22%20%5Cl%20%22tags-for-combined-reports).

#### New Option: Enable/Disable Image Figure

This release introduces a new option which allows you to enable or disable automated figures which are inserted for every inline image contained within the styled tags.&#x20;

You can disable automated figures as follows:

*{@...\_styled(“image\_figure”:“none”)}*

For more information on how to enable this option, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-tags%22%20%5Cl%20%22tags-for-combined-reports).

#### New Style: Image Display Style

This release introduces a new style which allows you to independently set a style for inline images contained within the styled tags.

This allows you to have finer control over the styling which is applied to the images.&#x20;

For example, this [guide](https://github.com/AttackForge/ReportGen/issues/33) will show you how to automatically apply a border to every inline image in your report, using this new style.

For more information on how to apply this style, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-styles%22%20%5Cl%20%22microsoft-word-character-styles).&#x20;

#### New Style: Image Description Style

This release introduces a new style which allows you to independently set a style for inline image descriptions contained within the styled tags.

This allows you to have finer control over the styling which is applied to the descriptions which appear beneath the inline images, for example the captions or filenames.

For more information on how to apply this style, visit this [link](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-styles%22%20%5Cl%20%22microsoft-word-character-styles).

#### Charts now supported in loops

You can now create charts inside loops. For example, this is useful if you are creating a new chart for every vulnerability.

### Improved Writeup Access Controls

We have improved the access controls you can set on your Writeups libraries.

You can now independently assign View or Edit access to every Writeups library, including the Main, Imports, Project and Custom libraries.

This means you can give people access to see writeups in a particular library, without having to risk them making any changes.

You can assign View access to allow users see the writeups, link them to their vulnerabilities on projects, or even create derivatives in a library they have Edit access to.

Access to libraries can now be assigned to any user based on their Role, membership on Groups, or individual assignment.

If you are using Groups, you can now assign those groups to the libraries – making Writeups library access easier to manage.

You also no longer need to assign the Library Moderator role. The new Writeups access controls will be applied based on your configuration settings, without having to change any user’s personal settings or user role.

<figure><img src="/files/u7wa7fIeeJ2yLUbMqRcE" alt=""><figcaption></figcaption></figure>

### New File Uploads & Inline Images

You can now upload files and evidence, and set inline images, for the following:

* Writeups
* Remediation Notes
* Test Cases
* Test Case Notes

This means you can now:

* Configure images/diagrams/illustrations to support your vulnerability descriptions, attack scenarios and remediation recommendations. You only need to set this one time during creation/editing of the writeup in your library.
* Capture your remediation evidence directly against the remediation notes for every round of retesting. Even your engineers and customers can upload evidence too against their remediation notes!

<figure><img src="/files/7HMi2E6m3CexDymkCuZM" alt=""><figcaption></figcaption></figure>

You can also:

* Configure images/diagrams/illustrations to support your test case details and execution flows. You only need to set this one-time during creation/editing of the test case in your library.
* Capture your test case evidence directly against the test case notes.

<figure><img src="/files/PYNWt2ZOXbiwGbW7JkW4" alt=""><figcaption></figcaption></figure>

You can also display these images in your reports using the {@...\_styled} tags and the files are also available via the Self-Service APIs.

### User Experience Enhancements

#### Preserved Table Filters, Sorting and State

We have improved the user experience when interacting with tables in AttackForge.

Now, when you make any changes to your projects or vulnerability tables, for example when you filter your data, sort your columns, or even view data from a particular table page – if you navigate away from the table, for example you click on a link to view the data – when you come back to the table, everything remains preserved as if you never left the table!&#x20;

This makes it easier to configure your tables with your preferred filtering and sorting and ensure that AttackForge remembers that for you for the duration of your session.

#### Responsive Design for Narrow and Wide/4K Screens

We have redesigned the user interface to better support narrow screens, wide screens and high-resolution 4K screens.

There is now more information presented on every screen, making it easier to see the important information you need without having to scroll the page or table.&#x20;

It also supports better dashboards and analytics views.&#x20;

#### Access Entire Asset Tables on Project Create or Project Scope

You can now filter and view all asset-related data when creating a project, as well as viewing and managing scope on a project after creation.

This makes it easier to find the exact assets you need to include on your project, as well as bulk actions for easy application to projects.

<figure><img src="/files/1gSL3DramjwLzRNSkMui" alt=""><figcaption></figcaption></figure>

#### Suppress Email Notifications on Review Notes

You can now suppress email notifications when creating review notes.&#x20;

This can help to reduce email noise during review cycles and focus notifications on the areas that matter most.

#### Drag-and-drop table settings

You can now drag-and-drop your columns in your table settings.

This makes it easier to re-organise your tables into your preferred viewing style.

<figure><img src="/files/c4e3nyk4CXvt3Zns3IUU" alt=""><figcaption></figcaption></figure>

#### Preview images by clicking on them

Now you can click on any inline image and open it in an image previewer.

#### Edit Remediation Notes and Test Case Notes

You can now edit your remediation notes and test case notes.&#x20;

This applies to owners of the record, or Administrators.

#### New columns for vulnerability, test case and user tables

We have added more data columns in more places.&#x20;

Vulnerability tables now include Status Last Updated datetime.

Users table also now includes login type i.e. local or SSO.

Test cases table also includes a count for all linked vulnerabilities for each test case.

#### View linked Project Request from Project, and vice-versa

You can now see the linked project request from the project dashboard and navigate to it.

You can also navigate to a linked project from the actioned project request.

#### Edit Test Cases from the Project Test Case page

There is now a shortcut button to edit a test case in the Test Suites module directly from the project test case page.

This makes it easier to make changes to test cases when required.&#x20;

#### Back to MFA QR code

When you are now enrolling your mobile authentication app for MFA by scanning the QRcode, there is an option to go back to the QRcode in case there are issues when trying to enter in the code.

#### Support Centre Is Now More Accessible

We have added more ways to access the [Support Centre](https://support.attackforge.com/attackforge-enterprise/getting-started), including from the user actions menu in the navigation bar.

#### Deleted users show at bottom of users lists

User select fields now group deleted users and present them at the bottom of the list, making it easier to search between active and deleted users.

### New Rich-Text Fields

Our AttackForge customer community asked for it, and now it’s here.&#x20;

Rich-text fields are now supported for the following fields in application and well as in reports and via the Self-Service APIs:

* Remediation Notes
* Vulnerability Notes
* Project Workspace Items
* Test Cases: Details field and Execution Flows
* Abuse Cases
* Test Case Notes

Make sure to update your reporting templates to use the {@...\_styled} variation of the relevant tag, to ensure it renders as rich-text in your reports.

<figure><img src="/files/4SvKh4oLhLAEgSFTQmXs" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oGSTXK42tZjluZsC21Cz" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/V4OXNEVjWrBWGGCCmUwh" alt=""><figcaption></figcaption></figure>

### New Test Suites & Import Test Cases

You can now easily import test cases into your test suites, in JSON, CSV or AttackForge Community formats.

<figure><img src="/files/2I1phcTd7MatE1T2VeZJ" alt=""><figcaption></figcaption></figure>

We have also released a [GitHub repository](https://github.com/AttackForge/TestSuites) with the latest industry testing standards, which you can now easily import into your test suites.

This helps to keep you up to date on the latest developments in testing standards.

The following standards are now supported:

* OWASP Web Security Testing Guide Version 4.2
* OWASP Application Security Verification Standard (ASVS) Version 4 - Level 1
* OWASP Application Security Verification Standard (ASVS) Version 4 - Level 2
* OWASP Application Security Verification Standard (ASVS) Version 4 - Level 3
* OWASP Mobile Application Security Testing Guide (MASTG) Version 2 - Level 1
* OWASP Mobile Application Security Testing Guide (MASTG) Version 2 - Level 2
* OSSTMM Version 3 - Human Security Testing
* OSSTMM Version 3 - Physical Security Testing
* OSSTMM Version 3 - Wireless Security Testing
* OSSTMM Version 3 - Telecommunications Security Testing
* OSSTMM Version 3 - Data Networks Security Testing

<figure><img src="/files/FWU7UkdtTW9GxB3bcKso" alt=""><figcaption></figcaption></figure>

### Import Writeups

You can now easily import test cases into your test suites, in JSON, CSV or AttackForge Community formats.

<figure><img src="/files/wDosolFBDEeX5xjHCt3e" alt=""><figcaption></figcaption></figure>

### New Features

#### Force user password change on next login

We have added an option which allows Admins to force a local user account to set a new password upon next login.

This is useful if you are inviting new users to your AttackForge and the user is not using the password reset workflow.

This feature can be enabled when creating/inviting the user, or via the users’ access settings page.

#### Bulk archive, bulk restore and bulk destroy projects

#### Bulk delete and bulk restore users

#### Bulk archive and bulk restore assets

We have added bulk actions on the projects, users and assets tables – to help reduce repetitive tasks.

We will be extending these actions over the next few releases to help further to improve efficiency and reduce repetitive tasks in AttackForge.

#### Import vulnerabilities from Checkmarx SCA

We have added an option to import vulnerabilities from Checkmarx Software Composition Analysis (SCA).

This import option supports JSON and XML export formats.

#### Review Notes without topics

You can now create review notes without specifying a topic.

This is useful if you have generalized comments relating to entire vulnerabilities or executive summary.&#x20;

#### Importing vulnerabilities - improved import feedback & skip duplicate detection

We have improved the user experience for importing vulnerabilities.

Now, when you import vulnerabilities – you can access the results for every vulnerability which was:

* Created
* Skipped
* Failed
* Not Imported

<figure><img src="/files/CY2cRTdsbs34QMkqSv2m" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/nLsmmdUiSj6cTtLDxMcY" alt=""><figcaption></figcaption></figure>

You can now also skip duplicate detection when importing vulnerabilities.&#x20;

This could be useful if you need to force an import, regardless of the existing vulnerabilities on the project.

<figure><img src="/files/BGMk5uGyeNz07IJS1tMt" alt=""><figcaption></figcaption></figure>

### Updates to Self-Service API

In this release we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

#### New REST Endpoint: UpdateVulnerabilitySLAs

We added a new endpoint which can be used to programmatically apply or update vulnerability remediation SLAs.\
See link for more information:&#x20;

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updatevulnerabilityslas>

#### New REST Endpoint: RegenerateAPIKey

We added a new endpoint for programmatic rolling of Self-Service API key for user.\
See link for more information:&#x20;

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/regenerateapikey>

#### New REST Endpoint: AddTestCasesToTestSuite

We added a new endpoint which allows adding bulk test cases to a test suite.\
See link for more information:&#x20;

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/addtestcasestotestsuite>

#### New REST Endpoint: GetFormConfig

#### New REST Endpoint: UpdateFormConfig

We added two new endpoints which allows to retrieve and update the new vulnerability form builder config.\
See links for more information:&#x20;

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getformconfig>
* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateformconfig>

#### New Event: VulnerabilityEvidenceCreated

#### New Event: VulnerabilityEvidenceUpdated

We added two new events for real-time notifications when evidence is created and updated on vulnerabilities.\
See links for more information:

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-evidence-created>
* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-evidence-updated>

#### New Event: VulnerabilityRemediationNoteCreated

#### New Event: VulnerabilityRemediationNoteUpdated

We added two new events for real-time notifications when remediation notes are created and updated on vulnerabilities.\
See links for more information:

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-note-created>
* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/vulnerability-remediation-note-updated>

#### GetProjects now supports advanced query filtering

We added support for Advanced Query Filtering for the GetProjects REST endpoint.\
See link for more information:

* <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojects>

#### GetProject\* Endpoints Now Return project\_testsuites

We updated all GetProject related endpoints to return the test suite names and ids assigned to the project.

### Video Tutorials

#### Vulnerability Form Builder

We created a short video on how to effectively use the new Vulnerability Form Builder included in this new release.

{% embed url="<https://youtu.be/vPZU3LxqvUE?si=73Pbu3rDjCI7mzVP>" %}

The video is available on YouTube: <https://www.youtube.com/watch?v=vPZU3LxqvUE>

#### Infrastructure Penetration Test

We also created a short video on how to perform an efficient infrastructure penetration test in AttackForge.

{% embed url="<https://youtu.be/IT74fi75-G4?si=eWNhe2N7Y_5WfVFr>" %}

The video is also available on YouTube: <https://www.youtube.com/watch?v=IT74fi75-G4>

## 2023-07-31

### New Workflows: Grouped Assets on Vulnerabilities

One of the most highly anticipated and requested workflows has just arrived!

Introducing Grouped Assets on Vulnerabilities.

You now have a choice for how ***you*** want to create and use vulnerabilities:

* Create unique vulnerabilities on every project, and assign relevant affected assets to each unique vulnerability;
* Create individual vulnerabilities for every asset; or
* Create a combination of unique vulnerabilities and individual vulnerabilities – for ultimate flexibility!

A single vulnerability can now have many affected assets assigned to it.

This can include detailed information for each affected component on every asset.

Using grouped assets on vulnerabilities can help you to:

* Increase efficiency when working on infrastructure penetration tests;
* Reduce the overall number of vulnerabilities whilst preserving affected asset data;
* Reduce effort required for quality review cycles on vulnerabilities;

#### Improve vulnerability importing with grouped assets

When you next import vulnerabilities on your project – you will have a choice between selecting *Individual* or *Grouped*.

Individual will allow you to import your vulnerabilities as you always have.

Whereas Grouped will allow you to automatically group affected assets for each vulnerability.

In the example below, we can see there was a **94% reduction in vulnerabilities**, whilst preserving the same amount of data.

This means you can focus your attention on the important vulnerabilities and track their affected assets much more efficiently.

<figure><img src="/files/h6ufQ5Q6I2VBZnqkbjEw" alt=""><figcaption></figcaption></figure>

You can view all of the affected assets, and for each asset – see related data for its affected components.

<figure><img src="/files/JEijQUcOpjdV7lwMYm41" alt=""><figcaption></figcaption></figure>

You can configure the Grouping options to adjust the rules for how the grouping is performed.

Once you have made your selection, you can move to the Edit and Review step.

Here you can see the final set of vulnerabilities for selection and make any remaining adjustments as needed prior to import.

<figure><img src="/files/Bo82icKzMBSATcQ2XA89" alt=""><figcaption></figcaption></figure>

You can still choose to configure your import options such as dynamic parser actions and selection of libraries.

Once your import begins, you will be kept update to date with its progress.

<figure><img src="/files/aTBg4LEF81LTCSvB8h0J" alt=""><figcaption></figcaption></figure>

And once it’s finished, you will see a summary of the import and option to view the vulnerabilities.

<figure><img src="/files/afl8mLVIUJspR5fcuilh" alt=""><figcaption></figcaption></figure>

Vulnerabilities with grouped assets will now show in your vulnerability tables, with option to expand each vulnerability to see its asset data.

<figure><img src="/files/icxAJoHbxsNqUDNMeWUN" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TmGzSGtXAYXyvPkeLOjS" alt=""><figcaption></figcaption></figure>

#### Register multiple affected assets for every vulnerability

When you next create a vulnerability – you will have a choice when determining how you want the assets to be assigned and tracked on the vulnerability.

You can choose between *Individual* and *Grouped*.

Individual will allow you to assign your assets to vulnerabilities as you always have.

Whereas Grouped will allow you to create a single vulnerability and assign all affected assets to the vulnerability.

<figure><img src="/files/chtRUTD8hoq8CIsVYG1A" alt=""><figcaption></figcaption></figure>

#### Capture and retain asset & component data on the vulnerability

Every asset can have its own notes, tags, and components.

Components can be used to track which part(s) of the asset has the vulnerabilities.

Every component can also have its own notes and tags.

<figure><img src="/files/P3ErYAcfjAnylzbGYTxO" alt=""><figcaption></figcaption></figure>

#### Track actioned status for each affected asset

Every asset can be individually tracked and actioned.

This is useful for monitoring the progress against assets on a vulnerability.

<figure><img src="/files/Z13QRc9nRNu5mxgEVf6r" alt=""><figcaption></figcaption></figure>

### ReportGen v2.7

We have just released another massive update for AttackForge ReportGen: The ultimate pentest reporting tool!

This release includes two (2) new Pentest Report Templates; support for grouped assets on vulnerabilities; a new support site for ReportGen; two (2) new options; seven (7) new filters; new styles; a new function; support for figures and more - adding even more power to your reports!.

<figure><img src="/files/7SUnQj6CKNr7fVmMpdlv" alt=""><figcaption></figcaption></figure>

#### Support for Grouped Assets on Vulnerabilities

Support for grouped assets has been added in this release of ReportGen.

For details on how adjust your template to take advantage of grouped assets, please visit this [Support Page](https://github.com/AttackForge/ReportGen/issues/28).

#### Pentest Report Template 3.1 and 3.2

This release introduces two (2) new pentest reporting templates:

* Pentest Report v3.1 - a template showcasing the features available in ReportGen v2.7+
* Pentest Report v3.2 - a template with minimal logic which can be used out-of-the-box, and has support for grouped assets on vulnerabilities

We have also released an updated example JSON test file which can be used for testing your templates.

#### GitHub Community Support Site

As part of our mission to support the growing community of AttackForge users, we have released a new dedicated Support Site for ReportGen.

This Support Site provides:

* Information on getting started with ReportGen;
* Template examples to achieve common use cases and reporting needs; and
* Place to ask questions and receive tips and help from our support team and the community.

We hope the new Support Site for ReportGen will make it easier for everyone to build awesome testing reports, with minimal effort!

You can access the new ReportGen Support Site from <https://github.com/AttackForge/ReportGen>.

#### New Option: Custom Styles for Individual Rich-Text Fields

You can now assign individual rich-text fields to different custom styles which are in your template.

This feature can be used with {@execSummaryNotesStyled}, {@description\_styled}, {@attack\_scenario\_styled}, {@remediation\_recommendation\_styled} or any styled custom fields.

For more instructions and details, please visit this [Support Page](https://github.com/AttackForge/ReportGen/issues/24).

#### New Option: Image Options Supported For All Styled Tags

In the previous release of ReportGen, we added support for including custom options to configure how your image descriptions are displayed in reports.

You can configure the images to show captions; prefer captions; show filename or show nothing.

In this release, we extended this feature to support any styled tags, including your own custom rich-text fields.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-options#toggle-image-description-filename-caption).

#### Styled Custom Fields

In this release, we added support to render custom rich-text fields.

You need to use the following format for the tag in order to render it in the report:

{@KEY\_styled}

Where KEY is substituted for the custom field key for the rich-text field.

#### Add Figures For Images

All images will now automatically prefix *Figure X:* to the image description.

This means you no longer need to manually inject figure numbers for each of your images inserted dynamically by ReportGen.

Figure numbers take advantage of Microsoft Word dynamic fields so you can easily update them if you need to manually insert any new images.

#### New Function: $equalsRegex

You can use this new function to perform an equality comparison for a variable against a value using a Regular Expression test.&#x20;

It performs a global, case insensitive test. For example, you can use it to test whether data is a URL, or an IP Address.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdequalsregexp).

#### New Filter: Float

You can convert a number to a floating-point number.&#x20;

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#float).

#### New Filter: Integer

You can convert a number to an integer.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#integer).

#### New Filter: Round

You can round a number to the nearest integer.

For more instructions and details, please visit this [Support Page](/app/modules/projects).

#### New Filter: RoundUp

You can round a number up to the nearest integer.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#roundup).

#### New Filter: RoundDown

You can round a number down to the nearest integer.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#rounddown).

#### New Filter: Capitalize

You can capitalize the tag. The first character will be uppercase, all others lowercase.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#capitalize).

#### New Filter: Titlecase

You can title case the tag. Words will start with uppercase letters, all remaining characters are lowercase.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#titlecase).

#### Styled and Labelled $help

To make debugging easier, we have added styled and label-supported $help functions.

Now when you use the $help function, the browser console will style and color-code it according to whether it relates to Scope or Variables.

In addition, you can pass labels to every $help function to make it easier to debug your template and is especially useful when printing multiple $help statements.

For more instructions and details, please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdhelp).

#### Improvements in Removing Line-breaks Before and After {@rawXML} Tags

Now when you insert any tags in your template which contain rawXML, such as styled tags for rich-text fields - the line breaks above and below the data will be automatically removed. This makes your reports look cleaner and reduces the need for manual post-generation efforts to remove the additional line breaks.

#### Test Case Workspace Notes

You can now inject your Test Case Workspace Notes into your JSON export, to make the data available for reporting purposes.

To do this, go to the Administration module, and from the Projects menu - select Test Case Workspace Notes from Export Project as JSON Additional Items section.

### New Functionality

#### Image Thumbnails and Preview

You can now view thumbnails for any uploaded image, as well as preview the image within the browser instead of having to download it.

<figure><img src="/files/gtWuoM1rkgTubux90W7m" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/YDB8tmD5f9A0obDDuQZr" alt=""><figcaption></figcaption></figure>

#### Import Assets from NMAP and Masscan

You can now import assets directly to your projects from your NMAP and Masscan files.

This will save you tons of time having to create assets manually!

<figure><img src="/files/R2sfbl6bVumItLNt3IH6" alt=""><figcaption></figcaption></figure>

You can also take advantage of the additional Hostnames and Ports fields if you are using the Assets Module.

These fields will be stored against the Asset in the module, so you can monitor and manage Hostnames and Ports centrally (outside of your projects).

<figure><img src="/files/HBIJtqDHeZ4KmiZrIgYg" alt=""><figcaption></figcaption></figure>

You can also view and modify the data prior to importing.

<figure><img src="/files/kloDegyMtzCyL3MIDCaQ" alt=""><figcaption></figcaption></figure>

#### Download Vulnerability Selection as JSON

You can now export a JSON file for a selection of vulnerabilities only.&#x20;

This will include all of the reporting data for those vulnerabilities.

<figure><img src="/files/SpIR76SRsUPt3YM4oZsc" alt=""><figcaption></figcaption></figure>

#### Search All Writeups Across All Writeup Libraries on Vulnerability Create/Edit

Now when you create a new vulnerability; or edit an existing vulnerability – you can search all of your Writeups which you have access to, without having to first select a library.

<figure><img src="/files/z8tR1bnocuitxdZ2YbE1" alt=""><figcaption></figcaption></figure>

#### New Custom Field: List

You can now create custom fields using the new ‘List’ type.

Lists are great for assigning multiple inputs for a field, for example creating your own tags or actions.

List types are also the required type when choosing to include Hostnames and Ports on Assets using the new NMAP and Masscan import options.

<figure><img src="/files/p1yVz1V8DiS1xoFF0Mpa" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/yNND5wIfSx9jsmHXHKLN" alt=""><figcaption></figcaption></figure>

#### Bulk Archive Writeups

You can now bulk archive writeups via the Writeups module. This makes it easy to remove unwanted writeups.

Archiving writeups will not impact any of your existing vulnerabilities which already reference those writeups.

#### Bulk Assign Assets to Test Cases

You can now bulk assign assets to test cases on a project.&#x20;

This is useful when you need to specify which assets in-scope for testing apply to each test case.

<figure><img src="/files/BIE7L0qcAFmNIAIBKyfF" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/jtKVdWTnoRIXP8J2kVDu" alt=""><figcaption></figcaption></figure>

### User Experience Improvements

#### Performance and Search Improvements

We have improved the server performance when searching or accessing any of the following data within AttackForge:

* Projects
* Assets
* Vulnerabilities
* Writeups

This means you should have lightning-fast response times when loading pages and menus!

#### View Asset Module Data on Project Scope

Privileged users can now view all of the asset data for in-scope assets directly from the Project Scope page.

This means you no longer need to access the Asset Module in order to get the data, and you can use the advanced filters to search your projects assets!

<figure><img src="/files/KYWET5qBozpT6dABhUc9" alt=""><figcaption></figcaption></figure>

#### Send Test Email Notifications for Custom Emails

You can now send test emails for any of the custom time-based emails configured in your Administration options.

This makes it easier to verify that your custom email rules are correctly applied and ensure your emails are looking exactly the way you need them to be!&#x20;

<figure><img src="/files/vnS02UbrsYdFomXLYevl" alt=""><figcaption></figcaption></figure>

### Updates to Self-Service API

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

#### GetProjectsAndVulnerabilities now supports advanced query filtering on projects and vulnerabilities

#### GetVulnerabilities now supports advanced query filtering on writeups

We have updated the *GetProjectsAndVulnerabilities* endpoint to support *q\_project* and *q\_vulnerability* advanced query filters.

We also updated the *GetVulnerabilities* endpoint to support the *q\_writeup* advanced query filters.

Advanced query filters allow you to create database-like custom queries which give you the power and flexibility to get the exact data you need.

This saves you the time and hassle of having to create integration code to make multiple queries or filter out the data you do not need.

For more information on how to take advantage of advanced query filters – please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/advanced-query-filter).

#### New REST Endpoint: DownloadWorkspaceFile

We created a new RESTful API endpoint - *DownloadWorkspaceFile* - which can be used to download a file from a projects’ workspace.

For more information on how to use this API – please visit this [Support Page](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/downloadworkspacefile).

## 2023-04-17

### Custom Fields Upgrade

#### **Set access controls on custom fields! Tailor your custom fields for roles, groups and users**

You can now configure view & edit access controls for your custom fields, and apply them to individual roles, groups or users.

This opens a world of new possibilities, for example:

* **Create custom project request forms for different customers, teams, business units or individual users**
  * Personalize your project request forms to your customers’ needs and requirements
  * Set up tailored forms for your pentest-as-a-service (PTaaS) to match your customers’ needs or subscription-level
* **Have custom project fields for admin-eyes only, or for pentesters – without your customers seeing them**
  * Set project budgets; admin notes; integration fields – ensure confidentiality with access controls
  * Configure project-level information for only your project coordinators or pentesters to see
* **Define custom vulnerability and writeup fields for different pentest teams**
  * Create personalized vulnerability and writeup forms for infrastructure teams, application teams, remediation teams, etc.
* **Configure custom vulnerability and writeup fields for different customers**
  * Control what vulnerability information is shared with which customers or teams
* **Assign custom asset and portfolio fields for different customers**
  * Configure information that is only relevant for specific customer assets or portfolios

<figure><img src="/files/kt2L0zj6cLcB28Zu81Pg" alt=""><figcaption></figcaption></figure>

You can also preview what your users can see using the ‘view-as’ feature.&#x20;

This can help you to easily and quickly configure and manage your access controls.

<figure><img src="/files/LcsIsPqhUF0VobxCTOPY" alt=""><figcaption></figcaption></figure>

All custom field access controls are also honored via the APIs, so unauthorized users cannot view or edit custom fields they are not supposed to.

#### **New custom field types: Table, Rich-Text, User & Group**

You can now configure custom fields with the following types:

* Table
* Rich-text
* User(s)
* Group(s)

These new types allow you to capture information on projects, vulnerabilities, assets, writeups, portfolios and test cases in ways never seen before in AttackForge.

Combining these new custom field types with the new ability to set access controls on custom fields, you can have this information available only to people with need-to-know.

<figure><img src="/files/oRmmghla47JDe1IjvCBY" alt=""><figcaption></figcaption></figure>

* **Table custom fields**

Table custom fields are a great way to collect and use data in AttackForge and in your reports which is in a tabular format.

For example, you may be performing a configuration or firewall review and the data from your tools only outputs into CSV or tables.

Now you can easily import that data from the APIs, edit the data in the application, and display the data in your reports.

* **Rich-text custom fields**

Rich-text custom fields have been one of the most requested custom field types – and its finally here!

You can set up rich-text enabled custom fields to use for your writeups or vulnerabilities, and best of all – it is supported in reports as well, so you can have custom styled fields easily and effortlessly showing for your customers. This also means you can enter data using lists, or create sections in your data using headings, or even highlight code snippets.

* **User custom fields**

User custom fields open the door to possibilities to assign users in AttackForge to projects or vulnerabilities.

For example, you may want to create Peer Review and Tech Review fields on your vulnerabilities and assign users accordingly.

Or you may want to associate a Level 1 Owner and a Level 2 Owner to certain vulnerabilities. This is all now possible.

User custom fields support single-select and multi-select, for cases when either one or many users can be assigned.

* **Group custom fields**

Group custom fields also opens many possibilities, such as assigning groups to vulnerabilities.

For example, you may want to associate a particular group to a vulnerability who are tasked with responsibility to fix it.

Group custom fields support single-select and multi-select, for cases when either one or many groups can be assigned.

#### **New custom field category: Test Cases**

We have added support for a new category of custom fields – test cases.

You can now define custom fields on your test cases and have this information available to your pentesters or customers.

We have also extended support for this new category in the Self-Service APIs.

<figure><img src="/files/ITVpDshqO4El6Vi12zYB" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/dm1CKiK35GBQI0J5ceJE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/WkzWSzDqiTEVLme551Qv" alt=""><figcaption></figcaption></figure>

#### **Configure rich-text information messages for custom fields**

You can now assign a custom information message to display in the information panel when users are completing your custom fields.

This is useful to help guide users on what information to enter in or select when filling in forms within your AttackForge.

<figure><img src="/files/f9hU2QK3x6FVzvhSKuAC" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/DfMhw3aWkzlKsyYOIhHT" alt=""><figcaption></figcaption></figure>

#### UX improvements on (re)ordering custom fields

You can now reorder your custom fields using drag-and-drop or clicking on the up and down buttons.

This makes it easy and efficient to set up your forms the way you need them to look.

### New Configuration

#### Set access controls on reporting templates! Tailor your reports for roles, groups and users

You can now configure access controls on your reporting templates. This makes it possible to:

* Have different reports for different customers
* Personalize reports to your customers needs, for example add their logo or only the data that they need
* Separate reports that your security team uses to that of your customers
* Tailor reports for different business units, without conflict!

<figure><img src="/files/bmJQllQjVdjWzIQxpp51" alt=""><figcaption></figcaption></figure>

#### Delegate adding project test suites and abuse cases to other roles and users

You can now delegate the ability to add test suites and abuse cases on projects to other roles and users.

This makes it possible to now allow your pentesters to have authority to perform this action when needed.

These delegations can be applied to entire roles from Administration page, or to individual users via the User --> Access --> Delegations feature.

#### Configure presets for custom import mapping rules

You can now configure custom rules for your pentesters to use when performing an import of vulnerabilities.

These rules work as dynamic custom parser actions, telling AttackForge how to map the imported vulnerability to a correct entry in your chosen writeups library.

It’s a great timesaver and made even more efficient now that you can save predefined rules and let your pentesters chose the relevant rule (and extend upon it) when importing.

Custom rules can be configured in Administration --> Vulnerabilities.

<figure><img src="/files/1i1Rl2tooAqK2pfp1Mnd" alt=""><figcaption></figcaption></figure>

#### Change login page background color

You can now configure a custom background color for your login page.

<figure><img src="/files/fNW2sFAV0oj7pwXpqibY" alt=""><figcaption></figcaption></figure>

### ReportGen v2.6

We have just released our biggest update ever (v2.6) for AttackForge ReportGen: The ultimate pentest reporting tool!

This release includes a new Pentest Report Template (v3); support for charts; four (4) new functions; three (3) new filters; new options; new styles, new variables and updates to existing filters and functions add even more power to your reports!

All examples mentioned in these release notes can be found on the homepage of the ReportGen tool.

<figure><img src="/files/DLV2UrhuNCo2QQZiGrCI" alt=""><figcaption></figcaption></figure>

#### Introducing Pentest Report Template v3

This release introduces a new contemporary pentest reporting template - showcasing the possibilities now available in ReportGen v2.5+.

The new Pentest Report Template v3 includes:

* Logic for a multi-phase project e.g. Web App Pentest + Infrastructure
* Redesigned Executive Summary, using custom Charts
* Redesigned Summary Findings
* Redesigned Vulnerability Details with more information and enhancements
* Redesigned Test Cases Details

This new template can be downloaded directly from within the ReportGen tool.

#### Introducing Charts

You can now create custom charts in your reports! The following charts are supported:

* Vertical Bar Charts
* Horizontal Bar Charts
* Pie Charts
* Donut Charts

Charts work with any data. You can create charts for your vulnerabilities, exec summary, test cases, attack chains or even categorize your data.

Charts also support Scope and Variables.

Every chart comes with configuration options (e.g. colors, font sizes, spacing, etc.) so you can configure and style the chart to your preferences.

#### New variable type: Dictionary

A Dictionary is a flat list of key:value pairs. It can be useful for capturing dynamic data, or for grouping data.

You can refer to the Dictionary anywhere you need it in your report.

Dictionary is supported on the following Functions: $declare, $push, $assign, and $keys.

* Example 1: Using a Dictionary to count all affected assets for every vulnerability, then prints the count alongside the vulnerability name.
* Example 2: Using a Dictionary to store every phase of testing e.g. Web App, Ext. Infrastructure, Int. Infrastructure etc. along with each vulnerability associated to each phase of testing, then print the phase and its vulnerabilities.

#### Combining filters in functions

We have made it possible to now combine Filters with your Functions!

This can be achieved in two (2) different ways:

* Example 1: Using a Filter inside the Function
* Example 2: Chaining a Filter to the output of a Function

#### New styled text: description, attack scenario and recommendations

For AttackForge Core and Enterprise users, you can now style your vulnerability descriptions, attack scenarios and remediation recommendations!

These tags will render a styled version based on the style set in-app using the WYSIWYG editor.

To switch over to the new styled tags, update your template to include the new tags.

```
{@description_styled}
{@attack_scenario_styled}
{@remediation_recommendation_styled}
```

#### New styles: AF Normal and AF List

We have added support for two (2) new styles:

* AF Normal which can be used to create a custom style for normal text inserted via the {@...\_styled} tags.
* AF List which can be used to create a custom style for bullet and numbered lists inserted via the {@...\_styled} tags.

These new styles provide the ability to have custom formatting for how your normal text and lists are displayed in your reports when using the {@...\_styled} tags.

To get started, create two new styles inside your Word template with the names 'AF Normal' and 'AF List'. Then apply a format to these styles.

When ReportGen builds your report, it will automatically map to these styles for you.

#### New option: configure image descriptions

This option can be set against the {@proof\_of\_concept\_styled} tag in order to adjust how the filename or caption is displayed under an image.

* image\_description: caption - will display the caption if it exists, otherwise will display nothing.
* image\_description: prefer-caption - will display the caption if it exists, otherwise will display filename.
* image\_description: caption - will display the filename.
* image\_description: none - will display no caption or filename.

#### New function: $keys

You can use this new filter to retrieve the value for a Dictionary.

* Example: Using a Dictionary to count all affected assets for every vulnerability, then prints the count alongside the vulnerability name.

#### New function: $isFirst

Use this function to check if you are in the first iteration of a loop.

For example, if you want to add a section heading BEFORE printing the vulnerability titles.

Another example is if you want to check if it IS NOT the first iteration of a loop.

#### New function: $isLast

Use this function to check if you are in the last iteration of a loop.

For example, if you want to add an extra line break after every vulnerability title except for the last.

Another example is if you want to check if it IS the last iteration of a loop.

#### New function: $index

Use this function to print the current index of the loop you are iterating over.

#### New filter: Search

You can use this filter to search for a value in a string and return the results (substring) if found.

#### New filter: Index

You can use this filter to access an item in an array using its index number.

#### Sort on custom tags and custom fields

We have now made it easy to perform a custom sort based on your custom tags or custom fields!

### New Functionality

#### Create a report with selected vulnerabilities only

You can now select one or more vulnerabilities, can create a custom report with only that selection.

This is useful when you need to get a report out to different teams, with only the context for vulnerabilities which are relevant to that team.

<figure><img src="/files/cnJNU7zO7vpkcg4Epbjd" alt=""><figcaption></figcaption></figure>

#### Rich-text editor for writeups

We have now added rich-text support for your writeups!

You can now have more detailed and styled information for your vulnerabilities.

<figure><img src="/files/WycvxWcUFLvwVgdLLyV9" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/OD9LVqR062wFxqrKimO4" alt=""><figcaption></figcaption></figure>

You can also include the styled versions in your reports using the following tags:

* {@description\_styled}
* {@attack\_scenario\_styled}
* {@remediation\_recommendation\_styled}

#### Configurable CVSS scoring for writeups

You can now independently configure CVSS Baseline, Temporal and Environmental scores for your writeups.

Simply select the relevant option from the drop-down when creating or editing your writeups.

The score will be used as a baseline when creating new vulnerabilities.

#### Link vulnerabilities between projects

You can now link vulnerabilities from one project to another.

This is useful when you want multiple projects to have a view of a particular vulnerability, or set of vulnerabilities, for example to consolidate for a round of retesting.

Linking vulnerabilities does not duplicate/clone the vulnerability, therefore your dashboards and analytics will be preserved.

Also, when linking vulnerabilities, any changes on the vulnerability, for example its set to Closed, will be applicable to all projects it is linked to.&#x20;

This makes it easy to fix it in one place, and have the results propagate everywhere.

However, if you intend to create a clone of a vulnerability, you can do that using the duplicate vulnerability feature instead.

<figure><img src="/files/byZYUs2qMKYtfIb2K6GA" alt=""><figcaption></figcaption></figure>

#### Configure your table preferences for test cases

You can now set your table preferences for when viewing test cases on a project.

This includes ability to configure:

* Default page size
* Default sort column
* Default sort order
* Which columns to display (including custom fields), and in which order

#### Specify delimiters for Affected Endpoints during vulnerability creation

You can now specify which delimiters you want to use on your Affected Endpoints when creating a new vulnerability.

This is useful if you have a delimited list which is not using commas, semi-colons or line separated; and is URL encoding friendly as you can switch semi-colon off.

AttackForge will also now show you how many vulnerabilities are going to be created based on your Asset and Affected Endpoint selections.

<figure><img src="/files/JM2tqk1rLYToJxvrwPd1" alt=""><figcaption></figcaption></figure>

#### Preserve tags when importing vulnerabilities

You can now opt-into preserving the tags on your vulnerabilities when using the import vulnerabilities feature.

This is different to the standard behaviour which inherits the tags from the writeup it is matched with during import.

This is useful if your vulnerabilities have unique tags which you need to preserve.

This option can be enabled in the import parameters section, prior to importing.

#### Updated Qualys parser to support new web application scan xml format

We have updated Qualys parser to include support for the new Web Application Scan XML format.

#### Duplicate vulnerabilities including its asset

When you duplicate a vulnerability, you now no longer need to specify the asset.&#x20;

This makes it easy to select a bunch of vulnerabilities and create an exact clone of the vulnerabilities.

You can then re-assign them to other projects if needed or adjust them using bulk actions.

#### Toggle vulnerability id in table columns

When viewing vulnerabilities in tables, you can now click on the View System Id button to see the system id for each vulnerability.

You can then toggle it back to view the custom id.

#### Toggle datetime for SLA and remediation plan table columns

When viewing vulnerabilities in tables, you can now click on the View Date button to switch the view from a countdown (e.g. 5 days) to an actual date.

### User Experience Improvements

#### Self-Service APIs are now grouped and include search bar.

We have now grouped all Self-Service APIs to make them easy to categorize, and also included a search bar to make it easy to find particular APIs.

<figure><img src="/files/vRCZJc59eTTHscxOm75V" alt=""><figcaption></figcaption></figure>

#### Renaming images will automatically update your proof of concept

Now when you rename an image you have uploaded to your vulnerabilities, AttackForge will automatically update your proof of concept for you, saving you the hassle of having to manually change that to reflect the new image name.

#### Importing vulnerabilities is now filterable by tags

When you import vulnerabilities, you can now filter your selection down using tags.&#x20;

This is useful for selecting certain vulnerabilities only, for example only exploitable vulnerabilities based on properties in the tags; or only patching or operating system related vulnerabilities.

<figure><img src="/files/flkiEVUEcI4bKgJ10ksd" alt=""><figcaption></figcaption></figure>

#### Hide expressions are now fully documented

We have now updated the documentation on our Support Centre to include full mappings for hide expressions, and examples for each data type and system field.

This makes it easier to create custom logic to hide your form fields, and only show them when the logic conditions are met.

For more information, please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions>

#### Improved modal and error handling for JIRA exports

We have updated the user experience when exporting vulnerabilities from a project into your JIRA project.

Now it will preserve your data in case there are any errors, so you can make quick changes. We have also improved the error handling and provided a new look form.

#### Informational vulnerabilities now included on project dashboard

The project dashboard will now include informational vulnerabilities by default.

You can also opt to switch it off to not display them in case there are too many informational vulnerabilities on the project

#### Vulnerability created event now shows in revision history

When a new vulnerability is created, the event is now registered in the revision history.

### Updates to Self-Service API

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

#### Updated all applicable REST endpoints to support various custom field types

We have updated all relevant APIs to now support string arrays and arrays of objects for custom fields.

This makes it possible to import data in various formats, for example:

* **Strings**
  * Strings are used to store data for **Input** fields, **Text Area** fields, **Date-picker** fields, **Select** fields and **Rich-Text** fields
* **String Array** (string\[])
  * String Arrays are used to store data for **Multi-Select** fields, **User** fields and **Group** fields
* **Array of Objects**
  * Array of Objects are used to store data in tabular format for **Table** fields.

Custom fields do not need to be configured in the administration settings in order to be created or updated via the APIs.&#x20;

However, if the custom field Key matches one that is already defined in the admin settings, it will be automatically typed to that setting when presented in the user interface.

For full details on what is supported, with examples, please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#using-custom-fields-with-apis>

#### Import historical vulnerabilities

We have updated the following APIs to support the ability to provide a *created* parameter, which overrides the default created timestamp.

* Create Vulnerability
* Create Vulnerability Bulk
* Create Vulnerability With Library

This allows you to set the date and time when a vulnerability was created, which is useful if importing historical vulnerabilities.

#### Advanced query filtering support for REST endpoint: Get Vulnerability Library Writeups

We have added support for advanced query filtering for REST endpoints: *Get Vulnerability Library Writeups*

Advanced query filtering is used to select the exact data set you would like the API to return. The filter works like a database query, where you can specify fields & operators - these help to narrow down the results to the data you would need. This filter is only supported for selected API endpoints. Please check the documentation for each endpoint for more details.

For example, you can use this filter to return:

***Writeups with title SQL Injection:***

`curl -G -X GET 'https://YOURAFTENANT/api/ss/library' --data-urlencode '`**`q={title: { $eq: "SQL Injection" }}`**`' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'`

***Writeups with the tag pluginID:53360:***

`curl -G -X GET 'https://YOURAFTENANT/api/ss/library' --data-urlencode '`**`q={tags: { $in: "pluginID:53360" }}`**`' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'`

***Writeups with the custom field NessusID and value 53360:***

`curl -G -X GET 'https:// YOURAFTENANT/api/ss/library' --data-urlencode '`**`q={custom_fields.name: { $eq: "NessusID" }, custom_fields.value: { $eq: "53360" }}`**`' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'`

The query filter supports the following operators:

* And
* Or
* Equals
* Not Equals
* In
* Not In
* Greater Than
* Greater Than or Equals
* Less Than
* Less Than or Equals
* Regular Expression

The query filter also supports datetime function, which allows you to modify the time and date to suit your query requirements.&#x20;

For example, you could ask it to show you all writeups in past 24 hours or past 7 days.

For more information on how advanced query filtering works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/advanced-query-filter>

#### Update to Get Groups REST endpoint: added option for all groups

We have added the option to get all groups if the user is an admin user, instead of just the default behavior which is to return my groups.

#### Update to Get Project Report REST endpoint: exclude binaries

We have updated this endpoint to have an option to exclude binaries from the response, for example data related to evidence files for vulnerabilities.

This is useful for integrations where the evidence data is not needed.

## 2023-01-23

### **Introducing AttackForge Version 2!**

AttackForge Version 2 is now generally available for Enterprise and Core customers!

AttackForge Version 2 sets an even higher benchmark for Pentest Management Platforms; further improving the way security and engineering teams, service providers, and customers interact and collaborate with each other on pentesting projects and programs.

<figure><img src="/files/YTplwYAPS5MiUPbvICWm" alt=""><figcaption></figcaption></figure>

We have redesigned the user interface to make it simpler to perform daily tasks, whilst providing more flexibility when you need it.

We have also made significant improvements to address feedback from customers over the years.

Some of the changes to AttackForge includes:

* Overall improvements to efficiency of daily workflows for pentesters, security managers, engineering and application teams.
* Redesigned user interface to make it faster and easier to perform many tasks, as well as addressed many previously reported limitations in version 1 interface.
* Lays the framework for many of the future roadmap enhancements we have planned for AttackForge.
* Reduced the number of screens and clicks required to perform many workflows.
* Performance improvements when using the application.
* Analytics has been improved to provide more information and comparison options.
* Portfolios has been redesigned to make it easier to track and analyze different portfolios and streams.
* Projects have been overhauled to have new dashboards; easy-to-access options; new quick actions; modals for easy data entry and access to data.
* New Vulnerabilities module, providing greater insights into your vulnerabilities.
* Administration has been redesigned to make it easier to apply configuration options.
* Self-service API documentation is now Open API v3 compliant, including more API reference documentation inside the application.
* More options in more places i.e., you can archive/clone projects from multiple areas, download reports from multiple areas, etc.
* Addressed feedback on wording and terminology to make user interface more consistent.
* New user interface components for rich-text editors, tables, dashboards, menus, forms, etc.
* And much more!..

#### New Project Dashboard

<figure><img src="/files/zB476y7EEsTgIWOcZN0o" alt=""><figcaption></figcaption></figure>

#### New Portfolios

<figure><img src="/files/ou06BfAP2WHcyglLqnFo" alt=""><figcaption></figcaption></figure>

#### New Analytics

<figure><img src="/files/kcntDPzXEO05tvtGWt2W" alt=""><figcaption></figcaption></figure>

Our Support Centre has been updated for version 2.&#x20;

You can find more information at <https://support.attackforge.com/attackforge-enterprise/getting-started>

### ReportGen v2.5

We have just released version 2.5 for AttackForge ReportGen: The ultimate pentest reporting tool!

This release includes six (6) new filters; three (3) new functions; and updates to existing filters.

<figure><img src="/files/yqDSuBdxS4eiq9fxlYJ1" alt=""><figcaption></figcaption></figure>

#### New Filter: Resize

You can use this new filter to resize images. It works by setting a max-width value in pixels. The height will be automatically adjusted to match the same ratio.

Example resizing images in Steps to Reproduce (Proof of Concept) to 300 pixels wide:

```
{#vulnerabilities}
{#affected_assets}
{#proof_of_concept}
{%inlineScreenshot | resize:[“300”]}
{/}{/}{/}
Example resizing images in styled Steps to Reproduce (Proof of Concept) to 300 pixels wide:
{#vulnerabilities}
{#affected_assets}
{@proof_of_concept_styled | resize:[“300”]}
{/}{/}
```

#### New Filter: Replace

You can use this new filter to replace data.

For example, say you had some assets that looked like this: <https://application.com>

And you wanted to remove the *https\://* part, so it appears as follows: application.com

You could do the following:

```
{#vulnerabilities}
{#affected_assets}
{asset | replace:[“https://”,“”]}
{/}{/}
```

#### New Filter: ReplaceRegExp

You can use this new filter with a regular expression to replace data.

For example, say you had some assets that looked like this: <https://application.com?query=1>

And you wanted to remove the *?query=1* part so it appears as follows: <https://application.com>

You could do the following:

```
{#vulnerabilities}
{#affected_assets}
{asset | replaceRegExp:[“[?].*”,“”]}
{/}{/}
```

#### New Filter: Split

You can use this new filter to split data based on a separator - and return a list of items.

For example, assuming you have tags in this format: tag1:value

And you only want to show the right-hand-side (value), you could do the following:

```
{#vulnerabilities}
{#tags}
{$declare[tagSplitCounter][0]}
{#this | split:[“:”]}
{#$greaterThan[tagSplitCounter][0]}{.}{/}
{#$equals[tagSplitCounter][0]}{$increment[tagSplitCounter][1]}{/}
{/}{/}{/}
```

This example makes use of this filter to split the tag based on colon (:) as the separator. It also uses functions as a way to instruct the logic to skip over the first iteration of the loop (tag1) and then print everything after that (value).

#### New Filter: SplitRegExp

You can use the splitRegExp function to split data based on a Regular Expression separator - and return a list of items.

For example, if you wanted to only display the 1st paragraph of the vulnerability description, you could do the following:

```
{#vulnerabilities}
{title}
{$declare[SplitDecriptionOnParagraph][1]}
{#description | splitRegExp:[“\n\n“]}
{#$lessThanOrEqual[SplitDecriptionOnParagraph][1]}{.}{/}{$increment[SplitDecriptionOnParagraph][1]}
{/}{/}
```

#### New Filter: Trim

You can use this new filter trim the whitespace before and after a tag as follows:

```
{#title | trim}
```

#### Updates to Filters: Includes & Excludes

We have updated the Includes and Excludes filters to include support for Scope and Variables.

Includes Scope Example:

```
{#projectCustomFields}
{#customer_name}
{#projectGroups}
{#name | includes:[“%(../customer_name)”]}
Customer {customer_name} has access to this project.
{/}{/}{/}{/}
```

Includes Variables Example:

```
{$declare[CustomerName][“”]}
{#projectCustomFields}
{#customer_name}
{$assign[CustomerName][“%(customer_name)”]}
{/}{/}
{#projectGroups}
{#name | includes:[“$(CustomerName)”]}
Customer {$value[CustomerName]} has access to this project.
{/}{/}
```

#### New Function: $includes

You can use the new $includes function to check if a value exists or does not exist (excludes) within a variable.

To check if data exists:

*{#$includes\[variable]\[value]}{/}*

To check if data does not exist (excludes):

*{^$includes\[variable]\[value]}{/}*

The following example creates a unique list of affected asset names, then prints the list.

```
{$declare[UniqueAssets][[]]}
{#vulnerabilities}
{#affected_assets}
{^$includes[UniqueAssets]["%(asset)"]}
{$push[UniqueAssets]["%(asset)"]}
{/}{/}{/}
{#$value[UniqueAssets]}
{.}
{/}
```

#### New Function: $append

You can use the new $append function to append data to an existing variable.

*{$append\[variable]\[value]}*

Example below will create a new variable, then append the vulnerability title to it.

```
{$declare[VulnTitle]["Title: "]}
{#vulnerabilities[0]}
{$append[VulnTitle]["%(./)"]}
{/}
{$value[VulnTitle]}
```

#### New Function: $sort

You can use the new $sort function to sort the data within a variable.

To observe the variables available for sorting - use the $help function.

Example 1: Sort A List

```
{#$sort[UniqueAffectedEndpoints][":asc"]}{/}
```

Example 1: Single-Key Sort

```
{#$sort[UniqueAffectedEndpoints]["af_sys_affected_endpoint:desc"]}{/}
```

Example 2: Multi-Key Sort

```
{#$sort[UniqueOWASPTop10Vulns]["priority:asc","title:desc"]}{/}
```

### New Functionality

#### Customers/Engineers can set vulnerabilities to Not Ready for Retesting

Sometimes vulnerabilities are assigned for retesting, only to discover that engineers have jumped the gun and further adjustments might be needed before they are retested.

Customers/Engineers can now reassign vulnerabilities as *Not Ready for Retesting* in such cases.

<figure><img src="/files/GyZd2jnCnL2KDySYQAeQ" alt=""><figcaption></figcaption></figure>

#### Customers/Engineers can bulk assign vulnerabilities to Ready for Retest & Not Ready for Retest

Assignment of vulnerabilities as *Ready for Retesting* and *Not Ready for Retesting* can now be applied in bulk by customers.

<figure><img src="/files/OQ7IZj933wVug4dFdpME" alt=""><figcaption></figcaption></figure>

#### Hide Conditions now supported on all Project Request system fields

The project request form is an integral part of the pentest project lifecycle. It is the important first step of the process, where customers can request a new project or assessment.

We have now made it possible to add hide conditions against the system fields.&#x20;

This means you can extend the logic of your project request form to customise when and how the system fields are displayed during a new project request.&#x20;

This provides more personalisation and better user experience for your customers.

#### Ability to disable every Project Request system field

Following on from the enhancement above, we have taken this one step further to now support ability to disable all system fields in project request form, if desired.

This allows you to create a fully custom project request form, tailored to your needs, without any implicit fields for your customers to complete.

<figure><img src="/files/lYIDaLP735TveBESoxbt" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/7bppf0zZlgHuSAasU4so" alt=""><figcaption></figcaption></figure>

### Updates to Self-Service API

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

#### New REST endpoint: CloneProject

This endpoint can be used to clone an existing project. This is an effective way to:

* Prepare for a new round of testing
* Track vulnerabilities for specific assets across projects
* Focus retesting on open vulnerabilities

When cloning a project, the new project will get access to:

* Project settings, which can be adjusted for the new project - this includes name, codes, test suites, scope, email templates, portfolios, custom fields & project team
* Project workspace, included all notes & files previously uploaded / created (OPTIONAL)
* Project notes previously created (excluding private notes) (OPTIONAL)
* Executive summary, including uploaded files (OPTIONAL)

You can also select which vulnerabilities (if any) you would like to carry forward into the new project. This is useful for performing a retest on existing vulnerabilities, as part of the new round of testing.

For full details on how to use this endpoint, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/cloneproject>

#### Advanced Query Filtering Support&#x20;

We have added support for advanced query filtering for REST endpoints: GetVulnerabilities, GetProjectVulnerabilities, GetVulnerabilitiesByAssetName, GetVulnerabilitiesByGroup

Advanced query filtering is used to select the exact data set you would like the API to return. The filter works like a database query, where you can specify fields & operators - these help to narrow down the results to the data you would need. This filter is only supported for selected API endpoints. Please check the documentation for each endpoint for more details.

For example, you can use this filter to return:

Critical or High vulnerabilities only:

```
curl -G -X GET 'https://YOURAFETENANT/api/ss/vulnerabilities' --data-urlencode 'q={ priority: { $in: ["Critical", "High" ] } }' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'
```

Open Critical or Open High vulnerabilities only:

```
curl -G -X GET 'https://YOURAFETENANT/api/ss/vulnerabilities' --data-urlencode 'q={ $and: [ { priority: { $in: [ "Critical", "High" ] } }, { status: { $eq: "Open" } } ] }' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'
```

Critical or High Ready for Retest vulnerabilities only:

```
curl -G -X GET 'https://YOURAFETENANT/api/ss/vulnerabilities' --data-urlencode 'q={ $and: [ { priority: { $in: [ "Critical", "High" ] } }, { is_retest: { $eq: "Yes" } } ] }' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'
```

Critical or High vulnerabilities discovered in past 24 hours:

```
curl -G -X GET 'https://YOURAFETENANT/api/ss/vulnerabilities' --data-urlencode 'q={ $and: [ { priority: { $in: [ "Critical", "High" ] } }, { status: { $eq: "Open" } }, { created: { $gte: datetime("now", "-1 days") } } ] }' -H 'Host: localhost:3000' -H 'X-SSAPI-KEY: ***' -H 'Content-Type: application/json' -H 'Connection: close'

```

The query filter supports the following operators:

* And
* Or
* Equals
* Not Equals
* In
* Not In
* Greater Than
* Greater Than or Equals
* Less Than
* Less Than or Equals
* Regular Expression

The query filter also supports datetime function, which allows you to modify the time and date to suit your query requirements.&#x20;

For example, you could ask it to show you all vulnerabilities in past 24 hours or past 7 days; or filter all vulnerabilities with SLA expiring in next 24 hours or next 7 days.

For more information on how advanced query filtering works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/advanced-query-filter>

#### Query projects by asset(s) tested/in-scope&#x20;

We have added support for querying projects by specific asset(s) which were in-scope for testing, for REST endpoint: GetProjects

This is useful if you need to perform analysis on which projects a given asset was tested, or if it has not yet been tested.

The filter supports:

* exact name match
* partial name match
* case sensitive match
* case insensitive match
* single asset
* multiple assets
* any combination of the above

For more information, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojects>


# 2022

## 2022-10-10

### **Introducing Delegations**

This release introduces an exciting new feature – delegations.

Delegations provide delegates with the ability to perform specific workflows and functions, which are usually considered privileged tasks in AttackForge.

Admins can grant delegations to individual users, or globally against user roles.

Delegations can help to:

* Reduce the burden placed on admins and privileged users.
* Empower trusted individuals with autonomy to perform more tasks in AttackForge.

In this release, we have included the following delegations:

* **Create Projects**: User(s) can create new projects; edit their projects; and manage access to their projects.&#x20;
* **Action Pending Project Requests**: User(s) can view, edit, approve, reject, and request information for all pending project requests.

Individual user delegations can be granted and managed via the Users module.

<figure><img src="/files/PWj0EOBP7GqmzKCp4eC4" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/lRxGhyQUBKqc4DJvORbH" alt=""><figcaption></figcaption></figure>

In addition, global delegations can be applied via the Administration module.

<figure><img src="/files/O7GBIVhRS95kifa3UGgv" alt=""><figcaption></figcaption></figure>

### **Smart Vulnerability Imports**

Importing vulnerabilities can be a pain… especially when you need to adjust them before they can go to customers.

We have just released a new smart mapping feature – providing greater control when importing your vulnerabilities.

You can now consolidate vulnerabilities into single write-ups!

This powerful utility helps you to:

* Combine vulnerabilities into a single unique writeup
* Ensure your imported vulnerabilities are matched against known good and customer-ready write-ups&#x20;
* Speed up your quality assurance and review process
* Reduce the amount of duplicate and similar entries in your vulnerability libraries

For example, say you have three (3) vulnerabilities your wish to import from Nessus:

* SSL Version 2 and 3 Protocol Detection
* TLS Version 1.0 Protocol Detection
* TLS Version 1.1 Protocol Detection

However, you want to map these against one (1) single known-good writeup in your library - which covers all various TLS related configuration issues:

* Weak TLS Implementation

You can now do this easily with a single custom mapping expression!

<figure><img src="/files/Djj71QX3rIOpYjrWJkVS" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/0PzFGWjh5Wrok3x8br3Y" alt=""><figcaption></figcaption></figure>

An example of a smart mapping expression (rule):

**`vuln.tags.pluginID $in writeup.tags.pluginID`**

This expression works as follows:

* For every imported vulnerability (**vuln**), do the following
  * For every writeup in the library (**writeup**), do the following
    * Does the vulnerability have a tag 'pluginID' (**vuln.tags.pluginId**)?
    * Does the writeup have a tag 'pluginID' (**writeup.tags.pluginID**)?
    * Is one in the other? (vuln.tags.pluginID **$in** writeup.tags.pluginID)
    * If yes, map this imported vulnerability to the writeup in the library
    * Otherwise, proceed to import vulnerability as per normal

Smart mapping can take advantage of:

* up to **twelve (12) datapoints** on the vulnerability; and&#x20;
* up to **ten (10) datapoints** on the vulnerability library writeup

Every expression utilizes operators, which can be combined to build more powerful expressions!

* NOT or ! - used to negate an expression.
* AND or && - used to *and* multiple expressions.
* OR or || - used to *or* multiple expressions.
* \== - used to check for equivalency.
* \=== - used to check for equality.
* != or !== - used to check for not equivalency.
* \> - used to check for greater-than comparison.
* < - used to check for less-than comparison.
* \>= - used to check for greater-than-or-equals comparison.
* <= - used to check for less-than-or-equals comparison.
* ( ) or !() - used to group statements together.
* $in - used to check if a value is in a list of values.
* $nin - used to check if a value is not in a list of values.
* \=\~ /RegEx/i - used to check for a regular expression.

For more details on how smart mapping works, please check the following link on our Support centre:

<https://support.attackforge.com/attackforge-enterprise/getting-started/creating-vulnerabilities#custom-import-mapping>

### **ReportGen v2.4 Released**

We have just released version 2.4 for AttackForge ReportGen!

<figure><img src="/files/WyyfSNwbByqHlwecJhzK" alt=""><figcaption></figcaption></figure>

#### **Updates to Filters**

This release introduces an update to **filterBy** to include:

* filterBy:'AffectedAssetProperties'&#x20;
* filterBy:'AffectedAssetCustomFields', and&#x20;
* filterBy:'AffectedAssetCustomFields-CountVulns'

This filter is used to retrieve a set of vulnerabilities where the affected assets meet certain conditions.

For filterBy:'AffectedAssetCustomFields' and filterBy:'AffectedAssetCustomFields-CountVulns' - these filters are used in the exact same way as filterBy:'AffectedAssetCustomTags', however will filter vulnerabilities by their custom fields instead of by their custom tags.

For filterBy:'AffectedAssetProperties' - this filter works on other properties associated with the affected assets, such as CVSS scores, priorities, status, and resolution reason. For example, you can return a list of vulnerabilities and their affected assets which are closed, as follows:

<figure><img src="/files/vBHg2OeBd6clwtOqV9C5" alt=""><figcaption></figcaption></figure>

Or you can extend the filter to match multiple AND or OR conditions. For example, you can return a list of vulnerabilities and their affected assets which are either open or ready for retest.

<figure><img src="/files/K0ESUcWfl7vYk8m56gcN" alt=""><figcaption></figcaption></figure>

This filter works with any *key:value* pair on *affected\_assets*. If you are unsure which properties you can use this filter on, try using the Helper function to see which fields are available to you.

![](/files/apehZ0nBjrW8MZ61qIJp)

#### **Performance boost!**

We have introduced a new compression engine for AttackForge Core and Enterprise users which improves report generation by up to 70%! This is particularly noticeable on large reports with lots of images.

#### **UX improvements**

You now no longer need to include {#individualReport} tag in your AttackForge Core and Enterprise templates.

#### **Bug fixes**

We have fixed few different bugs which relate to rendering of lists and line breaks in the reports.

### **New Functionality**

We have added new functionality to make AttackForge even better for you and your teams and customers!

#### **Approve project request with project clone**

You can now approve a project request with a clone.

When selecting **Approve Request & Clone Project**, you can set up the new project based on parameters and vulnerabilities from a previous project.&#x20;

This is ideal if the request is for a new round of testing for a previously tested application, system or set of assets.

<figure><img src="/files/RXo3hLpgML1BO9wiGpGT" alt=""><figcaption></figcaption></figure>

#### **Project cloning options**

When cloning a project, you can now select from different cloning options available such as:

* Clone Executive Summary? Yes/No
* Clone Project Notes? Yes/No
* Clone Project Workspace? Yes/No

#### **Template Proof of Concept / Steps to Reproduce**

You can now configure a template steps to reproduce / proof of concept that will be automatically copied to the POC field when creating a new vulnerability.

<figure><img src="/files/PvPW25fC4Xn8FngiZBWU" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/lWX1EKeaohhwOzsObend" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/yEvFwt6zJIKl6xzfuqWl" alt=""><figcaption></figcaption></figure>

#### **Bulk-add remediation notes**

You can now bulk apply remediation notes to selected vulnerabilities.

<figure><img src="/files/Nlo2yOwRUIRs34degz5z" alt=""><figcaption></figcaption></figure>

#### **Custom JIRA export issue mapping**

You can now set additional JIRA export options for mapping vulnerabilities to JIRA tickets:

* **Custom Issue Type** - you can export your vulnerabilities to your JIRA project using a custom issue type (i.e. not Bug, Story or Task).
* **Custom Priority (Critical / High / Medium / Low / Info)** - you can export your vulnerabilities to your JIRA project using a custom mapping for issue rating. For example, default mapping in JIRA for Critical is Highest. However, your project may be configured to use another value which is not Highest. Here you can enter that value to map the vulnerabilities accordingly.

#### **Custom sign-in page text**

You can now set a custom sign-in page message. This could be used for welcome messages or disclaimers.&#x20;

You can set your custom sign-in message from Administration --> Configuration --> Users tab.

<figure><img src="/files/5EaLCK15KFddozqaCBTw" alt=""><figcaption></figcaption></figure>

#### **Disable Attack Scenario field**

You can now disable the Attack Scenario field, so that it is no longer require when creating or editing a write-up in the library.

We have also made this field optional if you choose to keep to enabled.

#### **Exclude groups filter in analytics**

You can now exclude groups from your analytics.&#x20;

This is useful if you want to perform analysis such as *‘show me analytics for all projects and vulnerabilities which aren’t related to this group (or groups)’*

#### **Configure default role for new registrations**

You can now configure the default role which is assigned to newly registered users, or users automatically created via Single-Sign-On Just-In-Time user provisioning.

You can set your default role from Administration **-->** Configuration --> Users tab.

### **UX Improvements**

#### **Custom projects table in Portfolios**

We have now added the ability to modify your table settings (such as columns, visibility, pagination, order, etc.) for projects when viewing Portfolios and Streams.

#### **User schedule improvements**

We have added more data for users when viewing the schedule and related projects.

All projects are now color-coded, you can easily track the status of each project assigned to the user.

We have also included more information for each project, including Role which helps you to better understand and filter the users’ role on each project.

<figure><img src="/files/rqAPauXOPL5hyujGKnwX" alt=""><figcaption></figcaption></figure>

#### **Global pending vulnerabilities**

The global dashboard now includes Pending vulnerabilities, making it easy to track vulnerabilities which require attention or quality review.

#### **Custom ‘Select’ fields now support search**

All ‘Select’ type custom fields now support search, making it easy to select an entry from a large list.

### **Updates to Self-Service API**

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

We have created the following new APIs:

* New REST method: *UploadTestcaseFile*
  * This method can be used to upload a file to a test case on a project.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadtestcasefile>
* New REST method: *UpdateUser*
  * This method can be used to update a users’ details.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updateuser>

We have also made the following updates to existing APIs:&#x20;

* All vulnerability related endpoints – we now include *altCustomFields* as an alternative format for returned custom tags and custom fields
* All project related endpoints – we have added *project\_extended\_status* and *project\_testing\_progress* fields to returned project data
* CreateTestsuite, AddTestcaseToTestsuite, UpdateTestsuite, UpdateTestcaseOnTestsuite – we have added *code* & *sort\_order* fields
* CreateGroup, UpdateGroup – we have added *linked\_groups\_view*, *linked\_groups\_upload* and *linked\_groups\_edit* fields
* CreateProject, UpdateProject – we have added *portfolio\_streams* field

## **2022-07-18**

### Time-based Custom Email Notifications

This release introduces an exciting new feature – time-based custom email notifications.

AttackForge now has a powerful utility for generating custom-emails based on our new rules-based engine.

This utility allows you to craft & send custom emails on a daily or weekly recurring cycle.

Custom emails allow you to create your own workflows for reminders, escalations, or reporting.

Examples of when you might use this feature include:

* Setting automated reminders for vulnerabilities which are nearing or overdue on their SLAs or Remediation Plans
* Setting automated summary emails of vulnerabilities over defined periods, for example Critical Vulnerabilities in Past 72 Hours
* Setting automated notifications for vulnerabilities based on their custom tags or custom fields, for example Vulnerabilities Ready for QA in Past 24 Hours

These are just few examples of what custom emails can bring to your organisation.&#x20;

Custom emails extend the robust notifications that already come standard with AttackForge workflows.&#x20;

Administrators can configure custom emails from the Administration --> Configuration --> Custom Emails tab.

![](/files/ulW8pbUYvzLrvSZrgyEW)

Every custom email can have its own unique set of configuration options, including:

* **Key** - this is used to reference this custom email rule.
* **Email Frequency** - this is where you define the repeating frequency for this custom email.
* **Email Time** - this is where you define which hour of the day you would like this custom email to be sent.
* **Type** - this is where you can configure the type of email you would like to send, and the data you will have access to in your email for each recipient.
* **Filter** - this is where you can filter the data from the Type based on your unique requirements for this custom email.
* **Recipients** - this is where you configure the audience for this custom email.
* **Subject** - this is the email subject for the custom email.
* **Body** - this is the email body for the custom email.

Custom time-based emails can be configured to be sent on a daily or weekly basis, and at any time during the day.&#x20;

For example, you can configure a custom email to be sent between 8AM and 9AM each day to ensure recipients have the information at the start of their day; or between 5PM and 6PM so that they have a summary from that day.

AttackForge supports **twelve (12) filters & functions** you can use to filter the data set that is relevant to your custom email.

AttackForge also supports **sixteen (16) fields for vulnerabilities** – helping you filter to the right data set that is relevant to your custom email.

The recipients of your custom emails can include **twenty (20) different audiences** – helping to ensure the right people are informed, every time.

Every recipient will receive a personalized vulnerability list based on vulnerabilities & projects for which they have access to.

The data which can be inserted into your custom emails is extensive, with **over sixty (60) metatags** currently supported.

Below is an example of a custom email that shows each recipient a personalized list of vulnerabilities which are 7-days from reaching their SLA:

![](/files/OxJKaGXFPROYsediukvp)

More information including examples can be found on <https://support.attackforge.com/attackforge-enterprise/getting-started/custom-emails>

### Test Case Workspaces & Execution Flows

Test cases now have their own page, complete with:

* dedicated workspace to capture evidence & notes for each test case; and&#x20;
* execution flows and steps which help to guide a tester through the process of how to perform the test case.&#x20;

![](/files/JdkT3zpWfNVLxni0itKY)

#### Dedicated Workspaces

![](/files/bfs1k3dL4bFqAzbBM2Ts)

Every test case has its own dedicated workspace, where testers can document information and upload supporting files relating to the testing process.

Workspace notes are a great way to:

* store evidence for how the test case was performed;
* capture notes and observations during testing;
* record information relating to particular tested assets; and
* document conversations and events relevant to the test case.

Every workspace note has ability to upload supporting files.

Workspace notes are only visible to the testers on the project, where as the pre-existing Notes & Evidence features can still be used for customer & report facing notes and evidence.

#### Execution Flows

Execution flows can be assigned to each test case, and can have many uses such as:&#x20;

* Document steps and procedures guiding a person in how to perform the test case;
* Document which tools should be used to perform the test case;
* Document internal processes and procedures required by the test case; and&#x20;
* Provide links to external resources.

![](/files/d4ya0SEWucI62TZvN3wB)

Execution flows are made up of ‘steps’ which can be defined for each test case within the Test Suite Builder module:

![ ](/files/D5dvt2rK5T8gPWoFNQLT)

### ReportGen v2.3 Released

We have just released version 2.3 for AttackForge ReportGen!&#x20;

#### Pentest Report Template v2

In this major update, we have introduced Pentest Report Template v2.&#x20;

![](/files/rUB7eL3aUlGAVs49hbjR)

This new template demonstrates the sophistication and power we have been building into ReportGen over the past 18 months, showcasing the possibilities available in ReportGen v2+.

This template contains the following enhancements:

* Redesigned Executive Summary - new dual-column layout + extra tags + styled executive summary notes
* Redesigned Testing Summary - new layout + extra tags for overview of testing progress
* New Section 'Summary Findings' - color-coded tables with overview of all vulnerabilities
* Custom AttackChain Images - use your own images in your attack chains. New placeholders are included
* Redesigned Vulnerability Details - new dual-column layout + color-coded vulnerability headings + styled POCs with center-aligned images and italicized captions
* Whitespace Reductions - reduced whitespace to make reports more practical and concise
* Redesigned Test Cases - new dual-column layout + color-coded section headings
* New Section 'OWASP Top 10 Mapping' - demonstrates power of Functions to create custom dynamic sections within your reports
* Updated Vulnerability-to-Asset & Asset-to-Vulnerability Mappings - color-coded for easy consumption of data
* Updated Table of Contents
* {#projectCustomTags} & {#assetCustomTags} - utilizes custom tagging to display new data in the report
* New fonts & headings
* DateFormat() filter - filter has been applied to dates & times

You can download this new template from Templates section inside ReportGen.&#x20;

We have also released an updated example JSON test data which can be used for creating templates.

#### Custom JSON Data Now Supported

You can now use ReportGen with custom JSON data and files!

ReportGen now supports the {data} tag which provides access to the top-level array or object in your JSON file.

This tag provides direct access to the entire JSON file - providing support for custom data which is not included in a standard AF JSON project export file.

For example, if you had the following JSON file:

![](/files/24Ni817je62PmPCyQ8S1)

You can print this data in your custom report as follows:

![](/files/qQv0JhxFqbY5uOmTZnYQ)

![](/files/56FwEsiwk2PKzQgId7cd)

You can access many of the pre-built Functions and Filters to add powerful logic and formatting to your custom data.

#### New Style: AF Images

We have added support for a new style AF Images which can be used to create a custom style for images and their captions inserted via the {...\_styled} tags.

This new style provides ability to have custom formatting for how your images and captions are displayed in your reports, for example in your executive summary or steps to reproduce / proof of concepts.

To get started, create a new style inside Word with the name 'AF Images'. Then apply a format to this style.

When ReportGen builds your report, it will automatically map to this style for you.

#### Project Vulnerability Links

You can now include links to your project and individual vulnerabilities in your reports.

This is useful to give the recipients of your report a link they can click in the report to then be directed to the project or a certain vulnerability.

### New Functionality

#### Portfolio custom fields

We have now added the ability to define custom fields for your portfolios; as well as ability to enable/disable the standard portfolio fields.

Administrators can modify the portfolio field settings from Administration --> Configuration --> Custom Fields – Portfolios.

#### Active Directory integration – options for Upload & Edit groups

#### Allow groups to receive project notifications

When linking Active Directory groups to AttackForge groups, you can now specify which level of privileges will be assigned to the group member.

This is useful if you have Active Directory groups for engineering or security teams and would like to automatically assign Upload and Edit permissions for the user to the related AttackForge group’s projects.

Groups can also now receive project email communications. This can be enabled when creating or editing the group settings.

![](/files/V8nGbcdEo58rZmPSYuyk)

#### Manually re-apply and delete vulnerability SLAs

Vulnerability SLAs can now be automatically or manually enabled per project.

Usually, SLAs will be automatically applied to any new vulnerabilities created or imported on your projects.

However, you can opt-out of applying SLAs automatically, and instead apply them manually on selected vulnerabilities.&#x20;

This is useful if you want SLAs to applied only under certain conditions, for example:

* Apply SLAs only at the end of the project;
* Apply SLAs only when the application team acknowledges the findings;
* Apply SLAs only on certain projects, for example compliance/regulatory projects;
* Apply SLAs only on certain vulnerabilities that require an SLA.

To apply SLAs manually on your project, select Manual option:

![](/files/1Q1gPbUU5FIq9in6ioPs)

You can bulk re-apply SLAs on vulnerabilities. This will remove the existing SLA on the vulnerability and replace it with a new SLA from the SLA ruleset.

If no SLA exists on the vulnerability, a new SLA will be applied.&#x20;

![](/files/j6pRei2Zw5VS2vLMrWTu)

This can also be performed on an individual vulnerability:

![](/files/0xOwmuXDwzLNzJ2fzH4h)

You can remove SLAs for vulnerabilities.

![](/files/06IHmGkVSNCBCNXfm5Eb)

This can also be performed on an individual vulnerability:

![](/files/EEAVQu6azTX1H5TlT546)

#### View asset vulnerabilities in Assets module

Every user can now view vulnerabilities for assets (which they have access to) via the Assets module.

The vulnerabilities can be viewed by clicking on the name of the asset within the Assets module.

#### Resend welcome invitation email

If a user does not receive the initial welcome invitation email when they are invited to join your AttackForge tenant, you can now resend the welcome email from the Users module using the Actions menu.

### UX Improvements

#### More data columns in tables

#### Customize tables – projects, assets, vulnerabilities & library

We have enhanced the number of columns which are now available in many of the data tables, particularly relating to vulnerabilities.

This provides access to more data which can be used for filtering vulnerabilities, or as part of the CSV table export.

We have also combined this with new options to configure your tables to set your preferences relating to:

* Default page size
* Default column to sort on&#x20;
* Default column sort order
* Toggle columns which are displayed
* Toggle column position/order in which they are displayed

![](/files/Tly3Bg3sWkIncM2FSOtS)

#### Project custom fields show in Tracking page

When viewing the project tracking & information page, we are now displaying project custom fields.

This can be useful to share more information with your project team about the given project.

#### Remove disabled buttons – reports, exports, collaboration

We have removed options and buttons from view when they are not explicitly enabled within configuration.

This helps to focus attention of users to configured options only.&#x20;

### Updates to Self-Service API

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

We have created the following new APIs:

* **New REST method: GetPortfolio**

This method can be used to retrieve information for a specific portfolio.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getportfolio>

* **New REST method: GetPortfolios**

This method can be used to retrieve information for all portfolios.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getportfolios>

* **New REST method: GetPortfolioStream**

This method can be used to retrieve information for a specific stream on a portfolio.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getportfoliostream>

* **New REST method: GetVulnerabilityRevisionHistory**

This method can be used to retrieve revision history for a specific vulnerability.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getvulnerabilityrevisionhistory>

## 2022-05-16

### Project Clone

This release introduces a powerful new workflow – *Project Clone*.

Project Clone can help you to:&#x20;

* **Reduce the amount of manual effort** when preparing for a new round of testing
* **Track unique vulnerabilities** more easily across projects
* **Focus retests** on specific vulnerabilities, as new projects

When cloning a project, the new project will get access to:

* Project settings, which can be adjusted for the new project - this includes name, codes, test suites, scope, email templates, portfolios, custom fields & project team
* Project workspace, including all notes & files previously uploaded / created
* Project notes previously created (excluding private notes)
* Executive summary, including uploaded files
* Vulnerabilities (if any) you would like to carry forward into the new project

When carrying vulnerabilities forward into the new cloned project, the new project will have access to exactly the same vulnerabilities – that means vulnerability status, remediation notes, revision history, any changes to the vulnerability will remain intact.&#x20;

* ***Vulnerabilities are not transferred**.* Vulnerabilities will become available in the new project and will also remain available in the source project.
* ***Vulnerabilities are not copied**.* This means there will be no duplication of vulnerabilities.
* ***Vulnerabilities are universal**.* Any changes to these vulnerabilities in source project, will also apply to the new project, and vice-versa.

*This important design element* ensures that your vulnerability dashboards, analytics and vulnerability management activities remains true, no matter how many cloned projects.

To start, you can select *Clone Existing Project* from project module menu; or select *Clone Project* from projects action menu.

![](/files/0J4JsPKC2oxPys34FeEo)

Next, select the project from list which you would like to clone.

![](/files/N75W82vtOaZOdxJx3muU)

&#x20;Review & adjust settings for new project.

![](/files/aPfvdrFb7roM005QKD7Q)

Select any vulnerabilities from previous project you would like to bring into this new project.

![](/files/RTGimh9g3Qx6XOrFe0xj)

Once you are done, simply click *Create Project* and your new cloned project will be ready.

![](/files/oECc3KB6hepnrg728hkL)

### ReportGen v2.2 Released

We have just released version 2.2 for AttackForge ReportGen!&#x20;

In this major update, we have introduced **Parent objects.**&#x20;

ReportGen now automatically includes the parents for each object in your JSON project/reporting file.&#x20;

This means you can traverse up or down anywhere in the report, to access the right data you need.

For example, say you were looping through each vulnerability and you wanted to print the project name as well as the vulnerability title - you could do the following:

`{#vulnerabilities}`\
`{parent.projectName} – {title}`\
`{/}`

Now instead if you are looping through affected assets and you want to print the project name + vulnerability title + affected asset name - you could do the following:

`{#vulnerabilities}`\
`{#affected_assets}`\
`{parent.parent.projectName} – {parent.title} – {asset}`\
`{/}{/}`

If you are unsure of what data or parents are available to you at anywhere in your report, you can use help function:

`{#vulnerabilities}`\
`{#affected_assets}`\
`{$help["%()"]}`\
`{/}{/}`

This will print a help section in your browser console when you try to run the report, which will detail all data you can access, including any parents, at that time and section within your template.

This release also introduces a **new filter called 'filter'.**&#x20;

You can use this filter to select objects within a list that match a particular condition.

For example, if you wanted to filter your vulnerabilities by critical AND easily exploitable you could use the following:

`{#vulnerabilities | filter:’easily_exploitable === true AND priority === “Critical”’}`\
`{title}`\
`{/}`

Another example is filtering affected assets based on remediation status AND priority. Note this example applies the filter to the {#affected\_assets} and utilises "parent" to access the priority from the vulnerability.

`{#vulnerabilities}`\
`{#affected_assets | filter:’remediation_status === "Open" AND parent.priority === “Critical”’}`\
`{title}`\
`{/}{/}`

The following operators are supported when using this filter:

* **NOT or !** - used to negate an expression. For example !(priority == "Critical")
* **AND or &&** - used to and multiple expressions. For example priority == "Critical" AND zero\_day == true
* **OR or ||** - used to or multiple expressions. For example priority == "Critical" OR priority == "High"
* **==** - used to check for equivalency. For example priority == "Critical"
* **===** - used to check for equality. For example priority === "Critical"
* **!==** - used to check for not equivalency. For example priority !== "Critical
* **>** - used to check for greater-than comparison. For example likelihood\_of\_exploitation > 5
* **<** - used to check for less-than comparison. For example likelihood\_of\_exploitation < 5
* **>=** - used to check for greater-than-or-equals comparison. For example likelihood\_of\_exploitation >= 5
* **<=** - used to check for less-than-or-equals comparison. For example likelihood\_of\_exploitation <= 5
* **( )** - used to group statements together. For example (priority == "Critical") AND (zero\_day == true) OR ((priority == "Critical") AND (likelihood\_of\_exploitation >= 8))

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters>

This release also introduces support for a **new tag {@execSummaryNotesStyled}** which can be used to display styled executive summary with images.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-tags>

![](/files/iSAMK7T6S5cdIZy3ebpl)

### New Functionality

* **View executive summary in-application**
* **Review notes now available in executive summary**
* **Executive summary gets rich-text support**

Project team members can now view the executive summary within the UI, without having to download the reports.

Review notes have also been added to the executive summary, making QA faster and easier!

Executive summary now includes support for rich-text which means you can style your executive summary and render it styled in your custom reports.

![](/files/a6he8r9VybtHufIH4L9n)

* **Inactivity account lockout policy**

Admins can now configure a global inactivity account lockout policy for non-admin accounts.

This policy can be used to prevent users from signing in if they have exceeded the policy, for example have not logged into the application for at least 6 months.

When a user is blocked due to inactivity, the *Users* module will indicate this within the Status column.&#x20;

Admins can re-activate login for the affected user by selecting *Allow sign-in* from the user actions menu.&#x20;

After the user signs in, they will automatically fall under the inactivity policy going forward.

To configure the global inactivity account lockout policy, go to *Administration --> Configuration --> Security* and set *Disable Inactive Non-Admin Users?* To YES.

Select the number of days the policy should apply (1-365 days). Save your configuration.

![](/files/3SHFttN5UxnnI3Rn4I4D)

* **User account expiration**

Admins can now expire users. Once a user is expired, they will no longer be able to log into the application or use the Self-Service API.

This feature is great for contractors, external partners or temporary service accounts used for integrations.

When a user is expired, the *User*s module will indicate this within the Status column.

Admins can re-enable a user by adjusting their expiration date.

To configure user account expiration, go to *Users* module and using the actions menu, select *Set Expiry Date* for the user.

* **Project team notifications now includes vulnerability ready for retest, re-opened & closed events**

When setting up a new project, or editing an existing project, you can now select the following options under *Email Project Team on Following Events*:

* *Vulnerability Ready for Retesting*
  * Will send an email to notify that the vulnerability has been marked as ready for retesting
* *Vulnerability Re-opened*
  * Will send an email to notify that the vulnerability has been re-opened
* *Vulnerability Closed*
  * Will send an email to notify that the vulnerability has been closed

These notifications can be configured for individual project team members, when setting or updating their project team access records.

Users can also set their preferences for these notifications via *Notifications* module.

These notifications can also be forced via project settings.

![](/files/sW57JQTBYWp9JEuWkBPY)

* **Import vulnerabilities as pending or visible**
* **Add custom tags prior to importing vulnerabilities**

When importing vulnerabilities on your project, you can now set them as *visible* (everyone on project team can see them) or *pending* (only edit team members aka pentesters can see them).

You can also set custom tags prior to importing the vulnerabilities. This can help to save time by tagging the vulnerabilities immediately, so they are ready for custom reports.

![](/files/DgxNxfoHMFMJjFpZtIZX)

* **Asset fields now available in daily start/stop testing emails**

We have added ability to reference asset details when sending daily start/stop testing emails.

![](/files/NSuNLzLXjQxcjaUtPiHH)

For more information please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects#custom-email-notifications-on-daily-start-and-stop-testing>

* **Project CSV export has more vulnerability & asset fields**

We have updated the CSV export for projects to now include the following fields:

* Vulnerability ID
* Vulnerability Alternate ID
* Vulnerability Title
* Vulnerability Status
* Vulnerability Status Updated
* Affected Asset Name
* Affected Asset ID
* Affected Asset Library ID
* Affected Asset Library External ID
* Likelihood of Exploitation
* Zero-day
* Retest
* Description
* Attack Scenario
* Recommendation
* Notes
* Steps to Reproduce
* Tags
* Custom Tags
* Custom Fields
* CVSSv3 Vector
* CVSSv3 Base Score
* CVSSv3 Temporal Score
* CVSSv3 Environmental Score
* Remediation Notes
* SLA
* Release Date
* Target Remediation Date
* Created
* Created By
* Modified

### UX Improvements

* **Redesigned vulnerability page, including rendering images**

We have redesigned the vulnerability page to provide easier access to critical information, as well display images within steps to reproduce and notes.

![](/files/zNNEHfjBh4sEdVNqn3iX)

* **New vulnerability table columns – tags & custom tags**

We have now added tags and custom tags columns when viewing vulnerabilities on a project.

This makes it easier to track and filter vulnerabilities, particularly for reporting or integration purposes.

![](/files/xXSbktGA0DcuSF0HjpJ6)

Also as a reminder, you can update your project vulnerabilities table settings by clicking on the blue cog.

![](/files/z0yHh3G8omjXuQszLL7q)

Here you can configure which table columns are shown and in which order, as well as default pagination, sort column, sort order, and others.

* **Approved project request automatically assigns files to new project workspace**

Now when you approve a project request, any uploaded files as part of that request will automatically be uploaded to the projects’ workspace.

This reduces manual effort of transferring files, and ensures all information uploaded by the customer is available on the project and for the pentesters.

* **Project Edit users can now see all team members on project team + group access to project**

Project team members with edit access to the project can now view entire project team, including users with inherited access via groups.

This makes it easier for pentesters to know exactly which persons have access to the project, if they need to collaborate with them.

* **Users module now shows last active via app and last active via Self-Service API for all users**

When viewing users in the Users module, it now shows when they were last active via the application and also the Self-Service API.

This helps to monitor user session durations, and activities against the Self-Service API.

This can also be used for troubleshooting purposes.

### New Configuration Options

* **Pick new UI theme colors**
* **Upload new logos for UI and reports**

Admins can now configure new UI theme colors for the default standard theme for all users, as well as adjust the logos used for login page, in-app and on reports.

This provides greater flexibility and freedom to personalize your AttackForge interface.

These settings can be configured from *Administration --> Configuration --> Miscellaneous*

![](/files/PzdEntq6iXHS3MF0Ys2K)

* **New project custom field type: table**

Admins can now configure a new type of custom field for projects – *Table.*

This field type can be used to capture complex data, such as multiple records of data with different types of fields per record.

The table field displays ability to define columns, where the user can then create rows of data against these columns.

When creating a table field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Label** - This is the label that will be displayed in the form for this table.
* **Required** - This is used to determine whether the table is mandatory or optional in the forms.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See Hide Conditions for more details.

You can then add columns by clicking Add Column Field . Each column has the following options:

* **Type** - Input field, Text Area, Select, Multi-Select or Datepicker
* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Value / Selected Options** - depending on the Type, this will allow you to specify default selected options/value for this field.
* **Label** - This is the label that will be displayed in the form for this table.
* **Required** - This is used to determine whether the table is mandatory or optional in the forms.

![](/files/ASEgMTioBmaKddxx8cRn)

The form will present all of the columns (fields) for the user to enter, and ability to add rows.

![](/files/XFnXA2I6WRiKOIT3bSVW)

* **Set max date option for SLAs**

In the last release we introduced vulnerability SLAs to help improve vulnerability management and reduce risks.

In this release we have added ability to set a max date option for each SLA.

For example, you may have an SLA rule for *Critical Vulnerability in Cardholder Data Environment*.

You may also have an internal company policy that *all critical vulnerabilities in CDE must be fixed within 10 days or no later than Q1 of the year*.

Now you can define that policy when setting or modifying your SLAs:

![](/files/PSDVSgigqplc3a9jVw67)

* **Disable new user admin emails + welcome email**

Admins can now disable the email which is sent to admins when a new user is registered, invited or created.

This option can be toggled from *Administration --> Configuration --> Emails*

* **Disable CSV / JSON / ReportGen custom reports**

Admins can now disable ability to download CSV, JSON or ReportGen custom reports for either client users or all users.

This can be used to control which types of reports or exports your users are allowed to access, and is an extension of previous ability to disable PDF, DOCX and HTML reports.

This option can be toggled from *Administration --> Configuration --> Reporting*

### Updates to Self-Service API

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

We have created the following new APIs:

* **New REST method: CreateUsers**

This can be used to create bulk users. It is useful when pre-registering users in AttackForge.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/createusers>

* **New REST method: UploadWorkspaceFile**

This can be used to upload files to a projects’ workspace. It is useful when setting up a new project or pentest as part of integrations with tools such as ServiceNow or JIRA.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadworkspacefile>

* **New REST method: InviteUsersToProjectTeam**

This can be used to bulk invite users to a project. It is useful when setting up integrations with tools such as ServiceNow or JIRA.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/inviteuserstoprojectteam>

* **New REST method: RemoveProjectTeamMembers**

This can be used to bulk remove members from a project team. It is useful when removing access to projects programmatically.

For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/removeprojectteammembers>

## 2022-03-14

### Custom Vulnerability SLAs and Remediation Plans

Vulnerability SLAs are a powerful way to triage vulnerabilities to make vulnerability management more effective and efficient.

AttackForge helps to keep on top of vulnerabilities as they get closer to their SLA, by making it easy to filter, identify, action and export.&#x20;

Every vulnerability can be automatically assigned with a custom SLA.

AttackForge provides a rules-based engine to configure custom vulnerability SLAs. This powerful utility allows you to create SLAs which meet specific conditions based on vulnerability, asset and project datapoints.

Every SLA is color-coded and includes a countdown tracker for easy filtering and sorting.

Vulnerability SLAs can be enabled by Administrators via Administration module.

![](/files/0v0i67Lx1HWEu6nCsTnf)

AttackForge SLA rules-engine is a powerful utility to configure SLAs going beyond typical “Critical/High” ratings.&#x20;

For example, you can create rules for vulnerabilities on “Internet-facing” assets, or vulnerabilities within scope of compliance such as PCI-DSS.

AttackForge SLA rules-engine supports over fifty (50) datapoints across vulnerabilities, assets & projects – greatly improving vulnerability compliance tracking and triaging to reduce risk.

AttackForge SLA rules-engine also supports over ten (10) different operators, allowing you to link together various datapoints in logical ways to create custom rules.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/vulnerability-slas>

![](/files/v7GKLvHPQwLiOgGcqzRi)

We have also introduced a new workflow to capture target remediation dates for vulnerabilities – Remediation Plans.

Remediation plans can be submitted by your customers, developers, engineers, and teams.

Remediation plans help to track when vulnerabilities are planned to be fixed, to help security team keep on top of open vulnerabilities.

Every remediation plan includes a countdown tracker to make it easy to identify and action vulnerabilities which are getting close to, or have already exceeded, their remediation plan dates.

Administrators can enable this functionality via Administration module.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/retesting>

![](/files/fvG8lR0HJ9gH9ILRgx4m)

![](/files/FHaXhzDK0EPd1UZ6aDXc)

### ReportGen v2.1 Released

We have just released version 2.1 for AttackForge ReportGen!&#x20;

In this major update, we have introduced ReportGen Functions.&#x20;

You can now use functions in your templates to program custom logic and create sophisticated and powerful reports.

This release introduces twelve (12) new functions - seven (7) Procedures and five (5) Operators.&#x20;

You can access Functions in the ReportGen tool menu.

![](/files/9foa9YkBPvYQhgfDjMzk)

#### Procedures

* **$declare** - use this procedure to declare a variable, which can then be used in other procedures and operators.
* **$push** - use this procedure to add data to a variable list/array.
* **$increment** - use this procedure to increment a variable with a numeric value.
* **$decrement** - use this procedure to decrease a variable with a numeric value.
* **$assign** - use this procedure to assign a new value for a variable.
* **$value** - use this procedure to retrieve the value for a variable.
* **$help** - use this procedure to print diagnostic information to your ReportGen browser console, to help you with using procedures and operators in your template.

#### Operators

* **$equals** - use this operator to perform an equality comparison for a variable against a value.
* **$lessThan** - use this operator to perform a 'less than' comparison for a variable against a number.
* **$lessThanOrEqual** - use this operator to perform a 'less than or equal' comparison for a variable against a number.
* **$greaterThan** - use this operator to perform a 'greater than' comparison for a variable against a number.
* **$greaterThanOrEqual** - use this operator to perform a 'greater than or equal' comparison for a variable against a number.

To help you with creating your custom templates, you can now use the $help function to print diagnostic information to your ReportGen browser console.

This helps you to access that correct data you need in your report, and to help you with understanding what scope (tags/keys) is available.

Insert this function wherever you need help in your template, and it will provide you with information inside the ReportGen tool.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions>

![](/files/ECWhuYIrMDPHEUr1MQ4w)

Version 2.1 also includes two (2) new Filters to provide greater flexibility when accessing your tags and data – dateFormat and sort.

* **dateFormat**
  * This filter can be used to convert a date to a desired format. It uses named formats and masks to allow for custom date formats.
  * For example, this filter can be used to produce timestamps in user-friendly formats such as Saturday, June 9, 2007.
* **sort**
  * This filter can be used to sort the data within a tag by a key within scope (or multiple keys).
  * For example, this filter can be used to sort vulnerabilities by their CVSS score within the report.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters>

Version 2.1 also now includes support for styled project notes using the in-app WYSIWYG editor.

In your template, add the following tags:

`{#projectNotes}`**`{@notes_styled}`**`{/}`

Version 2.1 also includes new {#asset\_details} tag which includes following details for project scope:

* **{id}** - id for the asset on the project
* **{name}** - name of the asset on the project
* **{library\_id}** - id for the asset in the library
* **{library\_created}** - created timestamp for asset in the library
* **{library\_updated}** - updated timestamp for asset in the library
* **{library\_name}** - name of the asset in the library
* **{type}** - type of asset
* **{external\_id}** - external id for the asset
* **{details}** - details for the asset
* **{#assetCustomFields}** - you can define & use custom fields in AttackForge.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-tags>&#x20;

### Asset Custom Fields

You can now configure custom fields for your assets.

Custom fields can be used to capture metadata for your assets.&#x20;

This data can be used within the portal, or within your reports or API integrations.

Custom fields support various types such as input fields, select fields, date-picker, etc.

Custom fields can be set to mandatory or optional within forms.

Custom fields can also be displayed in tables, for easy filtering and sorting.

![](/files/Thb9NQtFQpKzgmv9OOYC)

![](/files/D3lO7YtssFyP4QS3mpIz)

### New Functionality

* **New retest round functionalities**

Every round of retesting now has a dedicated scope of work, as well as date when retest will be ready to commence.

Security teams can now independently action each round of retesting, determining when it has been completed and which vulnerabilities were retested.

We have also added option to cancel a retest.

For more information please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/retesting#request-a-retest>

![](/files/JtdICUPIAl9LOS9RdNM6)

* **Custom one-off email notifications to project team**

Security teams can now send custom one-off email notifications to project team members.

![](/files/nVCnCXd3k7jnkjZQVqjl)

* **New notification type for new vulnerability emails**

When a new vulnerability is discovered – you can now send one (1) single email with the details for each new vulnerability.

You can still send an individual email per vulnerability, if desired.

This new email notification type supports tags to allow you to customize the content for each vulnerability to your requirements.

For more details please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects#custom-group-email-notifications-on-new-vulnerabilities>&#x20;

![](/files/mb8gBwn6RHGagHjcZQBB)

### UX Improvements

* **Project Upload users get new dashboard buttons**

We have added easy-to-access buttons for project “upload” users to access the workspace and notes on project dashboard.

![](/files/xBoD4zQtp0EDzQj20hGf)

* **Prevent session timeout when creating/updating project notes**

We have made adjustments to avoid sessions timeout when creating or updating project notes.

* **Project Edit users can update project custom fields**

Project “Edit” users can now update project custom fields when editing the project.

### New Configuration Options

AttackForge now supports multiple SSO / identity providers.

Each SSO login button can be relabelled according to preferences.

![](/files/pqIdARIthNN2PqDOBPci)

### Updates to Self-Service API

In this release, we have improved our Self-Service REST APIs to provide more flexibility and options when interacting with AttackForge.

We have created the following new APIs:

* **New REST method:&#x20;*****GetProjectsAndVulnerabilities***
  * This can be used to retrieve a list of projects and their vulnerabilities. It is useful when exporting to other systems or generating custom analytics & reports.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectsandvulnerabilities>
* **New REST method:&#x20;*****GetUserByEmail***
  * This can be used to retrieve a user by a given email address.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getuserbyemail>
* **New REST method:&#x20;*****GetUserByUsername***
  * This can be used to retrieve a user by a given username.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getuserbyusername>
* **New REST method:&#x20;*****ArchiveProject***
  * This can be used to archive a given project.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/archiveproject>
* **New REST method:&#x20;*****RestoreProject***
  * This can be used to restore a given project.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/restoreproject>

We have also updated the following APIs:

* **REST&#x20;*****CreateTestsuite*****&#x20;to include following new fields:**
  * id
  * created
  * name
  * description
  * tags
* **REST&#x20;*****CreateGroup*****&#x20;to include following new fields:**
  * id
  * created
  * last\_modified
  * name
  * group\_owner
  * primary\_contact\_email
  * primary\_contact\_name
  * primary\_contact\_number
* **Updates to REST methods:&#x20;*****CreateAssetInLibrary, GetAssetInLibrary, GetAssetsInLibrary, UpdateAssetInLibrary*****&#x20;to include following new fields:**
  * custom\_fields
* **Updates to REST methods:&#x20;*****GetVulnerabilities, GetProjectVulnerabilities, GetVulnerabilitiesByAssetName, GetVulnerabilitiesByGroup, GetVulnerabilityById*****&#x20;to include following new fields:**
  * vulnerability\_affected\_asset\_custom\_fields

## 2022-01-17

### Custom Vulnerability Libraries

Admins can now configure custom vulnerability libraries and apply access controls to those libraries.

Custom vulnerability libraries can be used to:

* Create libraries for different security teams, for example pentest teams
* Create libraries for different vendors & external teams
* Create libraries for individual users
* Create DRAFT & QA libraries for workflows
* Create libraries for security testing activities, for example Web App, Red Team, Network Infrastructure
* Create libraries for specific platforms or technologies, for example Windows, Linux, REST API, etc.

Custom vulnerability libraries can be accessed when:

* Creating or updating vulnerabilities on a project
* Importing vulnerabilities on a project
* Creating or updating write-ups in the Vulnerability Library module
* Using the self-service API

![](/files/lixAJeQTSWk5dp84einA)

![](/files/ey5CwlT6jxp5M684DMUe)

Custom vulnerability libraries are configured in the Administration module.

Admins can also configure access controls for the system Imported Vulnerabilities and Project Vulnerabilities libraries.

![](/files/LhfKtMWWxibsTUZXj9gw)

### ReportGen v2 Released

We have just released version 2.0 for AttackForge ReportGen!&#x20;

In this major update, we have redeveloped the internal reporting engine to improve performance and work more efficiently with very large reports (many thousands of pages).

We have also redesigned the standalone tool to include a new user interface, with better support for accessing training materials, templates, troubleshooting as well as improved error handling for testing your templates.

![](/files/xXnEFrzJIqzKPgziwJPm)

The following new features have also been added to this version:

#### **1. WYSIWYG-styled Proof of Concepts (POCs) / Steps to Reproduce**

You can now style your POCs / Steps to Reproduce using the in-app WYSIWYG editor.

In your template, replace the following tags:&#x20;

`{#proof_of_concept}{text}{%inlineScreenshot}{/proof_of_concept}`

With the following:&#x20;

`{@proof_of_concept_styled}`

You can also set default styles in your template which will apply when report is generated.&#x20;

Following styles are supported:

* **AF Heading 1** - style for WYSIWYG editor Heading 1
* **AF Heading 2** - style for WYSIWYG editor Heading 2
* **AF Heading 3** - style for WYSIWYG editor Heading 3
* **AF Code Snippet** - style for WYSIWYG editor PRE

![](/files/8PflI9K1dOdmkWxeTnAM)

#### **2. New filters: hasAny & hasAll**

***hasAny***

You can use this filter to check whether at least one value exists in a tag. This can be used to create logic gates or sections within your reports.

Example: `{#tags | hasAny:['CVE','cve']}Associated CVEs...{/}`

***hasAll***

You can use this filter to check whether all supplied values exist in a tag. This can be used to create logic gates or sections within your reports.

Example: `{#tags | hasAll:['OWASP Top 10','CWE Top 25']}This vuln is OWASP Top 10 & CWE Top 25...{/}`

#### **3. Tags and CVSS scores now on every affected asset**

Vulnerability tags and CVSS scores can now be referenced on every affected asset {#affected\_asset} using the following ReportGen tags:

* {cvssv3\_vector}
* {cvssv3\_base\_score}
* {cvssv3\_temporal\_score}
* {cvssv3\_environmental\_score}
* {#tags}

#### **4. Access full details for assets equally affected**

You can now access all the tags included in {#affected\_asset} for assets which are equally affected, using a new tag {#assets\_equally\_affected\_full\_details}...{/}

### Tracking Affected Endpoints

You can now track & remediate vulnerabilities at the affected endpoint level i.e. port number, http parameter, etc.&#x20;

This is in addition to the affected asset which is already available.

Affected endpoints can help you to:

* Track and remediate every vulnerable port number on a host or IP address
* Track and remediate every vulnerable HTTP parameter for a web application or API
* Track and remediate every vulnerable line of code for a file
* Identify and track known vulnerable components within all assets

![](/files/sfTuzT8R9LsbNwZXMiMK)

![](/files/ru2h83dI6dJGhQp8V2Ri)

&#x20;Affected endpoints can be referenced directly in your custom reports:

![](/files/ZOUACAwW2vCtkqXi1x8K)

Affected endpoints are also automatically captured during import of vulnerabilities from Nessus and will be extended to other tools shortly.

![](/files/TsQu0PdojEzYupmJwDro)

This new functionality can be enabled by your Admins via the Configuration module.

For a step-by-step guide on how to enable this functionality and capture this information for each asset – please check following support article:

<https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#sys-field-affected-endpoint>

### New Functionality

#### \[ENT + CORE] New tags & sections for daily status update emails

We have released new tags which can be referenced in your daily status update emails:

* **{projectStartDate}** - start date for the project.
* **{projectEndDate}** - end date for the project.
* **{key.\<custom\_field>}** - you can access your project custom fields using {key.\<custom\_field>} where \<custom\_field> is the key for your custom field. For example, if you had a custom field 'Out of Scope' and it had a key 'out\_of\_scope' - you can use {key.out\_of\_scope} to print the value of the custom field in this email.
* **{scope}** - this is the scope on the project. It is presented as an unordered list.
* **{link}** - hyperlink to view the project in AttackForge
* **{startTesting}...{/startTesting}** - information which will only be sent in email's when testing has started.
* **{stopTesting}...{/stopTesting}** - information which will only be sent in email's when testing has stopped.
* **{totalVulnsToday}** - total number of vulnerabilities that were discovered today.
* **{totalCriticalVulnsToday}** - total number of critical vulnerabilities that were discovered today.
* **{totalHighVulnsToday}** - total number of high vulnerabilities that were discovered today.
* **{totalMediumVulnsToday}** - total number of medium vulnerabilities that were discovered today.
* **{totalLowVulnsToday}** - total number of low vulnerabilities that were discovered today.
* **{totalInfoVulnsToday}** - total number of informational vulnerabilities that were discovered today.
* **{totalActionedTestCasesToday}** - total number of actioned test cases today.
* **{totalFailedTestCasesToday}** - total number of failed test cases today.
* **{totalRemainingTestCases}** - total number of remaining test cases on project.
* **{totalRemainingTestCases%}** - percentage of remaining test cases on project.
* **{totalCompletedTestCases}** - total number of completed test cases on project.
* **{totalCompletedTestCases%}** - percentage of completed test cases on project.

We have included an example below of using the new tags & sections for customized start/stop testing notifications:

![](/files/k05TYzqfZOOjwsjPA0Dy)

![](/files/OvGXmu4edHyZkTIkabrF)

#### \[ENT + CORE] Update custom fields on projects and vulnerabilities<br>

You can now update or delete any arbitrary custom fields created on your projects and vulnerabilities.

These are custom fields which you may have imported from other tools or scripts.

#### \[ALL] Bulk update selected vulnerability fields

You can now update selected fields only for vulnerabilities.&#x20;

This makes it easier to perform bulk partial updates for example changing priorities or scores.

![](/files/fecPGoHYf4yvRBMGZ0ae)

#### \[ENT + CORE] Ordering Test Suites and Test cases

You can now set a display order for each of your test suites and test cases.&#x20;

This allows you to control the order in which test cases on a project are presented to users.

![](/files/fIQHtlimajGKUJ8QFJxO)

![](/files/el7EpwgKyiqNB5PkxXsM)

![](/files/TzWhnhvldhZ8jTVpzZnu)

#### \[ENT + CORE] Bulk delete test cases on test suites

You can now bulk delete test cases on test suites. This is useful and more efficient when duplicating existing test suites and making necessary changes.

To bulk delete test cases on a test suite, visit the test suite and select ‘Delete Multiple Test Cases’ from the page menu.

#### \[ENT+ CORE] CSV exports now include custom tags & fields

When exporting CSV for your projects, custom tags & fields are now included for each vulnerability.

#### \[ENT + CORE] Import custom fields when importing vulnerabilities

You can now include custom fields when importing vulnerabilities directly on your projects.

For example: `"vulnerability_custom_fields": [{"key":"af_sys_affected_endpoint", "value":"10.0.0.1:443"}]`

#### \[ENT + CORE] Delete review notes

Users can now delete their own review notes. Admins can delete all review notes.

### UX Improvements

#### Customise project vulnerabilities table

You can now customize your project vulnerabilities table to adjust the default options for:

* Results per page
* Default column to sort on
* Default column sort order
* Toggle visibility for each column
* Toggle position for each visible column

![](/files/l7TDqPzBt8apAboaRYKB)

![](/files/UYweZcQWmZWGcgdpCsiV)

#### Workflow improvements when creating new library vulnerability from project

When creating a write up in a library directly from a vulnerability on a project, after saving the writeup, you will be redirected back to the page with all your previous data restored.

It will also select the new writeup automatically, so you can continue creating/updating your vulnerability with improved efficiency.

#### Bulk select unique vulnerabilities

You can now bulk select from unique vulnerabilities view in your project.

First switch to unique vulnerabilities view, then use page menu to make selections for each vulnerability.

#### Test suites table gets new columns

When viewing your test suites in the Test Suite Builder module, the following new columns have been added:

* Total number of test cases assigned to the test suite
* Whether test suite is visible in project requests (service catalogue)
* Display order for projects test cases

#### Improved datatables sorting

We have made improvements to the sorting algorithms to provide better sorting, particularly on columns such as vulnerability alternative id.

#### Project coordinators can delete test & abuse cases on a project

Project coordinators can now delete test cases & abuse cases on a project.

#### All reports now include project name in filename

When downloading a project report in any format, JSON file or ZIP archive – the filename now includes the name of the project.

### New Configuration Options

Administrators can now access even more configuration options, to further personalize AttackForge to your requirements & preferences:&#x20;

* Set default ReportGen template – this will provide a default selected option when user attempts to download a custom report
* Set default executive summary – this will create a default executive summary on every new project&#x20;
* Set default portfolios on new projects – this will automatically select portfolios/streams when a new project is created/approved
* Configure visibility, sort index, placeholder & label for project request custom and system fields
* Configure visibility, placeholder & label for asset system fields
* Service catalogue single-select – this will switch the ‘Testing to be Performed’ field from multi-select to single-select
* Access system fields in hide conditions – system fields are now available in all hide conditions and can be referenced using \_\_\<system\_field\_key>
* Disable zero-day field – zero-day field can now be disabled when creating/updating vulnerabilities

### Updates to Self-Service API

In this release, we have improved our Self-Service REST & Events APIs to provide more flexibility and options when interacting with AttackForge.

We have created the following new APIs:

* **New REST method: GetProjectReport**
  * This can be used to download reporting data for a project, for example to download the JSON export programmatically. It is useful when exporting to other systems or generating custom reports.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getprojectreport>
* **New Events notification: ProjectRequestCreated**
  * This event is generated when a new project request has been created.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-request-created>
* **New Events notification: ProjectRequestUpdated**
  * This event is generated when a new project request has been updated.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-request-updated>

We have also updated the following APIs:

* **REST GetProjectById, GetProjects & GetProjectsByGroup** to include following new fields:
  * project\_team\_notifications
  * project\_admin\_notifications
  * project\_start\_stop\_testing\_email
  * project\_start\_stop\_testing\_email\_additional\_recipients
  * project\_new\_vulnerability\_email
  * project\_new\_vulnerability\_email\_additional\_recipients
  * project\_forced\_emails
  * project\_executive\_summary
  * project\_attack\_chains
  * project\_team
  * project\_hold\_history
  * project\_retesting\_history
  * project\_streams
* **REST GetProjectRequestById, GetProjectRequests** to include following new fields:
  * reason\_for\_testing
  * custom\_fields
* **REST CreateProject** to include following new fields:
  * organization\_code
  * vulnerability\_code
  * team\_notifications
  * admin\_notifications
  * start\_stop\_testing\_email
  * start\_stop\_testing\_email\_additional\_recipients
  * new\_vulnerability\_email
  * new\_vulnerability\_email\_additional\_recipients
  * forced\_emails
  * custom\_fields
* **REST CreateProjectRequest** to include following new fields:
  * reason\_for\_testing
  * custom\_fields
* **REST CreateVulnerability, CreateVulnerabilityBulk, CreateVulnerabilityWithLibrary** to include following new fields:
  * import\_to\_library – now includes custom libraries
  * custom\_fields
* **REST CreateVulnerabilityLibraryIssue** to include following new fields:
  * import\_to\_library – now includes custom libraries
  * custom\_tags
  * custom\_fields
* **REST UpdateProjectById** to include following new fields:
  * organization\_code
  * vulnerability\_code
  * team\_notifications
  * admin\_notifications
  * start\_stop\_testing\_email
  * start\_stop\_testing\_email\_additional\_recipients
  * new\_vulnerability\_email
  * new\_vulnerability\_email\_additional\_recipients
  * forced\_emails
  * executive\_summary
  * custom\_fields
* **REST UpdateProjectRequestById** to include following new fields:
  * reason\_for\_testing
  * custom\_fields
* **REST UpdateVulnerabilityById, UpdateVulnerabilityWithLibrary** to include following new fields:
  * custom\_tags
  * custom\_fields
* **REST UpdateVulnerabilityLibraryIssueById** to include following new fields:
  * import\_to\_library – now includes custom libraries
  * custom\_tags
  * custom\_fields


# 2021

## 2021-11-08

### **New Vulnerability Libraries**

Tracking & managing your vulnerability write-ups / templates can be difficult – particularly when you might have thousands of write-ups.

To make life easier for you and your pentest teams - AttackForge now supports new vulnerability libraries:

* **Main Vulnerability Library** - Centralized library for your approved vulnerability write-ups
* **Imported Vulnerabilities Library** - Centralized library for your write-ups from tools & scanners
* **Project Vulnerabilities Library** - Project-related write-ups

Main Vulnerabilities

* This is the primary source of your vulnerability write-ups.
* It contains 1300+ pre-loaded vulnerabilities that come with AttackForge. You can also add your own.
* This library is shared, which means any user on a project with permissions to create vulnerabilities for the project, will be able to use any of the vulnerabilities in this library.

Imported Vulnerabilities

* This is where you can find all of the vulnerabilities you have imported from various tools & scanners.&#x20;
* This library is shared, which means any user on a project with permissions to create vulnerabilities for the project, will be able to use any of the vulnerabilities in this library.

Project Vulnerabilities

* This is where you can access vulnerability write-ups that are designated to specific projects.
* This is useful if you have project-specific or sensitive data which you would like to segregate from the Main and Imported libraries which are shared libraries.

![](/files/1yTluIDwzlV0bDRmATX3)

Your pentest teams now have an option to select which library they would like to use when creating a new vulnerability on a project, or when importing vulnerabilities.

![](/files/pukNpC0ULUDC7KtJ4WVv)

![](/files/5pr6Z9GoEvbWOgMS4IAa)

Project Vulnerabilities are designated to specific projects.&#x20;

This is useful if you have project-specific or sensitive data which you would like to segregate from the Main and Imported libraries which are shared libraries.

Vulnerabilities in this library must be assigned/linked to at least one (1) project. They can be assigned to more than one project, if it would be relevant to do so.

Only users with access to the linked project(s) will be able to use these write-ups when creating a new vulnerability, and only on the linked projects.

Users with access to this library will only be able to view & modify vulnerability write-ups for which the user has access to at least one (1) of the linked projects.&#x20;

By default, Admins will be able to see all write-ups/templates in this library.

![](/files/ky7VkVr6xQDdcehHBRwp)

You can easily re-assign vulnerabilities between libraries using the page menu options in the Vulnerability Library module.

This can be used to retrospectively transition your imported write-ups into the Imported Vulnerabilities library.

![](/files/fztVCI7mpEQxBwrt9Ecm)

![](/files/YipSP65BVXJG7dzpK1U9)

![](/files/86qqlewvman6OigkcWkY)

### **Custom Forms & Fields**

AttackForge now supports ability to create custom fields & forms in the user interface.&#x20;

This can help to capture information which is relevant to your organization, customers & vulnerabilities.

Custom fields can be accessed in the application, in JSON exports and also via the Self-Service API.

You can create custom fields & forms for the following:

* Project Request
* Project Creation
* Vulnerability Library (write-up)
* Vulnerability (on project)

To set custom fields, you must be an Administrator. You can create & manage your custom fields from the Administration module.

You can set custom fields from the Vulnerabilities and Projects tabs inside Configuration.

![](/files/P1zZj8EqJGlkctEinoXM)

![](/files/u4e0qicdZt8qZ8aCFzyJ)

AttackForge supports the following custom field types:

* **Input field** - display a single-line input box within the relevant forms
* **Text Area** - displays a multi-line input box within the relevant forms. Text area can be resized by the user within the form if additional space is needed.
* **Select** - displays a drop-down menu with a single item select within the relevant forms.
* **Multi-Select** - displays a drop-down menu with a multi-item select within the relevant forms. User can select one or more options.
* **Datepicker** - displays a calendar where the user can select a single date.

For each field, you can set the following options:

* **Key** - This the name of the field (e.g. database field name).
* **Placeholder Value** - This is the default value that will be displayed in the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the tables.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met.

Hide Conditions can be used to add simple or complex logic into your forms.

For example, you can add logic to only display a field once a user has made a selection in a previous field.\
Or you can extend this logic to check for certain values which have been selected.

Hide Conditions fully support JavaScript methods and boolean logic.\
This means you can create highly customised forms which are suited to your needs.

![](/files/2Di8oj7kA2lAUTTJoywA)

You can also choose to display custom fields in relevant tables within the application.

![](/files/fRjFkmLIXgAfmeWOpFAi)

Every custom field is available to use in ReportGen as well as the Self-Service REST and Events APIs.

For detailed guidance on how to create and use custom fields, please check the following support article:\
<https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms>

You can now also disable existing fields for new project requests.\
Combining with custom fields, this means you can create a fully custom project request / intake form based on the questions you need from your customers.

Your admins can disable existing fields for new project requests via the Administration module.

![](/files/QdCOIhvNBkvy2e7au9qS)

### **New Functionality**

* **Manage access control for your groups’ projects in one pla**ce

You can now easily add & remove projects for a group via Groups module:

![](/files/MRqKy1sEdx5szuHEh28U)

* **New global dashboard summary boxes**

There are new global dashboard summary boxes:&#x20;

* Projects Overrun
* Projects Ready for Retest
* Open Critical Vulnerabilities
* Open High Vulnerabilities
* Open Medium Vulnerabilities
* Open Low Vulnerabilities

![](/files/cDNOCTvQYvTASuQ7kVhL)

* **Added Bulk Update  Closed  Fixed & Risk Accepted options for vulnerabilities**

![](/files/cgeZXyBngtql3eOodkcu)

* **Toggle unique vulnerabilities globally**

You can now toggle between Unique and All vulnerabilities across entire application.<br>

![](/files/TpRPFPSSQeeI4BQJIaR7)

* **Link a group to Portfolio**

When configuring your Portfolios and Streams - you can select a group or multiple groups and assign their projects to a Stream.

![](/files/pvbaga009sJmqazvVCOJ)

* **Set recipients on review notes**

![](/files/CTRY00RzvoqIaRvE675X)

* **Notifications on new user registrations & invitations**

Admins now receive email notifications every time a new user is registered or created in the application.

* **Project Coordinators can now filter schedule by users**

Project coordinators can now filter the schedule per user in the Schedule module.

* **Increased file upload limit & improved experience for low bandwidth uploads**

The file upload limit has been increased to allow for larger files to be uploaded.

Changes have also been made to allow for users with low bandwidth internet connections to upload large files without timeout.

### **UX Improvements**

* **Project percentage completion across all projects**

![](/files/BycLELxPTxhlF8P7gxKh)

* **Tables filter now shows at top**

Column filters within tables has been moved to the top row, to improve UX when filtering

* **Tables now support horizontal scroll**

Various tables in the application now support horizontal scrolling. This provides better experience viewing & filtering data in tables.

* **Project request status page update**

When viewing a project request, the latest status has been moved to the top of the page along with any supporting information.

### **Updates to ReportGen**

In this release, we have added new tags to ReportGen – providing even more datapoints you can access in your custom reports.

The following tags have been added:

* **{projectDuration}** – project duration in days i.e. difference between start & end dates
  * This tag as available at top-level
* **{#assets\_equally\_affected\_full\_details}** – contains all details for affected assets when using *RemoveDuplicatePOCs* option in your template.
  * This tag is available in in the following sections:
    * {#retestingHistory}
    * {#vulnerabilities}
    * {#criticalVulnerabilities}
    * {#highVulnerabilities}
    * {#mediumVulnerabilities}
    * {#lowVulnerabilities}
    * {#infoVulnerabilities}
    * {#completedTestcases}
    * {#inProgressTestcases}
    * {#notTestedTestcases}
    * {#notApplicableTestcases}
    * {#passedTestcases}
    * {#failedTestcases}
    * {#remediatedTestcases}
    * {#abuseCases}
    * {#assetVulnerabilityMapping}

We have also launched a new tutorial video which covers the basics on how to set up a new template, create tables, insert images, access custom fields, and more.

You can access the tutorial video from here: <https://support.attackforge.com/attackforge-enterprise/modules/reporting#available-tags-for-individual-reports>

### **Updates to Roles**

From this release, users with **Upload permissions to a project** can now also:

* Create notes on a project
  * This can be used to capture test credentials and other information directly as a note
  * Users will only be able to view & modify their own notes

Also users with **Edit permissions to a project** can now also:

* Update project test window
* Add additional email recipients for automated vulnerability emails (if enabled by Admins / Project Coordinators)
* Add additional email recipients for daily start/stop testing notifications

For an up-to-date list of all user-related permissions, please check Access Control Matrix on our support site: <https://support.attackforge.com/attackforge-enterprise/access-control-matrix>

### **Updates to Self-Service API**

In this release, we have improved our Self-Service REST & Events APIs to provide more flexibility and options when interacting with AttackForge.

We have created the following new APIs:

* New REST method: *UploadVulnerabilityEvidence*
  * This can be used to upload evidence files for a vulnerability. It is useful when importing vulnerabilities from external systems for each bug bounty systems.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/uploadvulnerabilityevidence>
* New Events notification: *ProjectCreated*
  * This event is generated when a new project has been created.
  * For details on how it works, please visit [https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-create](#2021-11-08)
* New Events notification: *ProjectUpdated*
  * This event is generated when a project has been updated.
  * For details on how it works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/project-updated>

We have also updated the following APIs:

* REST *GetProjectById, GetProjects & GetProjectsByGroup* to include following new fields:
  * project\_organization\_code
  * project\_vulnerability\_code
  * project\_scoring
  * project\_team\_notifications
  * project\_admin\_notifications
  * project\_total\_assets
  * project\_critical\_open\_vulnerabilities
  * project\_critical\_ready\_for\_retest\_vulnerabilities
  * project\_critical\_closed\_vulnerabilities
  * project\_high\_open\_vulnerabilities
  * project\_high\_ready\_for\_retest\_vulnerabilities
  * project\_high\_closed\_vulnerabilities
  * project\_medium\_open\_vulnerabilities
  * project\_medium\_ready\_for\_retest\_vulnerabilities
  * project\_medium\_closed\_vulnerabilities
  * project\_low\_open\_vulnerabilities
  * project\_low\_ready\_for\_retest\_vulnerabilities
  * project\_low\_closed\_vulnerabilities
  * project\_info\_open\_vulnerabilities
  * project\_info\_ready\_for\_retest\_vulnerabilities
  * project\_info\_closed\_vulnerabilities
  * project\_custom\_fields (new custom fields)
* REST *GetVulnerabilityById, GetVulnerabilities, GetVulnerabilitiesByAssetName, GetVulnerabilitiesByGroup* + EVENTS *VulnerabilityCreated, VulnerabilityUpdated* to include following new fields:
  * vulnerability\_custom\_fields (new custom fields)
  * vulnerability\_library\_custom\_fields (new custom fields)
  * vulnerability\_project\_custom\_fields (new custom fields)

## 2021-09-06&#x20;

### New Module – Portfolios

Portfolios help you to track & monitor the progress of your penetration testing programs.

Want to know how your internal systems compare to your external systems? Or wanting to track security posture for your applications or compliance requirements? Portfolios makes this easy!

Portfolios represent high-level grouping for segments within your pentesting program(s).

Every portfolio is made up of Work Streams (Streams) – a collection of pentests which focus on specific areas within your portfolio.

Portfolios and Streams can help you track Business-as-Usual (BAU) pentesting and better understand where to focus your time and resources more effectively.

Projects can be assigned to many streams and portfolios. This ensures you are tracking the right vulnerabilities, across your enterprise. See examples below:

![](/files/-MiQMSOHSaB7Hl0vFnD9)

![](/files/-MiQMV3I48koD5OTGxGW)

Portfolios can help you to answer the following questions. Check out our blog on Portfolios to read how.

* *What is the exposure of our Internet facing applications? How many critical vulnerabilities are currently open on these platforms?*
* *How can we be sure that each business division has pentested everything they need to have tested?*&#x20;
* *How are platforms fixing vulnerabilities? Is it done within the required timeframes agreed in our internal policies or set by external regulators?*
* *How do our applications compare between 1st quarter and 2nd quarter? Are we getting any better?*
* *How are different business divisions and platforms comparing against each other? Where are you going to focus resources for next period?*
* *Which external suppliers are lagging?*

Every Portfolio and Stream has a unique dashboard which includes details on vulnerabilities, projects & assets - helping you make more informed business decisions when it comes to tracking and remediation.

Using Portfolios, you can reduce the amount of time you spend reporting to your boards, executives, committees, and auditors!

![](/files/-MiQMlH_to3IBukRZu2y)

Portfolios is currently only available to Administrators on AttackForge Enterprise.

### New Self-Service API – Events

With this release, we are launching an entirely new Self-Service API – **Events API**.

Events API provides you with **real-time notifications on important events**, such as new vulnerabilities discovered or testing progress updates.&#x20;

Events API helps you to easily automate workflows. It’s perfect for customisations and integrations into your enterprise ecosystem.

For example, you want vulnerabilities to be raised in both ServiceNow & JIRA *immediately* when they are discovered, and emails to be sent to relevant teams so they can action it. This is now possible using the Events API!

Events API complements our existing RESTful API. You can combine both APIs to have seamless two-way integrations and workflows between AttackForge and your tools.

Events API allows you to:

* Receive real-time notifications on new vulnerabilities – automatically export them into your vulnerability management and/or ticketing systems.
* Update your applications with live testing & vulnerability feeds.
* Notified immediately when vulnerabilities are ready for retesting, closed or re-opened.
* Know exactly when changes are happening on your projects, for example when testing starts and stops.
* Receive audit logs for users in real-time.&#x20;

Every event contains the same level of details information you can find in our Self-Service RESTful API.

![](/files/-MiQN7oP3OzJDUrKCT1J)

Getting started with the Events API is a breeze and takes only minutes to set up.

We have done the hard work for you – you can access our production-ready example clients within AttackForge or directly from our [GitHub repository](https://github.com/orgs/AttackForge/repositories).

Our example clients are available in **NodeJS**, **Python**, **Java**, **.NET** and **Go** – providing flexibility for your engineering teams.

Getting started with any client is as simple as 1,2,3!&#x20;

1. Download the client from our GitHub repository
2. Install the dependencies (single command)
3. Run the client & start receiving events

If you’re interested in seeing a live demo of the Events API in action – reach out to us to schedule it in!

![](/files/-MiQNTrr1aFXZOMkORHx)

### New Workflow – Review Notes

In this release, we have launched a new workflow to help make QA easy for your vulnerabilities. Introducing **Review Notes**!

Your pentest team can now create & reply to Review Notes for each of your vulnerabilities, as they perform QA.

Email notifications are enabled to ensure that people are made aware when they need to action changes for a vulnerability.&#x20;

![](/files/-MiQNgcLkBTjQpqHTnc4)

To perform efficient reviews & QA, you can select multiple vulnerabilities that you wish to review, and then add review notes to each vulnerability one-by-one. Best of all - you can do all of this from just one screen!

![](/files/-MiQOFyHAzXvOpStSHxq)

![](/files/-MiQOJsTZoKaj22yw9F4)

![](/files/-MiQOMuf7xjcxgabg5lC)

Once you have finished reviewing all vulnerabilities, you will see the Next option is no longer available - meaning you have reached the end of the review.

### New ReportGen Template Library

AttackForge ReportGen is by far the easiest to use reporting tool available right now – and it’s made even easier with its “no code” design, allowing your teams to create new reporting templates quickly and with minimal knowledge/effort required.

In this release, we have launched a library of ReportGen templates that you can use to create powerful custom reports out-of-the-box.

Every template comes with an example end-result so you can see the finished product.&#x20;

The templates included in this release are:

* Asset Report
* Auditor / 3rd Party Report
* Critical & High Vulnerabilities Report
* Executive Report
* Internal & External Findings Report
* Pentest Report
* Retest Report
* Technical Report
* Testing Progress Report
* Web App & Infrastructure Report

Templates are provided in DOCX format. You can adjust each template to your desire/requirements, then upload back into AttackForge when ready to use on you projects.

You can also access sample project data files (JSON) to test your own templates with.

![](/files/-MiQOfU7ZelrA-MRnJwL)

### New Functionality

#### **\[ALL] Delete multiple scope/assets on project at once & keep assets which are linked to vulnerabilities**

* You can now bulk select many or all assets to delete on a project.&#x20;
* AttackForge will prevent deleting any assets with exiting vulnerabilities.

![](/files/-MiZyj-FlsHg4TZRAk5q)

![](/files/-MiZylhbTb9An9Go_Ar_)

![](/files/-MiZyp3huA2cLS7FvfMW)

#### **\[ENT + CORE] Export/Sync screenshots & evidence with JIRA**

* Every time you export or sync a vulnerability with JIRA, the evidence/screenshots/files are now also exported/synced to JIRA.
* This makes it easier for your engineering teams to access screenshots to help them reproduce & fix vulnerabilities fast!

![](/files/-Mi_-4ZTGTmdjuZihwM2)

#### **\[ALL] Upload Files to Executive Summary Section of Report**

* The Executive Summary now supports ability to upload files.
* This reduces the manual effort required to insert screenshots into your reports for the executive summary.

![](/files/-MiQUe_x70Lj2LEkU22o)

#### **\[ALL] Captions Now Available for Images in Reports**

* You can now add captions to each of your images in the report.
* Captions will be automatically applied in ReportGen and displayed under the images.

![](/files/-MiQVo4iGQbJ8UphzxMF)

#### \[ALL] Rename a retest round&#x20;

* You can now create custom names for each round of resting.

![](/files/-Mi_-QWGv5KfwqmHSs51)

#### \[ENT] See all vulnerabilities for an asset in the Assets Module

* Administrators can now see all vulnerabilities for a given asset in the Assets module.
* This makes is fast & easy to identify all known vulnerabilities for an asset.

![](/files/-Mi_-owF1qZ2JJS42lhi)

#### **\[ENT + CORE] Assign users to multiple test suites at project creation**

* You can now assign multiple test suites to users during project creation.
* This eliminates the manual effort of assigning users to multiple test suites.

![](/files/-Mi_0It4fZaJnkt52rFm)

### UX Improvements

This release is jam-packed with updates to the user experience, to make AttackForge experience even better for you and your customers.

#### **\[ALL] New project statuses: Overrun & Retest**

* Projects will now display a status of Overrun and Retest in the projects & various modules.&#x20;
* This helps to quickly identify projects which require immediate attention.
* *Overrun* status applies when a project has exceeded the test window, and the test cases have not yet been completed.
* *Retest* status applies when a project has all test cases completed, and at least one vulnerability is flagged as ready for retesting.

![](/files/-MiQPTgrSA_XUMm_Hj6F)

#### **\[ALL] Collapse vulnerabilities into unique vulnerabilities**

* You can now collapse vulnerabilities into unique vulnerabilities, and toggle back to individual vulnerabilities.
* This helps to determine how many types of vulnerabilities have been discovered.

![](/files/-MiQPfBTHojy_rbiu6HE)

![](/files/-MiQPtooR9Cxn0FyJe0A)

#### \[ALL] Project column has been added to Global Dashboard vulnerability tables

* You can now see the affected project when viewing vulnerabilities in the global dashboard.

#### \[ENT] Admins can set/override daily/weekly notifications for users

* To improve experience for your customers, Admins can now enable/update progress notifications on behalf of your users.
* Progress notifications provide daily/weekly breakdowns of projects, vulnerabilities and testing progress – essential for your busy project managers and platform leads.

#### \[ALL] Retesting rounds now show vulnerabilities which were not tested

* You can now see which vulnerabilities were not retested for a new retest round, in addition to the vulnerabilities which were retested.
* This can help to identify vulnerabilities which need further attention.

#### \[ENT + CORE] View & restore deleted users

* Admins can now view and restore users. All user data is preserved on restore.

#### \[ENT + CORE] Access Control Matrix now available in support centre

* We have published an [Access Control Matrix](https://support.attackforge.com/attackforge-enterprise/access-control-matrix) on our support site to help your teams with setting up the right levels of access for your users and projects.

#### \[ENT + CORE] Redesigned Reporting Module to make it easier to download custom reports

* We have redesigned the Reporting Module interface to make it easier to select multiple projects and to download custom reports, as well as access new template library and uploaded/available templates.

### New Configuration Options

In this release, we have introduced new global tenant configuration options, to help you personalize and improve your AttackForge experience even further.

You can start using these new options via the Configuration section in the Administration module.

* Vulnerabilities – Add Placeholder Steps to Reproduce/Proof of Concept for all new vulnerabilities
* Vulnerabilities – Add Placeholder Notes for all new vulnerabilities
* Users – Enable/Disable Local Authentication
* Users – Enable/Disable SSO Authentication

#### \[ALL] Updates to ReportGen

This release includes updates to ReportGen – to help you create tailored, custom on-demand reports to meet your reporting requirements, and to reduce the time wasted on manually adjusting reports.

The updates in this release include:

* *New Filter – FilterBy*
* *New Metatags*

For more information please visit [Reporting](https://support.attackforge.com/attackforge-enterprise/modules/reporting).

#### New Filter – FilterBy

You can use this filter in order to extract filtered data for vulnerabilities using various conditions.

This filter is useful if you are creating custom sections in your reports, for example a section for ‘Web App Vulnerabilities’ or ‘Infrastructure Vulnerabilities’.

Currently the following conditions are supported:&#x20;

* **filterBy:'AffectedAssetReportGenTags'**
  * This filter can be used to retrieve a list of vulnerabilities which have affected assets that meet conditions in their ReportGen tags.
* **filterBy:'AffectedAssetReportGenTags-CountVulns'**
  * This filter can be used to retrieve a count of vulnerabilities which have affected assets that meet conditions in their ReportGen tags.

#### New Metatags

We have introduced the following new tags & updates to existing tags:

* {**#retestingHistory**} --> {**retesting\_custom\_round\_name**} - custom round name (optional)
* {**#retestingHistory**} --> {**retesting\_custom\_status\_name**} - custom status name (optional)
* {**#retestingHistory**} --> {**#vulnerabilities**} – Forty-seven (x47) new tags for vulnerabilities retested on the retesting round
* {**#retestingHistory**} --> {**#vulnerabilitiesNotTested**} – Forty-seven (x47) new tags for vulnerabilities not retested on the retesting round

### Updates to Self-Service RESTful API

In this release we have made updates to the Self-Service RESTful API to improve the data points available to you for vulnerabilities and test cases.

* ***GetVulnerabilities, GetVulnerabilitiesByAssetName, GetVulnerabilitiesByGroup, GetVulnerabilityById & GetProjectVulnerabilitiesById*** received the following new fields:
  * vulnerability\_alternate\_id
  * vulnerability\_cvssv3\_vector
  * vulnerability\_cvssv3\_base\_score
  * vulnerability\_cvssv3\_temporal\_score
  * vulnerability\_cvssv3\_environmental\_score
  * vulnerability\_steps\_to\_reproduce\_HTML
  * vulnerability\_remediation\_notes
  * vulnerability\_project\_code
  * vulnerability\_project\_groups
  * vulnerability\_evidence
  * vulnerability\_custom\_fields
  * vulnerability\_library\_custom\_fields
  * vulnerability\_project\_custom\_fields
* ***GetProjectTestcasesById*** received the following new fields:
  * locked

### New Themes

Themes have been a popular feature for AttackForge, with now Fourteen (x14) themes supported!

In this release we have introduced **five new themes**: *Midnight Ocean, Predator, BumbleBee, Purple Panther & Nebula*

![MIDNIGHT OCEAN](/files/-MiQRUFiB2udacxeqcC_)

![PREDATOR](/files/-MiQRZobs9sdXLumBmEI)

![BUMBLEBEE](/files/-MiQRbtlzkWbQ8c-vE-b)

![PURPLE PANTHER](/files/-MiQReU5RRB5WSj0UdmH)

![NEBULA](/files/-MiQRhJ_e6d2MwxfQJlj)

![](/files/-MiQVo4iGQbJ8UphzxMF)

### Add Test Suites After Project Creation

In this release we have introduced the ability for AttackForge Pro users to add more test suites to a project after the project has been created.&#x20;

![](/files/-MiQSgG4QHo-EhMiTkyh)

![](/files/-MiQSpFrujEDDqkEeeDq)

### Invite Team Members to Project at Project Creation

In this release we have introduced the ability for AttackForge Free & Pro users to invite their connected team members to their projects, at time of project creation.

![](/files/-MiQTGjC_zXK0mdciXb0)

## 2021-07-12

### Major UX Uplift!

In this release we have a significant number of improvements we have made to AttackForge to enhance the experience for you and your users.

These improvements are a direct result of the feedback from our customers over the recent months, and includes the following:

#### 1. Invite Project Team & Assign Test Suites at Project Creation or Approval

You can now invite your entire project team during the project creation or approval process, and assign their roles, test suites & manage their notifications – in one easy step!

You can define the following for each project team member:

* **Access Level**
  * Set the access level for the user on the project. This can be either View, Upload & Edit.
* **Project Role**
  * Set the users' project role on the project e.g. pentester, customer, developer, etc.
* **Email Notifications**
  * Set the emails which the user will receive on the project.
* **Assign to Test Suite**
  * Assign the user to a test suite. The user will be assigned to each of the test cases loaded on the project for the nominated test suite.

![](/files/-M_dm4tyF9SWNDQ-sok8)

#### 2. User-Friendly Vulnerability ID

AttackForge now supports an alternative vulnerability code that is configurable and used to generate user-friendly unique vulnerability identifiers for all vulnerabilities on the project.

For example, if you set a vulnerability code as SEC01 - the first vulnerability created on the project will have an alternate user-friendly unique identifier of SEC01-1. The next vulnerability will be SEC02-2 and so on.

You can update the vulnerability code on a project at any time, so long as it's a unique value (has not been used on any other projects) and is between three (3) to eight (8) characters in length.

When you update a vulnerability code on a project - all of the existing IDs for any of the projects' vulnerabilities will also be updated to match.&#x20;

![](/files/-MawDQdXCdZM4X0WZ_-K)

![](/files/-MawGD0uz-5FDr_jj6Ij)

#### 3. Validate Project Code & Get Latest Project Code

You can now validate the project code to check whether an existing project exists using the same code.

You can also fetch the latest project code, to help with sequencing.

![](/files/-Me3Pn-WKGmyFBTZqMDQ)

#### 4. Manage Project Email Notifications

You can now set & control which email notifications a user will receive on a project.&#x20;

Project Team Notifications are intended to help keep you informed throughout the lifecycle of a project. For example, you can choose to be notified when testing has commenced or stopped daily, when new vulnerabilities are discovered, or when a project is on-hold - plus more.

To receive these notifications, you must be a member on a project team. Your administrators and project coordinators will invite you to the relevant project teams. In addition, project-level notifications must be enabled on the project. Your administrators and project coordinators will configure this for you, per project.

The Project Team Notifications include the following:

* **No Emails** - Under normal circumstances, you will not receive any email notifications for any projects you are a team member.
* **All Emails** - You will receive all enabled emails for all projects you are a team member.
* **Daily Start/Stop Testing** - You will receive notifications each time a team member starts or stops testing each day, where this option is enabled on the project.
* **New Critical Vulnerability** - You will receive notifications each time a team member discovers a new critical vulnerability, where this option is enabled on the project.
* **New High Vulnerability** - You will receive notifications each time a team member discovers a new high vulnerability, where this option is enabled on the project.
* **New Medium Vulnerability** - You will receive notifications each time a team member discovers a new medium vulnerability, where this option is enabled on the project.
* **New Low Vulnerability** - You will receive notifications each time a team member discovers a new low vulnerability, where this option is enabled on the project.
* **New Informational Vulnerability** - You will receive notifications each time a team member discovers a new informational vulnerability, where this option is enabled on the project.
* **Project Role Updated** - You will receive notifications each time your role on a project has been updated, where this option is enabled on the project.
* **Project On-Hold/Off-Hold** - You will receive notifications each time the project is placed on-hold or off-hold, where this option is enabled on the project.
* **Retest Completed** - You will receive notifications each time a round of retesting has been completed, where this option is enabled on the project.

![](/files/-Me3PvXwGsfAj0RXxZNX)

A user can choose to opt-out of project email notifications via Notifications module.

If you decide to disable certain types of emails, even when they are enabled for you on the project - you will not receive them. You ultimately control the project notifications you will receive.&#x20;

![](/files/-MaacKkATfb1TG_HGvfV)

However, under certain circumstances - an administrator or project coordinator may decide to force an email to be sent, for example new critical vulnerability that you should be aware of. Your administrators and project coordinators will configure any forced emails, per project.

![](/files/-Me3QMILNz4bpkVN6Pip)

We have also introduced new email notifications when a users’ role on a project is changed, and we also now include their role on the project invitation email.

#### 5. Friendly Links Now Included In All Email Notifications

We have introduced links in all emails which provide a friendly URL that will redirect the user to the relevant page on AttackForge, even if they are not yet logged in.

This helps to improve user experience by allowing the user to access a project, vulnerability, or event - with a single click!

This feature is also fully compatible with Single-Sign-On.

![](/files/-Me3QQolv-djYOMpmt8E)

#### 6. Configurable Welcome Invitation&#x20;

When creating a new user via Users module, the user will now receive a welcome email that can be configured via Administration module.

You can configure this email to contain a warm welcome message, or instructions on how to access the portal.&#x20;

The email supports full HTML.

![](/files/-Me3QVLCry_C0S4yaGp4)

#### 7. Project Team Displayed on All Projects

We have included extra column on Projects table to include project team.&#x20;

This helps to easily find & search who is on which projects.&#x20;

![](/files/-Me3QZZlZoOjUlB4PuEo)

#### 8. View Project Team and Group Access

We have included a new table which helps to separate which users are on a project team, and which users have access to the project via their group memberships.

This makes it easier to know who is actively involved with the project.

![](/files/-Me3QeG6OlZQd40ffjve)

#### 9. Resolution field for project vulns

We have included an additional column with the Resolution status when viewing project vulnerabilities.&#x20;

This helps to quickly determine whether a vulnerability has been resolved, and also the reason it was resolved – for example had been fixed, risk accepted, etc.

![](/files/-Me3QgwtXqTq09hAFAk9)

#### 10. Ready for Retest now prompts for note

When setting a vulnerability as Ready for Retesting, you can now add a remediation note at the same time – to help pentesters understand what fixes were put in place.

![](/files/-Me3QpGHiPnpkO37jPzJ)

#### 11. Other UX Improvements

We have also included the following UX improvements in this release:

* Admins can now Personalize Analytics for Other Users
* Performance Enhancements on downloading JSON Exports & Using ReportGen
* Major Bug Fixes in Various Parts of the Application, including when Creating/Editing Vulnerabilities in Projects & Library
* Upgraded Library to Handle Conversion of HTML-to-Text addressing number of Issues in Reports
* Pop-Up Warning Alert Now Included When Attempting Bulk Updates
* Improvements in Filtering & Comparison in Analytics

### In-App Customizations For Your AttackForge Experience

AttackForge provides a rich set of global tenant configuration options - allowing you to customize your workflows, features & user experience.

In this release, we have made these options available to you via the Administration module – allowing you to customize your tenant on-demand!&#x20;

You can personalize your email templates, change workflows, introduce or remove fields, set default values, configure your security settings – and much more!

The list of supported configuration options is regularly updated and can be found on our support site: <https://support.attackforge.com/attackforge-enterprise/configuration-options>

You can access the following Configuration modules from the Administration module in your AttackForge tenant:

* Emails
* Vulnerabilities
* Projects
* Reporting
* Modules
* Integrations
* Users
* Security
* Miscellaneous

![](/files/-Maw9H6ZFLDAxTKvqKSa)

### New Progress Notifications

In our previous May release, we introduced a new Notifications module to provide centralized & dashboard-style email notifications to keep your teams informed even whilst on the go.

In this release, we have extended this module to include Daily & Weekly Project Updates, as well as Daily & Weekly Admin Updates.

We have also included more information in these emails such as Projects Overrun, Projects Completed, and more detailed information for each project.

Every email notification is designed to provide important information relating to projects, vulnerabilities & user activity.

You can access Notifications via the global menu.

![](/files/-Maakz4l7yRvlwCJHJ4s)

![](/files/-Maal0A29zNengKurkPC)

### New Configuration Options

In this release, we have introduced new global tenant configuration options, to help you personalize and improve your AttackForge experience even further.

You can start using these new options via the Configuration section in the Administration module.

* **Projects** – New Organization Code field for Projects
* **Emails** – Create Custom Email Subject & Body for Invited Users
* **Vulnerabilities** – Enable/Disable Severity Field in Vulnerability Library
* **Vulnerabilities** – Enable/Disable Likelihood of Exploitation Field in Vulnerability Library
* **Vulnerabilities** – Enable/Disable CVSS Scoring Fields in Vulnerability Library
* **Modules** – Enable/Disable Project Request Workflow

### Updates to ReportGen

This release is action-packed with updates to ReportGen – to help you create tailored, custom on-demand reports to meet your reporting requirements, and to reduce the time wasted on manually adjusting reports.

The updates in this release include:

* **New Filter** – Store
* **New Filter** – FindVuln
* **New Reporting Option** - Remove Duplicate Proof-of-Concepts/Steps to Reproduce
* **New Reporting Option** - Remove Duplicate Evidence
* **New Metatags**

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting>

#### New Filter – Store

You can store custom data in arbitrarily defined tags using this filter.&#x20;

For example we can create a new custom tag called 'AllVulns' and reference it, along with its data, later in the template.&#x20;

This is useful if you are dynamically creating custom subsections/tables to reference in your report.

#### New Filter – FindVulns

You can use this filter to find a vulnerability based on a Title & Priority.&#x20;

#### New Reporting Option - Remove Duplicate Proof-of-Concepts/Steps to Reproduce

This option can be set at the beginning of your template in order to remove duplicate Proof-of-Concepts/Steps to Reproduce for vulnerabilities which have multiple affected assets and each affected asset has the same POC & Notes.

This option is useful to reduce duplicate entries where the POCs/Notes are the same, significantly reducing report size and making content more useful to the reader.

#### New Reporting Option - Remove Duplicate Evidence

This option can be set at the beginning of your template in order to remove duplicate Evidence for vulnerabilities which have already used/included the evidence within the Proof-of-Concept or Notes for any of affected assets, for example the screenshots have already appeared in-line within the Proof-of-Concept or Notes.

This option is useful to reduce duplicate evidence displaying, significantly reducing report size and making content more useful to the reader.

#### New Metatags

We have introduced the following new tags & updates to existing tags:

* {#affected\_asset} --> {alternate\_id} - user-friendly id associated with the vulnerability, set via project settings
* {#assetVulnerabilityMapping} - list of all assets on the project mapped to their vulnerabilities
  * {asset} - asset name
  * {#vulnerabilities} - list of all vulnerabilities the asset is affected by
    * {vulnerability} - vulnerability title
    * {priority} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
    * {status} - remediation status e.g. Fixed / Not Fixed
    * {#vulnerabilityDetails}
      * {#vulnerabilityCustomTags} - you can define & use custom tags/fields in ReportGen. For more details check out Creating Custom Fields within ReportGen Reports
      * {title} - title of the vulnerability
      * {priority} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
      * {remediation\_status} - either Open or Closed. Only Closed if all affected assets are also Closed.
      * {description} - description of the vulnerability&#x20;
      * {attack\_scenario} - attack scenario for the vulnerability
      * {remediation\_recommendation} - remediation recommendation for the vulnerability
      * {cvssv3\_vector} - includes the CVSS v3.1 vector string e.g. /AV/...
      * {cvssv3\_base\_score} - includes the CVSS v3.1 base score e.g. 10.0
      * {cvssv3\_temporal\_score} - includes the CVSS v3.1 temporal score e.g. 10.0
      * {cvssv3\_environmental\_score} - includes the CVSS v3.1 environmental score e.g. 10.0
      * {testcases} - list of all the linked test cases to the vulnerability
      * {#tags} - list of all tags
        * {.} - tag
      * {#affected\_asset} - details for the affected asset - see {#assetVulnerabilityMapping} - {asset}
        * {#assetCustomTags} - you can define & use custom tags/fields in ReportGen. For more details check out Creating Custom Fields within Individual Reports
        * {alternate\_id} - user-friendly id associated with the vulnerability, set via project settings
        * {asset} - asset name
        * {remediation\_status} - includes the remediation status of the vulnerability for the affected asset e.g. Open / Ready for Retest on \<DATE> / Closed on \<DATE>
        * {#remediation\_notes} - list of all remediation notes for this affected asset
          * {created} - date stamp when remediation note was created
          * {note} - remediation note details
        * {#notes} - list of all notes for this affected asse
          * {note} - note details
          * {%inlineScreenshot} - display inline images where they are included in the note
        * {#proof\_of\_concept} - details for proof of concept / steps to reproduce
          * {text} - proof of concept / steps to reproduce
          * {%inlineScreenshot} - display inline images where they are included in the note
          * {#proof\_of\_concept\_raw} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {#assets\_equally\_affected\_title} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
          * {#assets\_equally\_affected} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
            * {.} - asset name
      * {#affected\_assets} - list of all affected assets for this vulnerability
        * {#assetCustomTags} - you can define & use custom tags/fields in ReportGen. For more details check out Creating Custom Fields within Individual Reports
        * {asset} - asset name
        * {remediation\_status} - includes the remediation status of the vulnerability for the affected asset e.g. Open / Ready for Retest on \<DATE> / Closed on \<DATE>
        * {#remediation\_notes} - list of all remediation notes for this affected asset
          * {created} - date stamp when remediation note was created
          * {note} - remediation note details
        * {#notes} - list of all notes for this affected asset
          * {note} - note details
          * {%inlineScreenshot} - display inline images where they are included in the note
        * {#proof\_of\_concept} - details for proof of concept / steps to reproduce
          * {text} - proof of concept / steps to reproduce
          * {%inlineScreenshot} - display inline images where they are included in the note
          * {#proof\_of\_concept\_raw} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {#assets\_equally\_affected\_title} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {#assets\_equally\_affected} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {.} - asset name
      * {#evidence} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
        * {%fileBase64} - display image (if evidence type is of image format)
        * {fileName} - name of the file uploaded

## 2021-05-03

### Trend Analysis & Comparison Now Available in Analytics

You can now easily perform trend analysis by comparing key data such as projects, vulnerabilities, SLAs, etc. across periods of time & groups.&#x20;

This can help you to discover if you are getting better or worse. You can compare business units or customers over time.&#x20;

You can easily compare last year/month/quarter against this year/month/quarter using pre-defined filters.

Or you can select a custom date range for the time periods you want to compare.

You can also include Groups to track & compare how a business unit, supplier or team are performing over a given time period.

To compare Analytics, click on `Compare` button in the top-right of your Analytics dashboard.

![](/files/-MWfonkwhY5gRHJAvrLg)

![](/files/-MWfpowKTaqkZxrf3xe6)

![](/files/-MZHBrl_pDlOgao2CZ60)

### New Global Dashboard Notifications Module

You can now access global dashboard email notifications to keep you & your teams informed, even whilst on the go.&#x20;

Every email notification is designed to provide a dashboard summary of key information – for example projects, vulnerabilities, SLAs, group activity, user activity etc.

The first notification we have included in this release is the **Daily Admin Update**.

This email dashboard is intended to provide an overview of activities happening in AttackForge over the past 24 hours, and also to provide key information to help plan & prepare for the upcoming week.

The Daily Admin Update Email includes the following:

* Total number of Vulnerabilities discovered in past 24 hours, including Critical, High, Medium, Low & Info
* Total number of Vulnerabilities Closed in past 24 hours
* Total number of Vulnerabilities Ready for Retest in past 24 hours
* Total number of Projects Requested in past 24 hours, including project name & desired test window
* Total number of Projects In-Progress, including name, test window & total number of vulnerabilities
* Total number of Projects Waiting to Start in next 7-days, including project name & test window
* Total number of New Users in past 24 hours, including first & last names

Daily Admin Update Email supports following options:

* Enable/Disable - depending on whether you would like to use the feature or not. Default is Disabled.
* Selection of users to send the email to - you can individually add users to receive the email.
* Time each day the email will be sent - this is based on the geographical region assigned to your tenant. The emails will be sent at any given point during the selected hour.

You can access Notifications via the global menu. It is currently restricted to admin users only.

![](/files/-MXjjdxRFSbSWyZqlVyP)

![](/files/-MZHChO_n_SjGAtYpoqq)

![](/files/-MZHCXx2X7BvVRF0GhNU)

### Automating Access to Groups/Projects via Active Directory

Administrators can link Identity Provider (IDP) or Active Directory (AD) groups to AttackForge Groups.

This feature is available for Single-Sign-On (SSO) enabled tenants to help automate provisioning and removal of users to AttackForge Groups and their related projects, based on the users' IDP/AD groups.

This feature can help to ensure that users accessing AttackForge receive sufficient access to projects based on the Enterprises' own access control groups; and remove access to projects which they should not have access.

This option is Disabled by default. It is only enabled, on a AttackForge Group-by-Group basis, when an IDP/AD group is linked to the AttackForge Group.

For more information on how this feature works, please visit <https://support.attackforge.com/attackforge-enterprise/modules/groups#linking-identity-provider-active-directory-groups>

![](/files/-MZHDDo2h7nWpcPDIdGb)

### Revision History Now Available on Vulnerabilities

You can now access revision history for every change made to a vulnerability on a project or in the library.

This feature can help with tracking changes, for quality assurance or auditing.

The revision history includes:

* Field that was changed
* Datestamp when the change happened
* The user who performed the change
* The data *before* the change
* The data *after* the change

When viewing a vulnerability on a project or in the library you can click on the Revision History tab to see the changes.

![](/files/-MZHD_Np6ZGwk3oI9780)

### Set Custom Default Landing Page

The landing page is the first page a user sees when they log into AttackForge.

The default login landing page is the Global Dashboard; however you can now configure your own landing page to another area within AttackForge – for example Analytics, Vulnerabilities, Projects, Users, etc.

Admins can also update the login landing page on behalf of another user. This is useful to ensure smooth user experience for your customers.

Users can update their own landing page via Profile section. Admins can update landing page for another user via Users module.

![](/files/-MZHDkNfKdinlJBzJlNs)

### Access Project Logs On-Demand

You can now access project logs on-demand, to help with troubleshooting or auditing.

Admins can access project logs from the project dashboard page menu.

![](/files/-MZHDoyeOG1T76aVXVsQ)

### New Administration Module

The new administration module helps you to:

* Track & manage your AttackForge licensing
* Configure & customize your tenant
* Access Ticketing System (Backlog) and monitor support credits

The *Licence* tab provides overview of your AttackForge licence, including:

* Licence Type
* Licence Plan&#x20;
* SKU
* Licence Start Date
* Licence End Date
* Project Credits Used
* Project Credits Remaining/Available

You can also top-up your project credits via credit-card to avoid any disruption to your customers or business.

This is available for tenants on the **Cloud** or **Core** licence (with exception of *Unlimited* plan).

The *Configuration* tab provides tenant configuration options to customize your AttackForge experience. For a detailed list of all support configurations, please visit <https://support.attackforge.com/attackforge-enterprise/configuration-options>

NOTE: This section is new and still in progress. More configuration options will be included over the coming releases.

The *Support* tab provides a link and information on how to access Backlog - the AttackForge ticketing system for Core & Enterprise customers.

If your SLA includes Support Credits - they will also be listed on this page, including total number of support credits used and support credits remaining.

![](/files/-MZHEAPcKAxnROne1zwA)

### Updates to ReportGen

This release is action-packed with updates to ReportGen – to help you create tailored, custom on-demand reports to meet your reporting requirements, and to reduce the time wasted on manually adjusting reports.

The updates include:

* New Filter – Includes
* New Filter – Excludes
* New Filter – Count
* New Filter – Find
* New Metatags

For more information please visit <https://support.attackforge.com/attackforge-enterprise/modules/reporting>

#### New Filter – Includes

You can now check to see if a tag contains a specified value, or array of values, and continue if true/exists.

#### New Filter – Excludes

You can now check to see if a tag does not contain a specified value, or array of values, and continue if true/doesn't exist.

#### New Filter – Count

You can now use a 'count' filter to set an arbitrary counter for a condition, then reference that counter later on.

#### New Filter – Find

You can now search a tag which contains an array of objects to return an object which meets a specific condition.

#### New Metatags

We have introduced the following new tags & updates to existing tags:

* {#statusUpdates} - details for each project status update e.g. when project goes on-hold or off-hold
  * {status} - e.g. 'On-Hold' or 'Off-Hold'
  * {note} - reason why project was on-hold or off-hold
  * {created} - timestamp when project went on-hold or off-hold
* {asset\_library\_created} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
* {asset\_library\_id} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
* {asset\_external\_id} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
* {asset\_type} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
* {asset\_details} - asset details. NOTE: requires tenant configuration with Assets module enabled.
* {projectGroups} - details for each linked Group
  * {name} - name of the group
* {#retestingHistory} --> {retesting\_round} - e.g. 1, 2, 3, etc.
* {remediation\_status} - includes the remediation status of the vulnerability for the affected asset e.g. Open / Ready for Retest on \<DATE> \[NEW] / Closed on \<DATE>

### New Global Config Options Available

We have added new configuration options in this release which can be enabled on your tenant:

* Custom Email Template Header
* Custom Email Template Body Style
* Custom Email Template Footer
* Replace Likelihood of Exploitation with CVSS Score in Project Vulnerabilities pages/tables (default *Disabled*)
* Default ReportGen Project Custom Tags, to pre-fill & display on every project when a user attempts to create new ReportGen Project Custom Tags on a project (default *None*)
* Default ReportGen Vulnerability Custom Tags, to pre-fill & display on every project when a user attempts to create new ReportGen Vulnerability Custom Tags in the library (default *None*)
* Default ReportGen Affected Assets Custom Tags, to pre-fill & display on every project when a user attempts to create new ReportGen Affected Asset Custom Tags on a project (default *None*)
* Enable Password-Protection for all PDF Reports. Prior to download, user will be prompted to enter in strong password (default *Disabled*)

![](/files/-MZHF2LIrxd7oHXZEwfw)

### UX Enhancements&#x20;

This release is actioned-packed with user experience improvements.&#x20;

UX has been improved by:

* Alternate Flow to Approving a Project Request to includes changes to Project before approving it

![](/files/-MZHFDwzfA5r-B6D4jhw)

* Improved report generation & download speed by up to 20% using new optimized compression algorithms
* Better feedback when importing vulnerabilities and file is being parsed
* Option to use Affected Domain or Affected URL when importing from Netsparker
* Button to create an asset when creating a new vulnerability on a project
* Performance improvements by up to 15% on page load times when accessing Global Dashboard Vulnerabilities & Global Search
* New layout for Analytics Personalization to make it easier to build your personal dashboard
* Option to add a Remediation note at same time when Re-Opening or Closing a vulnerability
* Option to download project reports via page menu when viewing the project vulnerabilities&#x20;
* Automated email to user when they are added as a group member to a project
* Improved consistency in project breadcrumbs

## 2021-03-08

### ReportGen Gets A Power-Up!

This release is action-packed with updates to ReportGen – to help you create tailored, custom on-demand reports to meet your reporting requirements, and to reduce the time wasted on manually adjusting reports.

The updates include:

* Support for Conditions, Loops, Filters, Data Aggregation, Data Formatting & Assignments
* Support for Custom Tags
* New Tags Available

#### *Support for Conditions, Loops, Filters, Data Aggregation, Data Formatting & Assignments*

You can now add logic conditions to your ReportGen templates. These logic conditions can help make decisions on how your report should render, providing you with greater precision in your reports.

For example, if you want to create a section within a report which just contains the details for all PCI-DSS or SSL/TLS vulnerabilities, or results from the Internal/External vulnerability scanning – you can now do this!

Combining this new functionality with Custom Tags (also included in this release) provides you with full-control over custom sections within your reports.

For more information on how to use the new logic conditions in your report template, check out following support page for more details: <https://support.attackforge.com/attackforge-enterprise/modules/reporting#general-syntax-rules>

#### *Support for Custom Tags*

AttackForge ReportGen now lets you define your own custom fields/tags which can be referenced anywhere within your report templates.&#x20;

Custom fields can be used to capture additional information for projects, vulnerabilities and affected assets. This could include metadata, scoring, client information, or simply used for logically separating data within your reports - for example you can create a template to show just PCI-DSS vulnerabilities, or External vulnerabilities, etc.&#x20;

Custom fields/tags are arbitrarily defined – this means you can control the name & value of each field, to then reference in your ReportGen templates.

Custom fields/tags can be set at three (3) different levels:

* **Project-Level**
  * You can now define custom project-level fields which could be used to capture and include information relating to the overall project, for example client details, report classifications, test-related information, etc.
* **Vulnerability-Level (in library)**
  * You can now define custom vulnerability-level fields which could be used to provide supporting details for a vulnerability in the library, for example technical risk score, industry classifications, type classifications, references to internal sources/mappings, etc.
* **Affected Asset-level (vulnerability on project)**
  * You can now define custom affected asset-level fields which could be used to provide supporting details for a vulnerability on a project, for example whether its derived from internal or external scanning, whether it’s PCI related finding, etc.
  * You can also perform bulk-add/update to apply custom fields/tags across a selection of vulnerabilities on a project at one time (see following link for more details: <https://support.attackforge.com/attackforge-enterprise/getting-started/updating-vulnerabilities#bulk-add-reportgen-fields-tags>)

***Project-Level Example:***

![](/files/-MUWPumMAqRO59oQew0Y)

To start creating Custom Tags at Project-Level, check out the following support page: <https://support.attackforge.com/attackforge-enterprise/modules/reporting#project-level-custom-fields>

***Vulnerability-Level Example:***

![](/files/-MUWXW3THURPZirnalQg)

To start creating Custom Tags at Vulnerability-Level, check out the following support page: <https://support.attackforge.com/attackforge-enterprise/modules/reporting#vulnerability-level-library-custom-fields>

***Affected-Asset Level Example:***

![](/files/-MUWnl8MfeI0hmw1CeFV)

To start creating Custom Tags at Affected Asset-Level, check out the following support page: <https://support.attackforge.com/attackforge-enterprise/modules/reporting#affected-asset-level-project-custom-fields>

***New Tags Available***

We have added new ReportGen tags which can be used in your report templates.

The new tags include:

* **{#criticalVulnerabilities}** – details for just the Critical vulnerabilities on the project. Includes details for affected assets.&#x20;
* **{#highVulnerabilities}** – details for just the High vulnerabilities on the project. Includes details for affected assets.
* **{#mediumVulnerabilities}** – details for just the Medium vulnerabilities on the project. Includes details for affected assets.
* **{#lowVulnerabilities}** – details for just the Low vulnerabilities on the project. Includes details for affected assets.
* **{#infoVulnerabilities}** – details for just the Info vulnerabilities on the project. Includes details for affected assets.
* **{testcases}** – list of all the test cases linked to the vulnerability
* **{#passedTestcases}** – details for all the Passed test cases on the project.
* **{#failedTestcases}** – details for all the Failed test cases on the project. Includes details for the linked vulnerabilities which lead to test case being failed.
* **{#remediatedTestcases}** – details for all the Remediated test cases on the project. Includes details for the linked vulnerabilities which lead to test case being failed then remediated.

For more information on new tags and the data available for each tag, check out the following support page: <https://support.attackforge.com/attackforge-enterprise/modules/reporting#available-tags-for-individual-reports>

### Tracking Passed, Failed & Remediated Test Cases

In this update we have included ability to track Passed, Failed & Remediated test cases for every project.

Failed test cases can help to identify tests which need to be re-performed as part of remediation testing.

Remediated test cases help to identify which failed test cases have had all vulnerabilities fixed/closed.

You can fail a test case automatically by linking a vulnerability to a test case.&#x20;

When creating or updating a vulnerability on a project, select the failed test case(s) to link them.

You can also add a vulnerability directly from the test cases page, to quickly link the test case to the new vulnerability.

We have also included the ability to filter test cases by Passed, Failed & Remediated when viewing the test cases on the project.

You can also access the data for Passed, Failed & Remediated test cases in ReportGen (see above).

***See example below for Failed Test Case:***

![](/files/-MTOI1-s3zSp61HbdouS)

![](/files/-MTOIU6ZzdKbFmewmksf)

***See example below for Remediated Test Case:***

![](/files/-MTgk8ThzFDfNCZF4NgL)

![](/files/-MTgkCx8ecbSU9bdhBHs)

### Personalize Your Analytics

You can personalize analytics based on the data you want to see on your Analytics dashboard. Fine tune your SLAs based on how your organization tracks and measures them.&#x20;

To personalize Analytics, click on Personalize Analytics button in the top-right of your Analytics page.

In this release we have also added additional analytics widgets for the items below.&#x20;

Each widget can also be filtered based on time/date and groups.

* Zero(0)-Day Vulnerabilities
* Easily Exploitable Vulnerabilities
* OWASP Top 10 Vulnerabilities
* CWE Top 25 Vulnerabilities
* Critical Vulnerabilities
* High Vulnerabilities
* Medium Vulnerabilities
* Low Vulnerabilities
* Total Vulnerabilities
* Closed Vulnerabilities
* Open Vulnerabilities
* Ready for Retest Vulnerabilities
* Closed Vulnerabilities

You can select the analytics you want to display on your own Analytics dashboard:

![](/files/-MUzylLLwJGZOa54XXmL)

![](/files/-MUzYaDChd3Zk_ELs3re)

### New Global Config Options Available

We have added new configuration options in this release which can be enabled on your tenant:

* Disable default reports (PDF/DOCX/HTML) for all users or just client users – to force use of ReportGen custom reports (default Enabled)
* Default option for whether a new vulnerability is Visible or Pending – depending on your QA workflow (default Visible)
* Support for US date format e.g. MM/dd/YYYY (default Disabled)
* Default value for Project Name field when creating a new project (default None)
* Default value for Project Code field when creating a new project (default None)
* Default value for Scoring System field when creating a new project (default CVSSv3.1 Baseline)
* Default Project Groups when creating a new project (default None)
* Default Project Team Notifications (e.g. New Critical Vuln, New High Vuln, etc.) when creating a new project (default None)
* Default Project Admin Notifications (e.g. Vulnerability Ready for Retesting, Vulnerability Closed, etc.) when creating a new project (default None)
* Support for Middle-East work week e.g. Sunday to Thursday when requesting a new project (default Disabled)
* Auto-redirect to SSO login on visiting application login page (recommended for SSO-integration tenants with no self-registration workflow) (default Disabled)

As a reminder we also have the following configuration options already available:

* Custom domain for accessing the application
* Enable/Disable emails (default Enabled)
* Custom domain for all emails (default <discover@attackforge.com>)
* Whitelisted domains for self-registration (default None)
* Session length (default 30 minutes)
* Assets Library Module (default Disabled)
* Give project coordinators access to all new created projects (default Disabled)
* Email on change if IP address form last login (default Enabled)
* Local account self-registration (default Enabled)
* Admins require AF MFA on login via SSO (default Disabled)
* Simultaneous user sessions allowed (default Disabled)
* Custom email body for new registrations
* Custom blacklist for file upload extensions
* Self-password reset from login page (default Enabled)
* Custom value for Project Code in the UI (default Project Code)
* CIA ratings in the Vulnerability Library (default Enabled)
* Enable Slack (default Disabled)
* Enable Teams (default Disabled)
* Enable Discord (default Disabled)
* Custom default email body for daily start/stop testing email notifications
* Custom default additional email addresses for daily start/stop testing email notifications
* Custom default email body for project team email notifications e.g. new critical vulnerability
* Custom default additional email addresses for project team email notifications
* Rich-Text Editor or Text Area for Steps to Reproduce (POC) for project vulnerabilities (default Rich-Text Editor)
  * Text area will disable HTML conversion in reports & exports – to allow for verbatim POCs

### Updates to Self-Service API

In this release, we have included updates to the self-service API to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

The updates in this release include:

* Updates to *getVulnerabilities*, *getVulnerabilitiesByAssetName*, *getVulnerabilitiesByGroup* *getVulnerabilityById*, *getProjectVulnerabilitiesById*
  * Added new fields to return date stamp when status of vulnerability was last updated
  * Added new fields to return the Asset Library id & external Id values
* *createAssetInLibrary*
  * this new method allows authorized users to create new assets in the Assets Library
* *updateAssetInLibrary*
  * this new method allows authorized users to update assets in the Assets Library
* *getAssetInLibrary*
  * this new method allows authorized users to get an asset in the Assets Library by its Id
* *getAssetsInLibrary*
  * this new method allows authorized users to get assets in the Assets Library by filters

### UX Enhancements&#x20;

This release is actioned-packed with user experience improvements.&#x20;

UX has been improved by:

* Allowing users inspect & override vulnerability data before it get’s imported on a project

![](/files/-MSBH7xdMPFy7BBBCDlC)

![](/files/-MSBHaBT-H90qZyhhr6_)

* Project tracking page and tooltips (when hovering over project) now includes dates for each test suite, to help track when rounds of testing were performed on the project.
* Attack Chains now link to MITRE ATT\&CK® framework website – to help provide more detailed information on tactics used by adversaries in the attack chains
* Better error handling for all tools when importing vulnerabilities on a project, including guides for the CSV imports to indicate required fields
* Better mapping for CVSS scores, including to Likelihood of Exploitation, from tools when importing vulnerabilities on a project
* Better support for importing vulnerabilities form Netsparker
* Updates to editing multiple vulnerabilities on a project, to include:
  * Select All Vulnerabilities (currently filtered in the table)
  * De-select All Vulnerabilities
  * Select Critical Vulnerabilities (currently filtered in the table)
  * De-Select Critical Vulnerabilities (currently filtered in the table)
  * Select High Vulnerabilities (currently filtered in the table)
  * De-Select High Vulnerabilities (currently filtered in the table)
  * Select Medium Vulnerabilities (currently filtered in the table)
  * De-Select Medium Vulnerabilities (currently filtered in the table)
  * Select Low Vulnerabilities (currently filtered in the table)
  * De-Select Low Vulnerabilities (currently filtered in the table)
  * Select Info Vulnerabilities (currently filtered in the table)
  * De-Select Info Vulnerabilities (currently filtered in the table)
  * Bulk Add Tags
  * Bulk Add ReportGen Custom Tags
  * Stop Editing Multiple Vulnerabilities

![](/files/-MUzzFoY_s1hJcKhr3ui)

## 2021-01-12

### Updates to Scheduling & Planning Projects

Scheduling & planning projects is now even easier with the following updates in this release:

* Availability checker now available when grating user access to a project – making it easier to see which consultants are available (or not available) for the project & to help you with effective planning of resources.
* Calendar now available when grating user access to a project – making it easier to see which projects are currently scheduled or planned. You can also filter this calendar by user or time period.

![](/files/-MQnuhuCRTnHePNLw2lF)

* All calendars now have On-Hover feature which allows you to access key project status/progress information without having to leave the page.
* You can also now filter the schedule by user role, for example to see all projects for Consultants.

![](/files/-MQnuxPdgverJE-uu7Dt)

### Custom Email Notifications on New Vulnerabilities Discovered

When creating or updating a project, you can now set a custom email body for the new vulnerability notifications which are sent to the project team.&#x20;

You can also send the emails to additional recipients which are not already on the project team, for example SOC teams.

When creating a custom email body, ensure to include all HTML tags as the emails will be sent in HTML format.

You can adjust the standard template which is already pre-loaded in the form for you, please contact us for more details on how to do this.

![](/files/-MQnvCtkIOtcobOwlDxZ)

The following meta tags will map to the following details when the email is sent:

* {{firstName}} - this will include the firstName of the project team member. For Additional email recipients who are not on the project team, this field will be skipped.
* {{consultant}} - this is the first name & last name of the consultant who is sending the daily email.
* {{projectName}} - this will be the name of the project.
* {{priority}} - this is the priority of the vulnerability i.e. Critical, High, Medium, Low, Info.
* {{title}} - this is the title of the vulnerability.
* {{asset}} - this is the affected asset for the vulnerability.
* {{likelihood\_of\_exploitation}} - this is the likelihood of exploitation for the vulnerability. It is a number between 1 to 10.
* {{is\_zeroday}} - this is either Yes or No depending on if the vulnerability is a Zero-Day (0-day) or not.
* {{description}} - this is the description of the vulnerability.
* {{attack\_scenario}} - this is the attack scenario of the vulnerability.
* {{remediation\_recommendation}} - this is the remediation recommendation for the vulnerability.
* {{proof\_of\_concept}} - this is the proof of concept / steps to reproduce the vulnerability. This is rendered in full HTML.
* {{notes}} - this is the notes for the vulnerability.
* {{tags}} - this is the tags for the vulnerability. It is presented as an unordered list.

### Project Roles Now Available

Project Roles can be assigned to any user on the project. The roles include common stakeholders involved in pentest project lifecycle, including Red Teams, Blue Teams, Security Teams and Customers.

Project Roles are displayed in the Calendars & Project Tracking / Overview page, so that other team members can see who is on their project and also their role on the project - to help with collaboration and contacting the right person.

Project Roles are also included in the reports, alongside the project team member's name.

Project Roles do not provide the user with any additional access rights. Privileges on the project are controlled via the Access Roles.

![](/files/-MQnve6vmZ1eqnxGXpdT)

### Login Redirects Now Available

AttackForge Enterprise now supports login redirects to help your users & customers access the data they need, quickly and efficiently.

You can append any of the following redirects to the login URL, which can be shared with customers:

* ?redirectTo=sso
  * This will automatically redirect the user to sign in with Single-Sign-On (if available)
* ?redirectTo=register
  * This will automatically redirect the user to registration page (if available)
* ?redirectTo=resetPassword
  * This will automatically redirect the user to password reset page (if available)
* ?redirectTo=/#!/app/…
  * This will automatically redirect the user to a page within AFE, after the user has logged in.
  * !IMPORTANT - this is only supported with Local Accounts (Non-SSO)

Examples are provided below for reference:

* https\://\<AFE\_TENANT>/#!/login?redirectTo=sso
  * Redirect to SSO login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=register
  * Redirect to registration page
* https\://\<AFE\_TENANT>/#!/login?redirectTo=resetPassword
  * Redirect to password reset
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/#!/app/projects
  * Redirect to list of all projects after login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/#!/app/projects/5bdd20d8128aa82e0040a75d/dashboard
  * Redirect to Project Dashboard for a specified project, after login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/#!/app/projects/5bdd20d8128aa82e0040a75d/overview
  * Redirect to Project Overview for a specified project, after login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/#!/app/schedule
  * Redirect to Schedule / Calendar after login

### Projects OnHover Feature

You can now view quick project status/update by hovering over the project name, status or completed test cases in the Projects page.

This will provide an update on the project team and also the progress for each of the test suites/methodologies assigned to the project.

![](/files/-MQnwFQEdDzXWr-Gs9bC)

### Order Test Suites & Test Cases

Every test suite & test case now allows you to add a ‘Code’ which can be used for sorting & ordering test cases when displayed in projects & reports.

Code will appear before the details of the test case. For example: WEB-APP-001 Test for X, Y & Z; WEB-APP-002 Test for A, B & C; etc.

### Updates to Global Dashboard for Admins

Admins can now see the status for each of their project requests, including total requested; total pending; total approved; and total rejected.&#x20;

Clicking on any of the dashboard boxes will drill-down to view the data.

### Updates to Test Case Notes & Evidence

Adding test case notes & evidence will no longer refresh the page. You can apply a custom filter or sort to the test cases, and then create notes or upload evidence, without losing your customer filter or sort.

### Group Members Shortcut

You can now easily view users assigned to a group (group members) from the main Groups page, by using the Actions menu for a selected Group.&#x20;

This will redirect you to the Group Membership page without having to drill-down into each group.

### Group Members Full Details

When viewing the group members page, it now includes their email’s and usernames – to help with search, particularly if using SSO/AD integration.

### Better Error Handling for File Uploads

We have made changes to the error handling for file uploads so that it now provides you with the exact details why file was rejected in the alert box.

We have also removed the automated logout on failed upload, to help improve user experience.

### Delete Users & Project Requests

You can now delete a user or project request from AFE. When a user is deleted, any data they have created in the application will remain for integrity & auditing purposes.

### Updates to Project Request Form

We have now included an optional text field for users when submitting a project request. The field is Reason Testing Is Required (Justification).

This field can be used to gather details why the user is submitting the request for testing, for example it’s a new application; annual pentest; compliance exercise; etc.

### Scope Meta-tag Now Supported on Project Daily Start/Stop Emails

You can now include project scope in the daily start/stop testing email notifications. This is useful if you need to inform SOC teams with the details for assets which are being tested.

### Vulnerability Search Now Supports Tags&#x20;

When searching for a vulnerability in the library when adding/updating vulnerability on a project, the keyword search field now includes searching the associated tags assigned to the vulnerabilities, in addition to the vulnerability titles.

This makes it easier to search for a group or type of vulnerability, without having to know keywords in its title.

### Updates to User Search Fields

When searching a user in the application, the search field will now display the First Name, Last Name & Email address for the users – making it easier to find the user(s) you need.

### Updates to ReportGen&#x20;

We have included additional tags which can now be used in your ReportGen templates:

* {cvssv3\_vector} - includes the CVSS v3.1 vector string e.g. /AV/...
* {cvssv3\_base\_score} - includes the CVSS v3.1 base score e.g. 10.0
* {cvssv3\_temporal\_score} - includes the CVSS v3.1 temporal score e.g. 10.0
* {cvssv3\_environmental\_score} - includes the CVSS v3.1 environmental score e.g. 10.0
* {remediation\_status} - either Open or Closed. Only Closed if all affected assets are also Closed.
* {#abuseCases} - list of all abuse cases on the project
* {proof\_of\_concept\_raw} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
* {testcase\_code} - code assigned to the test case.
* {testsuite\_name} - name of the associated test suite.
* {testsuite\_code} - code of the associated test suite.

When downloading a report via ReportGen, the filename will now include the project’s name – making it easier to identify the report you need.

You can also now access the Offline ReportGen Diagnostic Tool to help with building your own custom ReportGen templates for AFE.

The tool can be accessed from the ReportGen page menu.

### Updates to Analytics Filter

We have now removed the placeholder Start & End dates when accessing the filter in Analytics – making it easier to add your own dates.

### Updates to Self-Service API

In this release, we have included updates to the self-service API to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

The updates in this release include:

* **GetProjectWorkspace**
  * this new method allows authorized users to view project workspace notes & metadata for uploaded files.
* **CreateProjectWorkspaceNote**
  * this new method allows authorized users to create new project workspace notes.
* **UpdateProjectWorkspaceNote**
  * this new method allows authorized users to update an existing project workspace note.
* **GetProjectNotes**
  * this new method allows authorized users to view project notes, including private notes (where applicable).
* **CreateProjectNote**
  * this new method allows authorized users to create new project notes.
* **UpdateProjectNote**
  * this new method allows authorized users to update an existing project note.

### Support for Acunetix

We have now included support for Acunetix when importing vulnerabilities on your projects.

![](/files/-MQnx3AxjBC-7Db1f4Oo)


# 2020

## 2020-11-16

### **Updates to Analytics – New SLAs, MTTR, Open Vulnerable Assets**

Measuring & Tracking performance of your security & pentesting program is crucial in understanding how individual business units, or the entire organisation, is performing over time.\
&#x20;This analysis can help to identify systemic issues across the organisation, or within function areas; and help to make informed decisions on remediation and placement of resources for security improvement.

![](/files/-MMDS7BWPOGrtGrGbu70)

In this release, we have introduced new SLAs, Mean-Time-To-Remediate (MTTR) & extended Assets with Open Vulnerabilities:

* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 15 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 15 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 30 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 30 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 45 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 45 days&#x20;
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 60 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 60 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 90 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 90 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 120 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 120 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 180 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 180 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Less than (<) 365 days
* SLA – Critical/High/Medium/Low Vulnerabilities Open Greater than (>) 365 days
* MTTR – Medium Vulnerabilities
* MTTR – Low Vulnerabilities
* Assets with Open Medium Vulnerabilities
* Assets with Open Low Vulnerabilities

### **Introducing Abuse Cases**

Abuse cases are project or assessment specific test cases. They are unique test cases which apply to the assets on the project or relate to the objective of the assessment.\
&#x20;Abuse cases help to ensure complete coverage for any given project, beyond the standard test cases.

For example, consider a web application pentest for a reverse auction website. Typically the pentest may cover the standard OWASP ASVS test cases, however the customer also requires that business logic tests are performed against the bidding functionality to determine whether it can be cheated or not. Abuse cases can be created to specifically test this functionality which relating to the application. This provides a higher level of assurance beyond standard test cases.

Abuse Cases can be created directly from the Test Cases section on a project by Admins or Project Coordinators; and are stored & tracked per project in the Test Suite Builder module under the new Abuse Cases tab.

![](/files/-MKfp7QoRHnAh6B4WjO8)

![](/files/-MKfpOQ0jPLV942QZUIW)

### **Updates to Search – Filter by Vulnerability Title, Search by Tags**

As your security & pentesting program grows and you collect valuable vulnerability data against your assets – the ability to drill-down on the exact information you need becomes essential.

To help with this, we have extended the Search capabilities in AFE to include following:

* Search all vulnerabilities (you have access to) by a Vulnerability Title – for example “*show me all vulnerabilities which are SQL Injection*”.
* Search all vulnerabilities (you have access to) by one or more Vulnerability Tags – for example “*show me all vulnerabilities which have a CVSS Score of 8.0. Now include those which are also OWASP Top 10*”.

You can also continue to search by an Asset Name or filter vulnerabilities by a Group.

![](/files/-MKfgCde9U0VxfB01vFQ)

### **Updates to Schedule – Progress Tracking + User Projects**

In this release we have included the following updates to the Schedule:

* Percentage completion for each project in calendar
  * Every project now has a percentage completion value next to the name in the calendar. This helps to identify at a glance how far the project has progressed.&#x20;
* Daily tracker now includes detailed progress breakdown for each individual test suite assigned to the project
  * This helps to identify progress on each phase of the pentest, for example:
    * Planning & Preparations (100%)
    * Web Application Pentesting (60%)
    * Abuse Cases (10%)
    * Retesting (0%)
    * …
* Filter by User now shows list of all the users’ projects
  * This helps to identify which projects the user is assigned to, and information relating to each of those projects such as status, vulnerabilities, test window, etc.
  * As this information is in a data table, it can be filtered or even exported to CSV for offline schedule copy

![](/files/-MMDSVh3KdnhYYt-ZNHE)

![](/files/-ML0e_faUBH3EWOkMVDj)

### **Customize Daily Start/Stop Testing Notifications + Send to Additional Recipients**

When creating or updating a project, you can now set a custom email body for the daily start & stop testing notifications which are sent to the project team.\
&#x20;You can also now send the emails to additional recipients which are not already on the project team, for example SOC teams.

This helps to create personalized notifications which relate to the specific project; and to also keep other stakeholders informed of testing where they are not explicitly invited to the project in AFE.

When creating a custom email body, ensure to include all HTML tags as the emails will be sent in HTML format. You can adjust the standard template which is already pre-loaded in the form for you.

The following meta tags will map to the following details when the email is sent:

* {{firstName}} - this will include the first name of the project team member. For Additional email recipients who are not on the project team, this field will be skipped.
* {{consultant}} - this is the first name & last name of the consultant who is sending the daily email
* {{started\_or\_stopped\_testing}} - this will be either 'Started Testing' or 'Stopped Testing' depending on the daily email action being performed.
* {{projectName}} - this will be the name of the project.

![](/files/-ML0cIOGlY3whqqUO7yo)

### **Project Notes Gets A Rich Text Editor**

We introduced Project Notes a few releases back. Since then it has been one of the most popular features, allowing pentesters on a project to create private notes, share team notes & also export reporting notes.

In this release we have included support for Rich Text Editor. This allows pentesters to create detailed notes with sections, headings, tables, etc. which can be used as example to capture observations during reconnaissance, and can be shared with the project team to help collaborate on a project; or stored privately for personal use.

![](/files/-MKfrSbVEqDsEnn6HQuP)

### **Updates to Self-Service API**

In this release, we have included updates to the self-service API to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

The updates in this release include:

* createVulnerabilityWithLibrary
  * this new method allows authorized user to create a new vulnerability on a project with linkage to an existing issue in the library (as opposed to providing custom description, attack scenario, recommendation, etc.)
* updateVulnerabilityWithLibrary
  * this new method allows authorized user to update an existing vulnerability on a project with linkage to an existing issue in the library (as opposed to providing custom description, attack scenario, recommendation, etc.)

### **Assign Vulnerabilities to Another Project**

As an Admin user, you can now re-assign a vulnerability to another project. Once a vulnerability is re-assigned, it will no longer be available on the current project.\
All remediation notes & evidence will also be relocated to the new project.

![](/files/-MKflEu-nD94ysRaMM02)

![](/files/-MKflL2U7qAFjQyNjbE0)

### **Project Coordinator Gets Extra Powers**

Due to the increasing role the Project Coordinators are performing in AttackForge, they are now given the following extra powers to help reduce burden on Administrators and to increase efficiency.

* Download ReportGen Base Template
* Upload new templates to ReportGen
* Modify existing templates uploaded to ReportGen

Project Coordinators can now perform following functions, in addition to standard user functions:

* can create new projects
* can update projects
* gets access to all new projects (optional)
* can invite users to projects
* can manage user access to projects
* can access all pending & actioned project requests
* can approve new project requests
* can request more information on project requests
* can reject new project requests
* assign assets to test cases on a project
* lock test cases on a project
* unlock test cases on a project
* download ReportGen Baseline template
* upload new ReportGen templates
* modify existing ReportGen templates
* full access to the Vulnerability Library module
* full access to the Test Suite Builder module

### **UX & UI Improvements**

This release is actioned-packed with user experience & user interface improvements.&#x20;

UX has been improved by:

* Allowing users to assign assets & upload files directly to workspace notes, in addition to the general upload section in the workspace
* Removing all internal CAPTCHAs to reduce user friction
* Allowing previously escaped characters & and “ to be saved on any field
* Removing double-escaping on save
* Reordered fields on Project Request form & Project Create form – to capture essential data first
* Removed timeouts on Viewing, Creating & Updating Attack Chains – to help with presentations & lengthy attack chains
* Disabled Copy/Paste & Drag/Drop screenshots feature in Rich Text Editor on vulnerability proof-of-concept – as this was not a supported feature and had caused issues for some users
* Select All in Test Cases & Project Vulnerabilities will now select all of the filtered data in the table, instead of all data in the table

UI has been improved by:

* Providing additional new themes allowing you to further personalize your experience in AttackForge. New themes include The Matrix, Lightning, Halloween & Redback

![THE MATRIX](/files/-MJzeASUgtQg9DMXWqGw)

![LIGHTNING](/files/-MJzeEGtJ4b1oMJ5T0AF)

![HALLOWEEN](/files/-MJzeI9DsGGny2XwxrsQ)

![REDBACK](/files/-MJzeNJWEZ_8D_hlnMu3)

## 2020-10-05

### Microsoft Teams Integration

AttackForge is a collaboration platform for Technology Teams, Security Teams & Engineering Teams. It helps to get the right people, in the right place with the right information.&#x20;

To help achieve this, AttackForge now integrates with industry leading collaboration platform [Microsoft Teams](https://microsoft.com/en-au/microsoft-365/microsoft-teams/group-chat-software).

Microsoft Teams allows you to engage in collaborative and inclusive meetings from anywhere with Teams meetings and Teams-enabled devices.

AttackForge integrates into your organizations Microsoft Teams via your Enterprise Microsoft Azure Identity Provider.

For detailed information on how to set up & use AF MS Teams integration – please visit <https://support.attackforge.com/attackforge-enterprise/getting-started/integrations/microsoft-teams>&#x20;

![](/files/-MHTwM2PCVVN0qfrq9Rv)

![](/files/-MHU0NMjMq_X2dq57j37)

### Perform Multiple Rounds of Testing on a Single Project

You can now perform multiple rounds of testing on a single project! This will help Enterprises to:

* Keep track of all testing & vulnerabilities against your assets, in one place
* Perform periodic assessments whilst maintaining all data in single project
* View historical rounds of testing performed against assets, without switching projects
* Make it easier for your auditors

To allocate a new round of testing on your project, click on *Add More Test Suites* button from the Test Cases page menu.

![](/files/-MHUSG9oVWb1GShqacll)

Select the test suites you would like to load on the project and click *Add Test Suites* to Project.&#x20;

The test suites will then be loaded on to your project.

By default, the new test cases loaded on to the project will be set to Unlocked/Active status.

If it is a new round of testing, you can automatically lock the previous test cases by selecting *Yes* to option *Assign Test Suites to New Round of Testing?* This will ensure the previous test cases can’t be tampered with or changed accidentally.&#x20;

It will also reset the project status to *Waiting to Start* and progress will be set to 0%.

![](/files/-MHUShstnB0scArM6hT2)

### Lock, Unlock & Delete Test Cases on a Project

You can now lock & unlock test cases on a project at any given time.

Locking test cases is useful if you need to allocate a new round of testing to your project, to ensure previous rounds of testing cannot be altered or tampered with.

When a test case is locked, it cannot be updated. You cannot add any new notes or evidence either. This provides greater assurance from an auditing perspective.

Locked test cases will not show up on or affect the project status and percentage completion.

Locked test cases will not show up in the reports as reporting is focused on the current round of testing. This helps to avoid lengthy reports on projects where multiple rounds of testing are performed.

To lock a Test Case individually - use the Actions menu on an unlocked test case and select Lock.

To unlock a Test Case individually - use the Actions menu on a locked test case and select Unlock.

You can also filter on Locked & Unlocked test cases.

![](/files/-MI1CCO1XZFuCubk7Hsm)

To perform bulk updates - use the Page menu to select the test cases and your option.

![](/files/-MI1ChXcRNEPrG7jNbny)

You can also delete test cases on a project. This can help if you need to remove test cases which do not need to be actioned on the project.

To delete test cases on a project, click on *Edit Multiple Test Cases* button from the page menu.

Select the test cases you would like to delete, then click on *Delete Selected Test Cases* from the page menu.

![](/files/-MHUTyKUqcdJ_uVkhfx1)

### Assign Assets to Test Cases

You can now assign assets to test cases. This helps to delegate tasks to individual assets to increase testing coverage and traceability.

You can assign one or more assets to the test case by clicking on editable All value in the Assigned Asset(s) column, and then selecting the assets from the list of presented options.&#x20;

You can multi-select in the field.

By default, all test cases assigned on the project will be allocated to all assets in the project scope.

![](/files/-MI2h7vqZ7y6qVSJ_dBy)

### Column Search Now Enabled on All Data Tables

You can now filter your search criteria to individual columns.&#x20;

This helps to extract the exact information you need for your reporting, management or follow ups.

You can search in one or more columns, and combine the search criteria across columns to narrow down your results even further.

The global search bar at the top of the table is still enabled so you can perform a table-wide search when you need it.

You can also use the *Export* button to export the data into a CSV after you have narrowed the search to the information you need.

![](/files/-MIgSQiPD9mKUi5lcNuk)

### Quick Actions Enabled on Projects

We have enabled a Quick Actions menu on the project dashboards, providing an improved user experience.

The Quick Actions menu helps pentesters access common functions on the project faster & without having to use the page menu.

The Quick Actions are visible for any person who has Edit permissions to the project.

![](/files/-MIgSWCuT5uM48hV2mpH)

### Project Coordinator Gets Extra Powers

Due to the increasing role the Project Coordinators are performing in AttackForge, they are now given the following extra powers to help reduce burden on Administrators and to increase efficiency.

* full access to the Test Suite Builder module
* assign assets to test cases on a project
* lock test cases on a project
* unlock test cases on a project

Project Coordinators can now perform following functions, in addition to standard user functions:

* can create new projects
* can update projects
* gets access to all new projects
* can invite users to projects
* can manage user access to projects
* can access all pending & actioned project requests
* can approve new project requests
* can request more information on project requests
* can reject new project requests
* assign assets to test cases on a project
* lock test cases on a project
* unlock test cases on a project
* full access to the Vulnerability Library module
* full access to the Test Suite Builder module

### Greater Control of Your Service Catalogue

We have added ability to toggle visibility of test suites on project requests.&#x20;

This allows you to control which test suites are published to your Service Catalogue for your customers to select from, when requesting a new project.

This also allows you to create & maintain test suites that are only visible by authorised users.

When creating or updating a test suite, select *Yes* or *No* for the option *Make Test suite Visible on Project Requests?*&#x20;

By default, all test suites are set to Yes/Visible unless you opt to hide the test suite.

### Timestamps & User Details for Test Cases on Reports

We are now including timestamps & user details for all test cases in the reports.

![](/files/-MIgShCBgMjRkKVjgAkr)

### Updates to Self-Service API

In this release, we have included updates to the self-service API to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

The updates in this release include:

* downloadVulnerabilityEvidence
  * this new method allows authorized user to download an evidence file which has been uploaded for a vulnerability on a project they have access to.
* getVulnerabilityById
  * this method has been updated to include the Steps to Reproduce / Proof of Concept in HTML format, in addition to the plain-text format.
  * this method has been updated to include the details for all uploaded files, which can be downloaded in the new downloadVulnerabilityEvidence method.
* getProjectVulnerabilitiesById
  * this method has been updated to include the Steps to Reproduce / Proof of Concept in HTML format, in addition to the plain-text format.
  * this method has been updated to include the details for all uploaded files, which can be downloaded in the new downloadVulnerabilityEvidence method.

### Nucleus Security Native Support for AttackForge JSON

Our friends over at Nucleus Security now natively support AttackForge JSON exports, allowing you to Post your AF Project JSON file directly to your Nucleus Security tenant – in one easy step.

This makes it hassle-free to export all of your pentesting vulnerabilities from AttackForge into your vulnerability management solution.

You can also set up an AttackForge Connector within Nucleus Security and upload your AF JSON files directly.

You can still use the API export for individual vulnerabilities.

![](/files/-MIgSasOD0jtmE6Hzd2R)

## 2020-09-07

### Download Multi-Reports & Group Reports

We have supercharged the Reporting module to take advantage of ReportGen capabilities!

Reporting module is a place where you can easily and quickly access reports on-demand, in any available reporting template, to save time & effort on manually creating or adjusting reports.

Using the New Reporting module, you can:

* download multiple individual reports at once for each of your projects, using your custom ReportGen templates
* download consolidated group report which contains all your data for multiple projects in one single report, using your custom ReportGen templates
* download individual reports for your projects in PDF, DOCX, HTML, CSV & JSON formats
* download individual ZIP archives for each of your projects

![](/files/-MGW90xfFe5HRamKwgTR)

AttackForge ReportGen helps you to create fully customized reports using your own DOCX templates. You can style and structure the reports however you need.

For Enterprise customers, you can access pre-existing report templates loaded by your Administrators.

Administrators can:

* **Upload New Templates** - they will be made available to all users to download custom reports
* **Download ReportGen Client-Side Tool** - this can be used to help build your custom DOCX template, with verbose logging enabled in the tool (browser console).&#x20;
* **Download Base Template** - this template contains all the meta tags that will map to your AttackForge project data. It should be the starting point when building any new templates.
* **Download Custom Template** - this template is used to create custom reports. You can download it to make necessary changes, then re-upload it to make the latest version available to users.
* **Delete Custom Templates** - using the actions menu, Administrators can delete any templates when required, for example uploading a new version for an existing template.
* View available custom reporting options.
* Download reports for any accessible projects using any of the available reporting options.

Non-Administrators can:

* View available custom reporting options.
* Download reports for any accessible projects using any of the available reporting options.

**Downloading Individual Reports**

* Step 1: Select the projects you wish to download an individual report
* Step 2: Select the template you wish to use, and click on `Download Individual Reports` button

A report will be created for each selected project using the selected template.&#x20;

**Downloading Group/Combined Reports**

* Step 1: Select the projects you wish to combine into a single report
* Step 2: Select the template you wish to use, and click on `Download Combined Report` button

A single report will be created which contains all the data for the selected projects. \
De-duplication is performed automatically to help reduce report size.

### Import Vulnerabilities Directly on Projects

You can now directly import vulnerabilities from your projects without having to use the AttackForge Connector.

This provides a faster & hassle-free way to import vulnerabilities on your projects, improving the user experience and making importing of vulnerabilities a breeze!

![](/files/-MGW9EODxE1N3SmzsbPg)

**How it Works**

* Select a tool you wish to import from, for example Nessus, BURP, Qualys, etc.
* After you select a tool, you will be prompted to select the output file from the tool in order to parse the data.
* Once the data has been parsed, you can then select the vulnerabilities you wish to import into your project.
* Once you have made your selection, click `Import Vulnerabilities` button and the vulnerabilities will be imported to your project. A summary of the import will be displayed in the notification boxes.

If you need to import data via the API, select `API` from the selection of import tools. The API is detailed and includes sample cURL request to help get you started.&#x20;

If a vulnerability template does not exist in the library, it will be automatically created for you. The next time you try to add the vulnerability, it will map to the existing template in the library.

Similarly if the affected asset does not exist on the project, it will be automatically created for you. The next time you try to add a vulnerability on the same affected asset, it will map to the existing asset on the project.

### Integrate Your Projects with Discord

AttackForge is a collaboration platform for Technology, Security & Engineering Teams. It helps to get the right people, in the right place with the right information.&#x20;

To help achieve this, AttackForge now integrates with industry leading collaboration platform Discord.

Discord is a group-chatting platform originally built for gamers, but which has since become a general use platform for all sorts of communities – in particular the InfoSec community.

AttackForge lets you integrate your projects to your own Discord server to create a private channel.

To link your Discord server to your AttackForge project and create a private channel, click on `Collaboration` button from your project dashboard then select `Discord`.

![](/files/-MGW9R57vIdmRaH2cewv)

Enter your details to connect to your Discord server & click `Create Channel`.

![](/files/-MGW9ZZo__lOklurM56V)

Once your channel is created, the following information will be displayed to all project team members.

![](/files/-MGW9blIDuh2wMhrBZGV)

### Performance, UI & UX Improvements

This release is actioned-packed with performance improvements, UI enhancements and an overall better user experience for all your users.&#x20;

Performance has been improved by:

* Redesigning the PDF, DOCX & HTML reporting functionality to reduce time taken to generate a report **up to 300%!** This is after we also included additional reporting content packed into each report – how awesome is that! 😊
* Redesigning the Data tables engine for Projects, Retesting, Reporting & Users modules – providing significant decrease in page load times of **up to 600%!** Now that’s fast 😊

UX has been improved by:

* Providing better support for importing vulnerabilities from Burp, Nessus & Qualys - including linking CVSS scores to Likelihood of Exploitation and supporting additional tags
* Updating the style of JIRA tickets & content which is exported & synced to JIRA, including better error handling and syncing
* Displaying the Owner & Last Modified when selecting an issue from the library on a project – helping you make better decisions when selecting the right vulnerability from the library
* Ability to score vulnerabilities in the library using CVSSv3.1, which are then referenced when adding a vulnerability on a project – saving time & effort when scoring vulnerabilities on every project; and improving standardization of scoring

![](/files/-MGW9jUapXgZpJgd5qLQ)

![](/files/-MGW9m4jrZINCX3IrWih)

UI has been improved by:

* Providing additional new themes allowing you to further personalize your experience in AttackForge. New themes include Neptune, Lost Woods, Amethyst & Firestorm

![NEPTUNE](/files/-MGW9v_qPaBcyfVJq8jR)

![LOST WOODS](/files/-MGWA0bffNG1UwKAwuAt)

![AMETHYST](/files/-MGWA4pGH1PtpUUxmsML)

![FIRESTORM](/files/-MGWAQ_gDwDAnkVBedmM)

## 2020-08-14

### **Export Data Tables to CSV**

You can now export any of your data tables to CSV. This allows you to quickly and easily export data from AttackForge to input into your own reports; to share information with others; or to perform your own analysis in Excel or other tools.

The export functionality will download a CSV containing all data visible in your data table.&#x20;

It also works with Search filter allowing you to extract the exact data that you need.

Want to export more or all records? Easy – just use the Show XX Entries drop-down menu to show more records.

This functionality has been implemented across all data tables in AttackForge.

![](/files/-MEdqKEIs-INyu1ZmNpF)

### **Updates to Analytics**

We have introduced a number of updates to Analytics module, to provide you with more information at your fingertips – and an enhanced user experience.

You can now see the **Days Open** for every vulnerability, when you drill-down on the analytics data. This helps with SLAs and getting on top of outstanding vulnerabilities.&#x20;

We have also included extra information in every table, such as Exploitability and Project.&#x20;

Now when you click on a link such as a vulnerability or project, it will open the data in a new tab – so you don’t lose your filtered analytics data.

Also when you filter your analytics & then drill-down on a data item, then click back button, you will be presented with your filtered data & options – so you don’t lose your filtered analytics data.

![](/files/-MEdqn9Fc-x6TkX-rxpJ)

### **Export Vulnerabilities Directly from Projects + Support for Azure DevOps**

You can now export vulnerabilities directly from your project for all supported platforms, as alternative to using the Connector.

We now support the following exports directly from your projects:

* Atlassian JIRA
* ServiceNow
* Azure DevOps
* Kenna Security
* Nucleus Security

We have also introduced support for Azure DevOps – now one of the leading platforms for orchestrating a DevOps toolchain.

Any authorised user on your projects can now easily self-export vulnerabilities as Work Items directly to your ADO Projects.

![](/files/-MEdqsHkq3kPZgtgpcLd)

![](/files/-MEdqv3MBSRIjGnMdZYF)

![](/files/-MEdqy4c_6jGSeVRvxGG)

### **Assign Test Cases To Project Team Members & Filter Test Cases**

You can now assign test cases on a project to a team member. This makes it easier to delegate tasks on a project; and to enforce accountability as well as increase efficiency by reducing doubling-up on tasks.

You can assign individual test cases to a person; or you can perform bulk assignments using page menu.

![](/files/-MEdr4EomrTwTAEqEbdQ)

![](/files/-MEdr6kpa6TFGJUGIHI1)

You can also filter test cases by the Test Suite, and also filter by:

* Test Cases Assigned to Me
* Not Tested
* Tested
* Testing In Progress
* Not Applicable

![](/files/-MEdrBFver7odq5UYbkT)

For more information on how it works, see [https://support.attackforge.com/attackforge-enterprise/getting-started/test-cases#assigning-test-cases-to-a-user](https://support.attackforge.com/attackforge-enterprise/getting-started/test-cases%22%20%5Cl%20%22assigning-test-cases-to-a-user)

### **Updates to ReportGen**

We have made a number of improvements to ReportGen to improve quality of your on-demand reports & reduce reporting noise and increase actionability.

* Duplicate Screenshots are now removed for every vulnerability, cutting report size down.
* Duplicate Affected Assets are now noted, instead of reported, significantly reducing the size of the report where there is a vulnerability affecting dozens of assets.
* ReportGen is now available in the Reporting module, along with all other on-demand report formats (PDF, DOCX, HTML, CSV, JSON & ZIP)
* Actions menus have been updated to include the Reporting option for ReportGen, allowing you to get access to reports faster!

For all the latest ReportGen metatags, try downloading a Baseline Template and check the new tags available!

### **User Experience (UX) & Performance Updates**

We have made improvements to the user experience when accessing various modules.&#x20;

Now when you access either Projects module; Test Suite Builder; or Vulnerability Library – and view information from any of the tabs – clicking the back button will take you back to the tab you were viewing, avoiding unnecessary extra steps.&#x20;

We have also rebuilt the rendering engine for data tables in the Dashboard; Analytics; Search; Vulnerability Library & Groups – providing significant decrease in page load times of **up to 600%!** Now that’s fast 😊

Feel confident showing thousands of records, and all the flexibility of the search to help you get the data you need – when you need it.

Also when you click on a vulnerability in your Vulnerability Library, it will now open in a new tab - so you don’t lose your filtered data.

We have also consolidated all Export & Collaboration integrations into single easy-to-access sections within your projects – allowing for multi-export & multi-collaboration on a single page.

### **Project Coordinator Gets Extra Powers**

Due to the increasing role the Project Coordinators are performing in AttackForge, they are now given the following extra powers to help reduce burden on Administrators and to increase efficiency.

Project Coordinators can now:

* create new projects
* update projects
* get access to all new projects&#x20;
* invite users to projects
* manage user access to projects
* access all pending & actioned project requests
* approve new project requests
* request more information on project requests
* reject new project requests
* full access to the Vulnerability Library

## 2020-07-12

### **ReportGen Now Available In Projects – Download Custom Reports On-Demand In Your Own Templates**

AttackForge ReportGen is a tool to help you create fully custom reports based on your own DOCX report templates.&#x20;

For Enterprise customers, you can now access pre-existing report templates - loaded by your Administrators - directly from your Project Dashboard by clicking ReportGen button.&#x20;

You can download reports on-demand, in any available reporting template, to save time.

This also provides your customers with flexibility to generate reports in multi-formats to help create tailored automated reports for their needs.

![](/files/-MC-8-8DOuQU3I_uu11w)

![](/files/-MC-8f4vIU58lchMU9ei)

Administrators can:

* **Upload New Templates** - they will be made available to all users on all projects to download custom reports
* **Download ReportGen Client-Side Tool** - this can be used to help build your custom DOCX template, with verbose logging enabled in the tool (browser console). This should be performed before uploading any new templates which will be available to customers, to ensure it is working as expected.
* **Download Base Template** - this template contains all the meta tags that will map to your AttackForge project data. It should be the starting point when building any new templates.
* **Download Custom Template** - this template is used to create custom reports. You can download it to make necessary changes, then re-upload it to make the latest version available to users.
* **Delete Custom Templates** - using the actions menu, Administrators can delete any templates when required, for example uploading a new version for an existing template.
* View available custom reporting options.
* Download reports on their project using any of the available reporting options.

Administrators can:

* View available custom reporting options.
* Download reports on their project using any of the available reporting options.

To download a report in a custom template, click on the `Download Report` button.&#x20;

Reports will automatically download in your browser - there is no need to use the ReportGen Client-Side Tool.

### **Project Notes Now Available**

Project Notes allows to create & store notes on your project. You can consolidate all your notes in one place, to make it easy to track & record information as you go.

The notes can include:

* **Private notes** - these are notes which are only visible to you.
* **Team notes** - these notes are available to project team members with Edit access to the project (pentesters/consultants).
* **Report notes** - these notes are included in the downloaded PDF, DOCX & HTML reports. They are also included in the JSON export & ReportGen.

Project Notes is only available to users with Edit permissions to the project.

You can access project notes from the project menu by clicking on Notes.

![](/files/-MA8cYmjKA6EWmGwZplE)

![](/files/-MA8eWK7_XkAoEpOLxYR)

![](/files/-MA8ej-5AbzUiJ5DnP2j)

### **Updates to AttackForge Connector**

We have updated the AttackForge Connector to include support for additional tools - allowing you greater flexibility when importing and exporting data to and from AttackForge.

We now support sixteen (16) industry tools & formats, with new tools & platforms constantly added to our roadmap.

The following tools & formats have been included in this release:

* Tenable.io
* Tenable.sc (Tenable Security Center)
* Netsparker
* Rapid7 Nexpose / InsightVM
* Rapid7 AppSpider / InsightAppSec
* AttackForge JSON – this can be used to import data from any AttackForge project into another AttackForge project. Particularly useful if you are a multi-tenant customer.
* CSV – this is a generic CSV importer that can work with any data. CSV template is available from within the Connector.
* Nucleus

### **Updates to Self-Service API**

In this release, we have included **2 NEW API Methods** to the SSAPI - to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

* *createVulnerabilityBulk*
  * this method allows user to create multiple vulnerabilities on a project, in one single request.
* *getApplicationAuditLogs*
  * this method allows user to download all exportable logs from the application. This can be integrated with tools such as Splunk, SolarWinds, ManageEngine, LogRythem, IBMQRadar & others

### **New Project Request – Request More Information From Customer**

Administrators can request more information for a new project request, before they Approve or Reject the request.

When requesting more information, an email will be sent to the customer with the details for the request. The information is also visible by clicking on the request to view the details.

Once an Admin has requested more information, the status of the request will be set to `Requested Information`.&#x20;

The customer can make necessary changes to the request in order to address the feedback, and once they save the updates - the status will be set back to `Pending Approval` and Administrators will be notified by email that the request has been updated and is ready for review.

![](/files/-MC-5XxP-tUZYcFWdAW2)

### **UX Improvements**

We have made the following enhancements to AttackForge to ensure yours’ and your customers experience is the best that it can be!

* Support for Scrolling Sidebar on Global Menu
* Now include \_likelihood\_of\_exploitation, \_severity and \_testcases for all vulnerabilities in the JSON export
* Managing Access to Projects (via Users module) now removes existing projects the user has access to
* Managing Access to Groups (via Users module) now removes existing groups the user has access to
* Managing Access to Self-Service API (via Users module) now removes existing SSAPI methods the user has access to; including button to Add All & Remove All when performing updates
* Unified Data tables – all data tables now have a unified experience. All data is loaded by default to assist with pagination. You can still filter number of records on screen using the Show XX Entries option. Search will now return results based on all records.
* Simpler & Unified Flow for Re-Opening & Closing Vulnerabilities on a project.

## 2020-06-15

### **Attack Chains Now Map to MITRE ATT\&CK Framework**

You can now map attack chains to MITRE ATT\&CK Framework.&#x20;

This helps to create standardised attack chains & threat models, and will benefit any Red Team, Blue Team or Purple Team activities in your environment.

Blue teams will be able to leverage MITRE’s global knowledge base of adversary tactics to get enriched information on each action performed in the attack chain.

Red teams will be able to articulate their attack sequence with more clarity by leveraging wealth of information relating to their attack pattern provided in MITRE’s framework.

Mapping to MITRE ATT\&CK Framework takes only minutes & is easy to do. Check out our tutorial video on how to start mapping your attack chains to MITRE ATT\&CK Framework:

{% embed url="<https://youtu.be/JtNoGook5h4>" %}

### **Service Catalogue Now Available to Your Customers**

When a customer is requesting a new project, they must specify the service which they would like to purchase or proceed with. The test suites are now presented to the customer as a Service Catalogue, allowing them to pick and choose what they would like to be performed on their project. Test suites can be adjusted to align with the security services offering for a consultancy or internal security team/function.

Every service in the catalogue includes a brief description, tags & total number of test cases that will be assigned to the project – should the customer select it.

They are visible to the customer by hovering over any service in the drop-down list.

For example, if a customer requires a PCI DSS penetration test to meet their annual penetration testing requirements, they can select the service from the catalogue and list the details for the PCI assets in-scope for the assessment (see below).\
&#x20;Or if the customer requires a Pre-Launch Assessment for a New Web Application – they can select the service & it will automatically load any test cases on the project related to this activity, once the project is approved.

The feature is also extended to Admins when manually creating a new project.

For more details please visit: <https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project>

### **CVSS v3.1 Temporal & Environmental Calculators Are Now Available**

Previously we had introduced an alternative scoring system which allows you to score your vulnerabilities using CVSS v3.1 Baseline in-app calculator.

We have now extended this to also include CVSS v3.1 Temporal & Environmental Calculators.

After you score a vulnerability using CVSS, it will automatically include the CVSS Vector String + CVSS Score for you as tags.

If you are using Temporal or Environmental scoring, it will include the Base Score, Temporal Score & Environmental Score as separate tags.

When creating a new project, or at any time during a project (via Edit Project) - you can select a scoring system for the vulnerabilities.&#x20;

AttackForge supports following scoring systems:

* Manual&#x20;
  * manually select Priority (Critical / High / Medium / Low / Info)
  * manually select Likelihood of Impact (0 to 10)
* CVSS v3.1 Baseline
* CVSS v3.1 Baseline + Temporal
* CVSS v3.1 Baseline + Temporal + Environmental

For more information please visit: <https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects#selecting-a-scoring-system>

### **Duplicate Vulnerabilities On Your Projects**

You can now duplicate any vulnerabilities on your project, against selected assets.

The system will create a new vulnerability (for each of the selected) and assign it to the assets which you have also selected.&#x20;

This makes it fast & easy to assign vulnerabilities to assets during a pentest where multiple affected assets have been discovered later on for a vulnerability which had already been reported.

For more information please visit: <https://support.attackforge.com/attackforge-enterprise/getting-started/managing-vulnerabilities/updating-vulnerabilities#duplicate-vulnerabilities>

### **Bulk Open or Close Vulnerabilities On Your Project**

You can now perform bulk action to Open or Close selected vulnerabilities on you project.

This makes it fast & easy to close or re-open vulnerabilities on projects where there is a large amount of vulnerabilities discovered.

This is particularly useful for issues relating to vulnerability scanners, where by many vulnerabilities may be observed fixed/remediated during retest.&#x20;

For more information please visit: [https://support.attackforge.com/attackforge-enterprise/getting-started/managing-vulnerabilities/updating-vulnerabilities#mark-vulnerability-as-closed-re-opened ](https://support.attackforge.com/attackforge-enterprise/getting-started/managing-vulnerabilities/updating-vulnerabilities#mark-vulnerability-as-closed-re-opened)

### **Adding Scope Now Supports New Lines**

You can now create new scope on a project using a line break, in addition to comma-separated values.&#x20;

This helps to avoid unnecessary effort of converting assets to comma-separated values where they are already leveraging a line break format.

For more information please visit: <https://support.attackforge.com/attackforge-enterprise/getting-started/project-scope#add-assets-scope>

### **Daily Tracker Now Includes Color-Coding**

We have updated the colors used on the daily tracker page to help identify relevant sections easier.

For more information please visit: <https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects#place-project-on-hold-off-hold>

## 2020-05-14

### **ReportGen Updates**

We have released an update to ReportGen Tool & Template files:

* ReportGen Tool:&#x20;
  * AttackChains are now supported
  * Updates to auto-scale images to correct dimensions without exceeding page width
  * Tags & Help information is now available in browser console
* ReportGen Template
  * Meta tags for AttackChains are now included
  * Updates to Testing Summary to include additional data/tags

### **JSON Export Updates**

We have released an update to the project JSON Export:

* Now includes AttackChains, including icons in base64
* Additional tags for Testing Summary section

### **New AttackChain Entities**

We have released an update to AttackChains:

* You can now select additional entities including Device, Server & Database.
* For the new entities, you can select from either an existing asset on the project; or enter a new asset name. Any new assets are only included for purpose of the attack chain and are not added to project scope.

### **Updates to Self-Service API**

In this release, we have included **39 NEW API Methods** to the SSAPI - to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

* createScope - this method allows user to create new assets on a project that they have Edit access to.
* updateScope - this method allows user to update assets on a project that they have Edit access to.
* createRemediationNote - this method allows user to create a remediation note for a vulnerability on a project that they have access to.
* sendDailyCommencementEmail - this method allows user to send daily commencement notification on a project they have Edit access to.
* sendDailyCompletionEmail - this method allows user to send daily completion notification on a project they have Edit access to.
* updateTestcase - this method allows user to update a testcase on a project they have Edit access to.
* createTestcaseNote - this method allows user to create a note on a testcase for a project they have Edit access to.
* requestRetest - this method allows user to request a retest on a project they have access to.
* confirmRetestCompleted - this method allows user to confirm retest is completed on a project they have Edit access to.
* updateExecSummaryNotes - this method allows user to update executive summary notes section of report on a project they have Edit access to.
* getGroups - this method allows user to get details for groups the user is a member of.
* getVulnerabilitiesByGroup - this method allows user to get details for all vulnerabilities for a group that they are a member of, with optional filter.
* getProjectsByGroup - this method allows user to get details for all projects for a group that they are a member of.
* getVulnerabilityLibraryIssues - this method allows user to get details for all vulnerabilities in the library.
* updateVulnerabilityLibraryIssueById - this method allows user to update a vulnerability in the library.
* getTestsuites - this method allows user to get details for all test suites.
* getTestsuiteById - this method allows user to get details for a Testsuite, including list of test cases.
* getUsers - this method allows user to get details for all users in the system, with option filter.
* getUserById - this method allows user to get details for a user in the system.
* getAssets - this method allows user to get details for all assets the user has access to.
* getAssetsByGroup - this method allows user to get details for all assets for a specified group.
* createGroup - this method allows user to create a new group.
* updateGroup - this method allows user to update a group.
* getGrou - this method allows user to get details for a group.
* addUserToGroup - this method allows user to create a new member on a group.
* updateUserAccessOnGroup - this method allows user to update a users’ membership for a group.
* createTestsuite - this method allows user to create a new test suite.
* updateTestsuite - this method allows user to update a test suite.
* addTestcaseToTestsuite - this method allows user to add a new test case on a test suite.
* updateTestcaseOnTestsuite - this method allows user to update a test case on a test suite.
* updateUserAccessOnProject - this method allows user to update a users’ role/permissions for a given project.
* createUser - this method allows user to create a new user in the system.
* deactivateUser - this method allows user to deactivate a user in the system.
* activateUser - this method allows user to activate a user in the system.
* getUserAuditLogs - this method allows user to get audit logs for a user, with optional filter.
* getUserLoginHistory - this method allows user to get login history for a user, with optional filter.
* getUserProjects - this method allows user to get details for all projects a user has access to.
* getUserGroups - this method allows user to get details for all groups a user has access to.
* getProjectAuditLogs - this method allows user to get audit logs for a project, with optional filter.

## 2020-04-13

### **ReportGen Now Available**

We have released AttackForge ReportGen which is a tool to help you create fully customizable reports based on your own DOCX templates.

ReportGen provides you with the flexibility and autonomy to create reports which are specific to your organization, requirements, target audience or style guidelines.

We have included a baseline template that is aligned with the AFE PDF report and includes all necessary tags to help you get you started. You can download the template from AFE.&#x20;

You can build upon this template or create new templates entirely, to reflect your reporting needs.

ReportGen is a self-contained HTML file and works in your browser. There is no need to install anything.

It works in an offline environment and requires no Internet or dependencies to run. All reports are generated locally in your browser.

ReportGen works as follows:

1. Download JSON export from your AFE project
2. Download ReportGen & AFE ReportGen Template
3. Open ReportGen in your browser. Select AFE JSON export file. Select DOCX template.
4. Your new report will automatically download.
5. Enjoy savings hours of reporting time! 😊

ReportGen is available to all users. There is a button on the Project Dashboard to access ReportGen, or you can access it directly via ReportGen module in navigation pane.

### **Updates to Self-Service API**

In this release, we have included the following updates to the SSAPI - to help provide you with more flexible and powerful ways of interacting with AFE.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

* createVulnerability - this method allows user to create a vulnerability on a project that user has Edit access to. Any new assets will be automatically added to the project. Any new issue descriptions will be automatically added to the library.
* updateVulnerabilityById - this method allows user to update a vulnerability on a project that user has Edit access to. You can update status of vulnerabilities using this method. Any new issue descriptions will be automatically added to the library.
* createVulnerabilityLibraryIssue - this method allows user to create a new vulnerability in the library, which can be used by users when creating a new vulnerability on a project.
* getprojectRequests - this method allows user to get project requests that the user has access to, with optional filter to narrow results.
* createProjectRequest - this method allows user to create a new project request. This method can be used to integrate into your existing workflows and systems, to enable seamless project requests via 3rd party systems and scripts.&#x20;
* getProjectRequestById - this method allows user to get a project request by its Id, if user has access to it.&#x20;
* updateProjectRequestById - this method allows user to update a project request by its Id, if user has access to it.
* approveProjectRequestById - this method allows user to approve a project request by its Id. Approved project requests are automatically created as new projects in the system, and users invited accordingly (including email notifications).
* rejectProjectRequestById - this method allows user to reject a project request by its Id. Email notification is sent to the requestor notifying them project has been rejected and reason(s) why.

### **Additional Project Email Notifications**

We have added support to enable project email notifications to project team or to admins on various events. This helps to keep people informed on progress and status changes for vulnerabilities on their projects.

Notifications can be enabled or disabled via project creation form, or via project update form.&#x20;

The following events can be enabled on a per-project basis:

* Email Project Team on:
  * New Critical Vulnerability
  * New High Vulnerability
  * New Medium Vulnerability
  * New Low Vulnerability
  * New Informational Vulnerability
* Email Admins on:
  * Vulnerability Ready for Retesting
  * Vulnerability Re-Opened
  * Vulnerability Closed

### **Download Project Assets as CSV**

We have added ability to download the project scope (assets assigned to a project) in CSV format. This helps testers extract scoping information from AFE more effectively so they can load it in various tools.

You can download the project assets CSV file via the Scope section on your project.&#x20;

### **Updates to Project JSON Export**

We have added support for uploaded files to vulnerabilities (as evidence) to be included in the project JSON export file. This includes all files, not just images.&#x20;

This helps to export your evidence into various tools in a consolidated way that can be automated. All files are encoded in Base64, including raw Base64 value and Base64 Data URL.

### **Updates to User Interface (UI) & User Experience (UX)**

We have included the following updates to UI/UX in this release:

* Updates to Analytics Groups filter when selecting 2 or more groups, a checkbox will now show up with ‘Only Search Projects With Selected Groups Linked To The Project’. If you click the checkbox and run the search, it will filter results based on projects where all of the selected groups are linked. Otherwise, you can continue to use the default search for groups which operates on an Inclusive or basis.
* Updates to Security Code form when logging in, to include OTP input box (instead of standard input box used previously). You can also use the keyboard Enter button to select Sign in with Mobile button (instead of having to click it with mouse).
* Updates to Project Scope field when creating a project, to make it a text area. This allows you to enter in multiple assets via comma-separated values, which is easier and faster when dealing with large groups of assets.

## 2020-03-16

### **Self-Service API Now Available**

We have released a Self-Service API for AFE. This API aims to provide you with more flexible and powerful ways of interacting with AFE.&#x20;

It utilises static API keys which are assigned to individual users and can be used in scripts, batch jobs, cURL requests, or other ways - to help with:

* Creating custom dashboards & analytics with the information you or your organisation needs, at any time
* Creating custom queries for projects, vulnerabilities, testcases, etc.
* Simplifying workflows for creating projects, requesting & approving projects, etc. initiated from your own tools/platforms
* Providing hooks into upstream & downstream pentesting flows, and integrations into Enterprise eco-system
* Creating service accounts with limited functionality to perform specific tasks only

In this release we have included thirteen (13) API methods – with more planned for future releases.&#x20;

Access to each method for every user is managed and controlled by Administrators via Users module. By default, users have no access to the Self-Service API. This must be enabled by an Admin for a given user, including scope of methods allowed for the user.

Every method has detailed documentation page which includes information & examples for all parameters (optional & mandatory); URL for each method; example cURL requests & example server responses.

The Self-Service API can be accessed from the global navigation menu by clicking on Self-Service API module.&#x20;

* getVulnerabilities - this method allows user to get all vulnerabilities in the system, that user has access to. It includes detailed information for every vulnerability, and optional filters to narrow results.
* getProjects - this method allows user to get all projects in the system, that the user has access to. It includes detailed information for every project, and optional filters to narrow results.
* getProjectById - this method allows user to get detailed information for a given project that the user has access to.&#x20;
* getProjectVulnerabilitiesById - this method allows user to get detailed information for all vulnerabilities on a given project that the user has access to, and optional filters to narrow results.
* getVulnerabilityById - this method allows user to get detailed information for a vulnerability that the user has access to.&#x20;
* getVulnerabilitiesByAssetName - this method allows user to get detailed information for all vulnerabilities which match specified asset name, that the user has access to, and optional filters to narrow results.
* getProjectTestcasesById - this method allows user to get detailed information for all project testcases for a given project that the user has access to, and optional filters to narrow results.
* getMostVulnerableAssets - this method allows user to get statistics on the Most Vulnerable Assets that the user has access to, and optional filters to narrow results.
* getMostCommonVulnerabilities - this method allows user to get statistics on the Most Common Vulnerabilities that the user has access to, and optional filters to narrow results.
* getMostFailedTestcases - this method allows user to get statistics on the Most Failed Testcases that the user has access to, and optional filters to narrow results.
* createProject - this method allows user to create a new project in the system.&#x20;
* updateProjectById - this method allows user to update any project in the system.
* inviteUserToProjectById - this method allows user to invite another user to a given project and specify their privileges/access on that project.

### **Updates to Core Framework Modules**

We have updated a number of core framework modules to the latest stable versions.

This will help to ensure stability, performance, reliability, security & robustness of the overall solution.

### **Bug Fixes in Reports and Testcases**

We have addressed a number of bugs in the reports which affected visual representation of certain sections or text.

We have also addressed a bug in the test cases where under certain circumstances, the counter on the project dashboard would not update accordingly to changes made to the test cases.

### **Sessions No Longer Timeout on Vulnerability Library Create/Update**&#x20;

We had previously addressed an issue where customers had experienced data loss during session timeout on creating or updating a vulnerability on a project.

We have now extended this to include the Vulnerability Library.

Now when you are entering or updating an issue in the Vulnerability Library, your session will remain active until you either navigate away or log out.

Session’s will timeout as per normal on all other screens, with exception of Add/Edit project vulnerability (see previous release notes).

## 2020-02-20

### **Updates to Connector**

We have updated the AttackForge Enterprise Connector to be compatible with the following tools / platforms.

This makes it easier and faster to import data from your favourite tools into AttackForge; or to export data from your AttackForge projects into other tools / platforms.

* Qualys
* OpenVAS
* OWASP Zed Attack Proxy

### **Updates to PDF, DOCX & HTML Reports**

We have updated the PDF, DOCX and HTML reports to address a number of issues & bugs, namely:

* Performance updates to increase speed of report generation
* Increased robustness of reports to effectively handle large projects with many thousands of issues/findings &#x20;
* Improved translation from HTML to Plain-Text for the Steps to Reproduce / POC, providing more consistent results
* Addressed a number of bugs with regards to visual representation of reports

### **Ability to Secure Delete Projects**

We have added support to provide Admin users with the ability to securely delete all data related to a project from the database, uploads & logs.

This allows you to ensure that any sensitive projects can be sanitized and securely removed. Note the records will exist in any prior backups taken.

In order to perform a secure delete, you must first Archive a project. From the Archived Projects tab (in Projects module), you can use the item menu to select from the following options. You will be prompted to confirm and authorise the action.

* Destroy Project Data (Keep Logs)
  * This option will delete all project data from the database & uploads, however will maintain the logs (which are available to Admins via Users module)
  * A new record will be created in the logs for the user, indicating the project ID & name that was deleted (for auditing & security)
* Destroy Project Data (& Logs)
  * This option will delete all project data from the database, uploads & logs.
  * A new record will be created in the logs for the user, indicating the project ID & name that was deleted (for auditing & security)

**IMPORTANT**: This feature is disabled by default for security reasons. The buttons will appear however will not work. This can only be enabled by request to AttackForge team.

### **Support for CVSS 3.1 Scoring**

We have added support for CVSS 3.1 as an alternative scoring system for vulnerabilities on projects. This aligns with industry best practices and helps you to enforce a more consistent approach to determining issue Priority and Likelihood of Exploitation.

When using CVSS scoring system, you only need to click the buttons which apply to the issue you have discovered. The Priority and Likelihood of Exploitation will automatically update based on your selection.&#x20;

Note the CVSS Vector will automatically be added to the Tags section and updated with each change in scoring.

To access this scoring system - when creating a new project, you will now see a drop-down menu allowing you to select the scoring system. By default, CVSS 3.1 is selected, however you can still access the previous scoring system by selecting Manual.

You can toggle between scoring systems for a project at any time by Editing the project and selecting the new scoring system.

## 2020-01-29

### **Updates to Analytics**

We have updated the Analytics module to provide you with even better discovery & analysis of your vulnerabilities and pentesting data.&#x20;

This will help to identify trends and problem areas and provide better tracking of progress on remediation activities.&#x20;

Analytics can also be filtered across Dates & Groups, so you have greater control over the time periods and business functions which are relevant to you & your reporting.

You can also drill-down on any of the metrics, to identify root cause.

The newly added areas to Analytics include:

* Total Projects
* Total Assets
* Assets with Open Critical Vulnerabilities
* Assets with Open High Vulnerabilities
* Critical & Open Vulnerabilities <30 Days, <60Days, <90 Days
* High & Open Vulnerabilities <30 Days, <60Days, <90 Days
* Mean-Time-To-Remediate (MTTR) for Critical Vulnerabilities
* Mean-Time-To-Remediate (MTTR) for High Vulnerabilities
* Top 10 Most Vulnerable Assets

### **Normalization of Vulnerability Import Data**

We have added support to match existing issues from API & Connector imports to relevant issues in the Vulnerability Library.

This allows you to:

* Import findings from various sources, via API or Connector
* For any newly created issues in the library during the import process, you can now freely make relevant changes to the text for those issues in the library – and have this reflected back on subsequent imports
* On the next import which has same issue details, they will be automatically linked to the updated versions in the library

This will save you time & effort from having to modify the descriptions, attack scenarios & recommendations every time you run an import.

For example, if you import a Nessus scan with the issue ‘SSL Certificate Expiry’ – on the first import, if it does not exist in the library - it will create the issue for you.

Then you can make changes to the issue in the library, for example change the title to ‘Expired SSL Certificate In Use’.

Next time you import a Nessus scan and it has SSL Certificate Expiry which is the same as before, it will be automatically linked to the updated issue in the library Expired SSL Certificate In Use.

### **Resource Manager View for Calendar / Schedule**

You can now filter the Schedule / Calendar by users, in addition to by projects – allowing you to be more effective when planning resources on upcoming pentests.&#x20;

You can see which projects that users/pentesters are assigned to for any given day/week/month and determine which users/pentesters are heavily utilized - or have capacity for projects.

You can compare multiple users at the same time, to get a clearer picture of the team’s overall capacity and availability.

### **UI & Compatibility Updates to Connector**

We have added support for AF Connector for the majority of common browsers, including Chrome, Firefox, Safari and Edge. This ensures you can access import & export functionality in your native browser, without having to rely solely on Chrome.&#x20;

We have also made minor UI updates for error handling, and also to display statistics on a successful import operation.

NOTE: BURP import is not supported in Firefox & Edge at the moment, due to Firefox and Edge not supporting native XML v1.1 parser (which is required by BURP XML exports).

### **New Workflow for Editing Multiple Vulnerabilities**

We have added a new workflow which allows you to make changes to multiple vulnerabilities on a project, one-after-another, all from a single screen.

This makes it easier and faster to perform QA on vulnerabilities, and review & make changes without losing track.&#x20;

This option lets you update each issue and move on to the next one, or you can traverse through the issues using the Previous and Next buttons until you find the one(s) you want to update.

You can access this workflow by selecting multiple vulnerabilities on the project and using the page menu to select ‘Update Selected Vulnerabilities (Individually)’.

We have kept the alternate workflow, Update Selected Vulnerabilities (All)’, which allows you to make bulk updates to vulnerabilities in one go.

This is useful when you need to update the details for all issues at the same time, for example update the POC for all selected issues.

### **Re-assign Affected Asset for Vulnerabilities**

We have added support to re-assign affected assets for existing vulnerabilities on a project. If you have created a vulnerability against an incorrect asset, you can now update the affected asset to the correct value.

This can be performed on an individual vulnerability, or you can also perform mass-updates to multiple vulnerabilities at the same time.

### **Meta Tags in Executive Summary Notes**

We have added support for the following meta tags in the Executive Summary Notes section of the reports. This will make it faster to reference the project’s details without having the look them up – or worry about making changes retrospectively if the project details are updated.

* {{{projectName}}} – will display project’s name
* {{{projectCode}}} – will display project code
* {{{projectStart}}} – will display project start date
* {{{projectEnd}}} – will display project end date

## 2020-01-14

### **Nessus & BURP Import Added to Connector**

You can now import vulnerabilities from Nessus and BURP export formats (.nessus & .xml) to a given project via AttackForge Connector.

This makes it fast & easy to add multiple vulnerabilities from scanners, in a matter of minutes.

Importing vulnerabilities is easy – simply download the Enterprise Connector from the module in AttackForge, open the HTML file and follow the steps.

You can select all vulnerabilities to import or adjust your selection by ratings (Critical/High/Medium/Low/Info). You can also individually select the issues you want to import.

We are currently working on other integrations for AttackForge Connector with customer-requested tools and platforms – watch this space!

### **Import Vulnerabilities via API**

You can now directly import vulnerabilities for a given project via AttackForge API.

This allows you to directly feed vulnerability data into your project, from various sources - including tools, scripts, or adding historical data.

All details on how to access the API, including sample working cURL requests, can be found from your project menu by selecting Import Vulnerabilities.

Only users with Edit access to a project, or Administrators, can access this API.

To help save you time & effort – if you import a vulnerability which does not already exist in your library, or if the affected asset does not already exist on your project – it will automatically create these for you.&#x20;

If the issue exists in the library, or if the asset exists on the project – it will automatically link these to your vulnerability that you are importing.

You can also use this API for bulk imports on projects.&#x20;

### **Daily Tracker Now Added To All Projects**

Every project now has a daily tracker which shows you how many vulnerabilities were discovered each day on the project, and how many testcases were actioned.&#x20;

You can click on the items to drill-down and see the corresponding details.

We have also included the history for all project On-Hold & Off-Hold notices, which are also included within the daily tracker.

Each notice includes the status (On-Hold or Off-Hold), the reason/explanation, and date/time stamp.

You can access the daily tracker from the Project Dashboard by clicking on Tracker button, or via the Schedule by clicking on the project name.

### **Project On-Hold / Off-Hold Enhancements**

When you place a project On-Hold or Off-Hold, you are required to enter an explanation which is sent to all project team members by email – to inform all stakeholders why the project is On-Hold or Off-Hold.

If a project is On-Hold, an alert box is displayed at the top of the project dashboard to inform project team members of the issue and when it was raised.

In addition, the global dashboard now displays details for Projects On-Hold – to help inform you & stakeholders of issues affecting projects as soon as you log in to AttackForge.

### **Sessions No Longer Timeout on Vulnerability Create/Update**&#x20;

We have addressed an issue where customers had experienced data loss during session timeout on Creating or Updating a vulnerability.

Now when you are on these screens, your session will remain active until you either navigate away or log out.

Session’s will timeout as per normal on all other screens.

### **Tags Added to Vulnerability Library**

Tags have been added to the Vulnerability Library to help you with searching the library more efficiently & effectively.


# 2019

## 2019-11-22

### **AttackForge Connector Now Available**

AttackForge Connector is our tool that allows you to export findings from AFE into other industry leading tools.&#x20;

It works with AFE JSON files which can be exported from your projects.

It’s client-side & self-contained HTML file – so no install is required. It can be downloaded within AFE from ‘Connector’ module.

Currently AttackForge Connector supports the following tools, however we have many tools planned for integration in upcoming releases:

* JIRA Cloud
* ServiceNow
* Kenna Security

AttackForge Connector aims to become our gateway product for bi-directional data integration between AttackForge and other tools & platforms.

AttackForge Connector works as follows:

1. Log in to AttackForge and download JSON report for the project/vulnerabilities you wish to export + AttackForge Connector file (from Connector module).
2. Open the AttackForge Connector HTML file and select the JSON file to upload.
3. Select the vulnerabilities you wish to export.
4. Select the tool which you would like to export selected vulnerabilities to.
5. Fill in export details for your tool.
6. Click submit. Vulnerabilities should be exported directly to the tool.

NOTE: Due to strict CORS security settings set by JIRA, ServiceNow & Kenna Security – direct exports from browser to the tools is not allowed (denied by browser) for security reasons.

Therefore, all export requests are routed via AttackForge proxy infrastructure to comply with CORS security settings set by the tools.

Please let us know if you would like us to help you configure AttackForge Connector to utilise your own proxy service.

### **JSON Report Now Available**

You can now export project vulnerability reports in JSON format (in addition to PDF, DOCX, HTML & CSV).

JSON reports contain all the information which is currently provided in the standard reports. You can customise content of the JSON report based on your Report Settings.&#x20;

JSON reports can be used to integrate AFE findings into your own existing reporting templates.

JSON reports can also be used to export AFE findings into other systems via AttackForge Connector, or via direct feeds into other tools.

### **Vulnerability & Asset Report Mappings**

We have now included 2 additional appendices within the vulnerability reports, to help provide a snapshot of affected systems and their remediation status.

1. Vulnerability-to-Asset Mappings: a list of all vulnerabilities and the assets/systems affected by that vulnerability (including remediation status)
2. Asset-to-Vulnerability Mappings: a list of all assets and the vulnerabilities affecting each asset (including remediation status)

### **Bug Fixes & Performance Improvements**

We have addressed a number of bugs (particularly in the PDF reports) and well as made performance optimizations (for page load times and reporting speeds) - to help improve user experience.

## 2019-11-04

### **Group Membership Now Available**

You can now link users to Groups. This will make it easier to manage visibility, collaboration and access to projects as your security & penetration testing program grows. For example:

* You can add management and executives to their related Groups so they can track performance and view analytics across their business units.
* You can add technology and engineering teams to their related Groups so they always have visibility of issues/vulnerabilities arising on their systems.
* You can add pentesters & security teams to their related Groups to ensure they always get the right access to new projects for delivery.

A few notes on how Group Membership works:

* Users can belong to one or more groups.
* When adding a user to a group, the user will automatically receive access to all projects that the group already has access to, and to any new projects which are created and also linked to the group.
* You can set the default access level/permissions for projects when adding the user to the group, and you can update this at any time. Any updates will apply to all projects linked to the group.
* When a user is removed from a group, their access to all projects which are linked to the group is also removed.
* When a project is added to a group, all group members will receive access according to their group default settings.
* When a project is removed from a group, access to all group members is also removed.
* You can still update a user’s access to an individual project at any time – for example a user might have View access to a Group, however can have Upload/Edit access to a specific project on that group; or can be removed from a specific project.
* You can still invite users to individual projects and manage their access as per normal.
* You can access Group Membership from Groups --> Group --> Users; or from Users --> \[Manage Access to Groups] or \[Grant Access to Groups]

### **Staging Workflow for Vulnerabilities**

When creating or editing a vulnerability, you can now control visibility of the issue. By default, vulnerabilities are set to be visible as soon as you create them.&#x20;

However you can choose to temporarily hide the issue so that only people with Edit access on the project can see it. And when you are ready – you can set it to be live/visible to entire project team.&#x20;

This will help you to register vulnerabilities as you test, and choose when you want this information released to the project team.

It can also help with allowing for review cycles, where vulnerabilities need to be reviewed before they are released to customer/stakeholders.

People with Edit access to a project will see an additional box on their project dashboard (Pending) - this is where the staged issues are held.&#x20;

Pending/staged vulnerabilities do not show in any dashboards, reports, search or analytics – until they are set to live.

### **Project Coordinator Role**

There is now a Project Coordinator role which can be applied to a user via Users module. Project Coordinators are intended to help facilitate & manage projects, without having to provide the user with Admin privileges.

Project Coordinators inherit standard user privileges, however gain additional abilities:

* Automatically receive view access to projects which have been created (manually or via project request workflow)
* Invite other users in the system, to projects which they have access to - for example invite pentesters, clients, developers, etc.
* Update a users’ privileges for a given project (except for their own privileges). This includes deleting/removing a user from the project.
* Update scope on a project.
* Ability to view all Pending project requests & Actioned project requests.

### **Enable or Disable MFA for Application User Accounts**

Administrators can now enable or disable MFA for application user accounts. By default, MFA is enabled on all application accounts when they are created. However admins can now disable (or re-enable) this for specific users (if required).

This may help in events where a user has lost their mobile device and cannot login, or other circumstances where MFA cannot be performed.

Note this does not affect MFA settings for SSO accounts.

### **Account De-activation & Self-Reset 2FA Enrolment**

Users can now choose to deactivate their account (if it is no longer required) via Profile menu (when logged in). Once an account is de-activated, the user cannot log back in (without an Admin first unblocking their account).

Deactivated accounts are not deleted from the system, and all data remains in AttackForge. Accounts can be re-activated by Admins at any time.

Users can now also self-reset their 2FA enrolment via Profile menu (when logged in). The user is required to authorise this using their current passphrase.&#x20;

Once reset is authorised & completed, the user will be automatically logged out and will receive a new QR code to scan upon next login.

### **Project ‘On-Hold’ Status**

Projects can now be set to ‘On-Hold’ status. This is intended for projects where testing has had to stop for various reasons, for example experiencing difficulties/delays, environment issues, etc.

Admins can set (and unset) a project to On-Hold using actions menu on Projects screen, or by using the Project menu (when on project dashboard).

### **Updates to README**

We have updated the README to include further details on Backing Up Application Data, including details on where files are stored/persisted on local file system – to help with your backup processes.

## 2019-09-19

### **Updates to Report Customisation**&#x20;

You can now upload your own logo that you would like to be included on the reports. This provides flexibility where reports need to be provided to different audiences or branded differently.

Each user has the freedom to upload their own logo which is saved to their profile, along with their own reporting options. Reporting options can be accessed from the Customize Vulnerability Report section (accessed from project menu or Reporting module).&#x20;

In addition, we have added ability to customize reports to show only vulnerabilities which are Open, Closed or Retesting – or any combination. This provides greater flexibility when generating targeted reports, for example you can generate a report which shows you only Critical & High vulnerabilities which are currently Open or Retesting.

### **Admins Can Now Create Users**

We have added ability for Admins to manually create new users in the system, without having to go through standard registration workflow. This provides greater flexibility and efficiency when accounts need to be created quickly and on short notice.

You can add new users by clicking on the ‘Create New User’ button in the Users administration module. For SSO users, you can enter the SSO username in the ‘Username’ field. Otherwise just include the email address.

## 2019-09-06

### **Stealth Mode Now Available**

We have released a new ‘Stealth Mode’ theme for AttackForge – it’s our version of Dark Mode and was requested by popular demand! Particularly useful for the pentesters/hackers **😊**

You can access Stealth Mode from the global menu. Your theme settings save against your profile, so you don’t have to keep setting it on each login. You can toggle between normal and stealth at any time.

### **Test Case Evidence Now Available**

Previously we had released ability to add notes for each test case on every project. Now we have introduced ability to also upload evidence/files for each test case. This expands AttackForge’ s capabilities and potential use for non-security testing projects, for example self-audits & compliance audits against PCI DSS, HIPPA, NIST, ISO, 3rd party due diligence, etc.&#x20;

However, for pentesting projects - this means you can now also include screenshots to support test cases. For example, if a test case is Not Applicable – you can add justification/note & upload screenshots. All notes & screenshots are date/time stamped, tracked by user and also included in the downloaded reports.

How it works:

1. Create a Test Suite for your audit, for example PCI DSS, HIPPA, NIST, ISO, 3rd party due diligence, etc.
2. Create a new project and apply the test suite.
3. If it’s a self-audit by 3rd parties, you can invite them to the project – they can then work through each of the test cases/checklist items and mark them off as they go, whilst also upload supporting evidence & add notes.
4. Customer internal team can then review the response to the checklist/test cases, add additional comments/notes, and if there are any issues they can be raised as an issue/vulnerability on the project. You can define your own issues e.g. ‘Policy Not In Accordance With Customer Guidelines’ in the Vulnerability Library.
5. If it’s an internal audit, you can follow the same process as above however without inviting 3rd parties to the project – instead Customer staff will run through the checklist.

### **Performance Improvements**

A number of performance improvements have been applied which makes using AttackForge smoother & faster. This includes optimizations to downloading reports to make it faster.

## 2019-08-20

### **JIRA Sync Now Available**

You can now sync your vulnerabilities with JIRA. This ensures that vulnerability data on a project is always kept up to date between AttackForge & JIRA.

Syncing is easy to do – after you have exported vulnerabilities to your JIRA project, you can then click the ‘Sync with JIRA’ button to pull in latest details for your selected vulnerabilities; as well push any new changes or notes.&#x20;

JIRA Sync works with any JIRA Cloud tenant & project, making it easy for your business stakeholders to stay on top of latest pentest findings and remediation activities on their projects.

### **Test Case Notes Now Available**

You can now add notes for each test case on every project. This ensures that supporting information and evidence is tracked against every test case performed.

For example, if you mark a test case as ‘Not Applicable’ or leave it as ‘Not Tested’ due to environment issues – you can now add supporting evidence & justification.

Or if you would like to assign test cases to individuals or share notes between pentesters when performing test cases – you can now do so using test case notes.

Each note is date & timestamped and linked to the user who created or updated the note for traceability.

### **Performance Improvements**

A number of performance improvements have been applied which makes using AttackForge smoother & faster. This includes updates to all major modules including Analytics, as well as improvements on load times for vulnerability library when adding/editing a new issue on a project.

## 2019-08-01

### **HTML Reports**

You can now download HTML reports for any given project - in addition to PDF, DOCX & CSV. These reports are fast to download, robust & customizable by format - which can be used for integration into other systems or for easy search & grep.

They are self-contained HTML files with all screenshots included. These reports will make accessing findings a breeze.

### **Markdown Now Available**&#x20;

Markdown is now available when adding or editing a vulnerability. You can apply markdown to Proof-of-Concepts/Steps to Reproduce which makes it easy to include code snippets for payloads, rich text formatting and more.

Markdown will make POCs more robust and combined with in-line screenshots previously released – you now have all you need to help developers reproduce issues quickly and effectively.

## 2019-07-08

### **ServiceNow Integration Now Available**

You can now export your project vulnerabilities to any ServiceNow tenant. Each vulnerability will be raised as an incident. You can select the category you would like to apply.

ServiceNow integration comes standard with AttackForge Enterprise and in addition to Atlassian JIRA integration. It is available to all project team members.

### **Manage Vulnerability Library from Add/Edit Project Vulnerability Screens**

If you are an Admin or Library Moderator - You can now Create a new vulnerability in the library, Edit an existing vulnerability in the library, Duplicate an existing vulnerability in the library and modify it, and Refresh your library – all from the project Add Vulnerability & Edit Vulnerability screens. This makes it easier and faster to manage your vulnerabilities as you are adding them to projects.

### **Update to Calendar**

Calendar (available from global menu) now displays pending projects (new project requests), in addition to projects which are Waiting to Start, In Progress and Completed.

## 2019-06-19

### **Screenshots Now Available In Vulnerability Steps to Reproduce & Notes**

You can now insert uploaded screenshots in the Steps to Reproduce & Notes section for each vulnerability, which will display in the PDF & DOCX reports.&#x20;

This will help readers better understand flow of steps when reproducing the vulnerability, as well as provide additional context to support the notes.

It’s easy to do – simply add three (3) curly braces around the file name – for example {{{screenshot.png}}}. You can insert screenshots at any place within the Steps to Reproduce & Notes sections.

It also works with renamed files too, for example {{{Step 1}}}.

### **Help & Info Now Available**&#x20;

We have added a Help & Info section which is available from the global menu. This includes FAQ which covers the most common questions we get from users.

We are aiming to include additional information in the near future as well as short video tutorials, to help people familiarize faster when using AttackForge Enterprise.

## 2019-05-21

### **Enterprise Groups**

We have now completed dashboards for groups. You can now view & drill down on following details for each group:

* Total vulnerabilities + critical + high + medium + low + zero-day + easily exploitable + CWE top 25 + OWASP top 10 + open + retesting + closed
* Total projects + waiting to start + in progress + completed
* Total assets
* Total attack chains
* Total project members (users)
* Group owner
* Primary contact (name, email, phone)
* Drill-down on each item above

### **Analytics**

We have added the ability to drill down in Analytics. You can now view & drill down on following details.

Also we had recently introduced filters – which allows you to filter this information based on start/end dates as well as groups.

* Critical vulnerabilities
* High vulnerabilities
* Medium vulnerabilities
* Low vulnerabilities
* Open vulnerabilities
* Retest vulnerabilities
* Closed vulnerabilities
* Zero-day vulnerabilities
* Easily Exploitable vulnerabilities
* OWASP Top 10 vulnerabilities
* CWE Top 25 vulnerabilities
* Top 10 Most Frequent Vulnerabilities

### **Rename Uploaded Files**

We have added the ability to rename files after they have been uploaded.

This allows you to rename screenshots in the report, to provide more details about what is happening in each screenshot. You can also rename uploaded workspace files or logs to give more meaningful descriptions.

## 2019-05-07

### **Enterprise Groups**

Groups feature allows admins to assign & track projects (and their related assets & vulnerabilities) to one or more groups, for example business units, internal clients, external clients, platform owners, etc.

This allows for broader visibility of security posture within organisational segments, and ability to allocate Group Owners and contacts who are responsible for systems (assets) in their group, and their related vulnerabilities.

This will help enterprises to visualize vulnerable areas within the organisation faster and more efficiently, to help plan remediation activities.

### Admins can now:

* Create & Update groups – includes group name, group owner, and primary contact details (name, email, phone)
* Assign projects (and their related assets & vulnerabilities) to one or more groups

### Users can now:

* View analytics across one or more groups, for a given period of time – for groups they have access to
* View all vulnerabilities for a given group – for groups they have access to

### Coming soon:

* Dashboard for each group which shows:
  * Total vulnerabilities + critical + high + medium + low + zero-day + easily exploitable + CWE top 25 + OWASP top 10 + open + retesting + closed
  * Total projects + waiting to start + in progress + completed
  * Total assets
  * Total attack chains
  * Total project members (users)
  * Group owner
  * Primary contact (name, email, phone)
  * Ability to drill-down on dashboard items

## 2019-04-24

### Users can now:

* Customize PDF & DOCX reports based on the content the user wishes to include in the report
* Currently there is 30 different content items which can be independently toggled on/off in the reports
* Each user can easily update & save their own global reporting options which applies to every report they download
* Customisation menu can be accessed from Project drop-down menu & Reporting module

### Admins can now:

* Customize PDF & DOCX reports for each user in the system
* Admin customisation menu can be accessed from Users module

### Project team members can now:

* Request new round of retesting – email will be sent to admins with request details (email is disabled in Demo env. to avoid spamming people)
* Confirm round of retesting is completed (if user has project Edit permissions) – email will be sent to all project team members to inform retesting is completed
* Track history for every round of retesting, including what was retested
* See retesting results in the reports

### We have also made some updates to the reports:

* Track remediation history
* Include number of assets affected by total vulnerabilities
* Include number of assets with Fixed issues
* Include number of assets still undergoing Retesting
* Include number of assets with Non-Fixed issues
* Include number of assets affected by individual vulnerabilities
* Summary if vulnerability is Fixed/Not-Fixed
* Border’s applied to screenshots (in PDF report)
* File name applied to screenshots


# Getting Started


# How It Works

Using AttackForge is as fast, easy and effortless.

<figure><img src="/files/adJaopTTIFJ1HF66twt7" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6Uf6BRLvXSvm2RxpMAKL" alt=""><figcaption></figcaption></figure>

## 1. **Request A New Project / Create New Project**

Customers can Request a New Project. Information will be provided to the security team for review and to action the project request. Customer will receive email notification based on the outcome. If approved, customer will be invited to the newly created project.

Authorized users can also manually create new projects. Define what will be tested (e.g. application, infrastructure, etc.); apply testing methodologies; test window; etc.

After you create a project, you will be redirected to the Project Dashboard. From here you can access everything else you need to start testing and working on your project.

## **2. Invite Project Team**

Invite people to your project e.g. developers, pentesters, customers, security managers, project managers, etc. Your project team can also upload necessary files and information required for testing e.g. test credentials, solution documents, diagrams, etc.

## **3. Start Working Through Test Cases**&#x20;

Test cases help to track what was and wasn't tested. Every test case can have detailed information and execution flows. Add supporting notes and evidence for each test case.

## **4. Add Vulnerabilities**

You can harness the built-in vulnerability library to quickly add vulnerabilities to your project/pentest; or create custom write-ups from the Writeups Library.

## **5. Generate On-Demand Reports & Export Findings into Tickets**

Project team, including your customers, can download automated reports in a variety of reporting templates.

Project team can also export vulnerabilities to ticketing tools such as JIRA, ServiceNow, Azure DevOps and more.


# Requesting A Project

## Overview

Customers can request a new project by clicking on Projects module, then selecting `New -> Project Request` from the page menu.&#x20;

<figure><img src="/files/UIJdyFSsYzGK9DxDF7aS" alt=""><figcaption></figcaption></figure>

During the project request process, the customer will complete a scoping form and upload any supporting information. The form will capture initial scope & details. All form fields, including custom fields, as well as presentation and form logic can be modified in Administration module.

<figure><img src="/files/FvEmfwOKswbCTGtW9XUe" alt=""><figcaption></figcaption></figure>

The customer can select one or more services from the service catalogue presented.

<figure><img src="/files/To4hEksX23DsPDdI6T5t" alt=""><figcaption></figcaption></figure>

Once the customer has completed the form, they can submit it for review. The relevant authorized users will be notified of the new project request and can commence the review process. The customer will also be notified by email that their request is pending review.

Users can view pending requests by clicking on Pending Requests from within the Projects module.

Users can make modifications to their pending requests.

<figure><img src="/files/R20QLstbOAzxJXdAJsWr" alt=""><figcaption></figcaption></figure>

Administrators and Project Coordinators can `Approve` or `Reject` requests.&#x20;

<figure><img src="/files/lYHBrlvluwYFB92wHn3g" alt=""><figcaption></figcaption></figure>

Project requests can be approved by selecting `Approve and Setup Project`.

When approving a project request, you can choose to setup the project as either a clone of an existing project; or as an entirely new project. Cloning a project is ideal if the request is for a new round of testing for a previously tested application, system or set of assets.

When opting to set up the new project as a clone from an source project, the details from the source project will prevail.

Alternatively when opting to set up the new project not as a clone, then the data from the project request will be pre-loaded into the form for convenience.

You can view the details from the project request in the Request pane.

The Info pane will include any specific information relating to the selected field in the form as you are completing the form. &#x20;

<figure><img src="/files/bsROpoWbqBvW1YZvDr5x" alt=""><figcaption></figcaption></figure>

If a request is Approved, the project will be automatically created and the customer will be invited to the project. The customer will be notified by email that their request was approved. The email will include a link to access the project dashboard.&#x20;

<figure><img src="/files/yuEDrsh43MTn0cWODNMK" alt=""><figcaption></figcaption></figure>

If a request is Rejected, the user can include a reason why the project was rejected. The customer will be notified by email that their request was rejected and the reason for rejection.

<figure><img src="/files/adJRHWj2qxkyfl4zwavg" alt=""><figcaption></figcaption></figure>

Authorized users can also request more information for a project, before they Approve or Reject the request.

When requesting more information, an email will be sent to the customer with the details for the request. The information is also visible by clicking on the request to view the details.

<figure><img src="/files/fyfn9AD8SvFVt7iV9iU5" alt=""><figcaption></figcaption></figure>

Once request for more information is made, the status of the request will be set to `Requested Information`. The customer can make necessary changes to the request in order to address the feedback, and once they save the updates - the status will be set back to Pending Approval and authorized users will be notified by email that the request has been updated and is ready for review.

All users can see the history of their project requests in the `Actioned Requests` section in Projects module.

<figure><img src="/files/dd58yaGQcFwKjINkmakO" alt=""><figcaption></figcaption></figure>

## Selecting a Test Suite from the Service Catalogue

When a customer is requesting a new project, they must specify the service which they would like to purchase or proceed with. The test suites are presented to the customer as a Service Catalogue, allowing them to pick and choose what testing they would like to be performed. Test suites can be adjusted to align with the security services offering for a consultancy or playbooks for internal security teams.

For example, if a customer requires a PCI DSS penetration test to meet their annual penetration testing requirements, they can select the service from the catalogue and list the details for the PCI assets in-scope for the assessment. Additional fields can be configured based on the service selected, to capture specific information for a variety of different security audits.

<figure><img src="/files/NPEULkUhxbI3WiWi05t1" alt=""><figcaption></figcaption></figure>

## Project Request Direct Access Controls

Every project request comes with direct access controls which can be applied to help ensure the right people are notified and access is granted as needed.

<figure><img src="/files/clsvQhV43aV2SUtfiX2r" alt=""><figcaption></figcaption></figure>

Access to project requests can be granted to the following:

* Users
* Groups

This is in addition to implied access granted for Administrators and Project Coordinators; or other users who have been delegated with the privileges to view, edit and action project requests via global delegations.

Access to each project request includes the following options:

* **View** - user can view the project request, however not make any changes.&#x20;
* **Edit** - user can view the project request and make changes.
* **Action** - user can action the project request.

Actions on project requests include:

* **Approve** - approve the project request and create a new linked project.
* **Reject** - reject the project request with a reason.
* **Request more information** - request the submitter to resubmit the project request based on feedback.

### Group Access

When a group is linked to a project request, the access controls applied to the relevant group members will apply to the project request.&#x20;

These settings can be managed directly from the groups settings:

<figure><img src="/files/5lL7pfvJ7NGHYYJoFeP4" alt=""><figcaption></figcaption></figure>

### Auto-Add Project Request to Group

Groups support an optional feature which allows project requests created by the groups members, to be automatically added to the group. This can be useful to automate access to project requests and reduce manual overheads of administering access.

<figure><img src="/files/WxwdUoOPnz8Ukr3sukpw" alt=""><figcaption></figcaption></figure>

In the example below, if ANY of the following users create a project request:

* Project Manager
* Joe Pentester
* Client User
* Account Manager

Then the following access will be granted automatically to that project request if the auto-add feature is enabled:

* Project Manager - will get Action access.
* Joe Pentester - will get View access, unless is project request creator in which will have Edit access.
* Client User - will get no access, unless is project request creator in which will have Edit access.
* Account Manager - will get Edit access.

<figure><img src="/files/rFsxFmfTUkKSMQhXKPZH" alt=""><figcaption></figcaption></figure>


# Creating & Updating Projects

Creating a new penetration test (pentest) or other security testing activity

## Overview

AttackForge is built upon Projects. Each project has scope (assets) and findings (vulnerabilities). Vulnerabilities are linked to assets.

Projects can be any of the following typical security testing activities, however is not limited to the following:

* **Red Team and Purple Team Assessments**
* **Web Application Penetration Test**
* **Web Services / API Penetration Test**
* **Mobile Application Penetration Test**
* **Network and Infrastructure Penetration Test**
* **Wireless Network Assessment**
* **Source Code Review**
* **Configuration Audit**
* **PCI-DSS Assessment**
* **SCADA Assessment**
* **OSINT Assessment**
* **Physical Security Audit**

Only Administrators, Project Coordinators, and delegated users can create a new project.

To create a new project, click on Projects module from the main menu. You will see a page which contains a table with all of your projects. To create a new project, click on `New -> Project` from the page menu.

<figure><img src="/files/1VicH1xkFX9MF2A1pukW" alt=""><figcaption></figcaption></figure>

## Project Name

The project name is used to identify this pentest or security testing activity within the system.&#x20;

## Project Code

The project code is also used to identify this pentest or security testing activity within the system. It does not have to be unique. It can be any value.

### Validating Project Code

AttackForge will validate the project code to check whether an existing project exists using the same code.

You can also fetch the latest project code to help with sequencing.

<figure><img src="/files/S31nIXxaMAtiVkEQYfXj" alt=""><figcaption></figcaption></figure>

## Linking Groups

You can link one or more groups to a project. Groups may be used in the following ways, however is not limited to following:

* Link a Customer to their project
* Link a Customer's 3rd party to the project (for example development agency)
* Link a Customer and their related organizational sub-units to the project
* Link a Platform / Technology to a project
* Link a Functional Team to the project
* Link a Security Team to the project

Linking a group will have the following effects:

* Any Group Members will automatically receive access to the project, based on their access level defined in the Groups' settings.
* Group Members will be able to filter Analytics based on the Group.
* Project data will be included in the Group Dashboard.

> **!IMPORTANT:** Group members do not receive access to project-related emails by default. You need to enable this option in the groups settings when creating/updating a group.

<figure><img src="/files/w8SO6JigTr2lE1fv6jLe" alt=""><figcaption></figcaption></figure>

## Linking Portfolio Streams

You can associate a project with one or more Portfolio Streams at time of project creation or approval; or when editing a project. Data from the project, such as vulnerabilities and assets, will be associated with the relevant portfolios and streams automatically.

<figure><img src="/files/05zBv2vFOsBNNfOJid94" alt=""><figcaption></figcaption></figure>

## Scope

Every project must have at least one (1) asset assigned to the project scope.&#x20;

Assets can be, but are not limited to, the following:

* URL for a web application or web services / API
* Name of an application or server
* Network subnet or IP address e.g. 192.168.0.1/24
* File (for example if performing a code review)
* Person (for example if performing social engineering)
* Address (for example if performing a physical security audit)

The asset(s) can be entered in manually or pasted in. You can also set your delimiter options for multi-scope projects.

<figure><img src="/files/7MPFuOh8u8eN4ZSWxInm" alt=""><figcaption></figcaption></figure>

If using the Assets module, assets can be selected from a list of pre-existing assets for which user has access.

<figure><img src="/files/0ALkCxUVzqMsoZsR0kDb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Utp8D7EMbTA5xCvzNkMs" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/qw8RiNeHep456bN9K1md" alt=""><figcaption></figcaption></figure>

## Test Suites

When creating a new project, you can select one or more testing methodologies/playbooks/checklists to apply to the project - referred to as Test Suites. &#x20;

A test suite helps:

* Clients understand exactly what was and was not tested on the project;
* Developers/Engineers link test cases to vulnerabilities;
* Pentesters structure their testing in a methodical, consistent & standardized way.

When hovering over each test suite, you will see brief information relating to what the test suite is intended for and number of test cases that will be assigned to the project.

AttackForge comes preloaded with test suites from industry benchmarks such as OWASP, MITRE OSSTMM, and more.&#x20;

You can create custom test suites via the Test Suites module. This allows you to define exactly what you will test on your projects; or for customers.

<figure><img src="/files/1eeDISyt1DPYlnuxsIzQ" alt=""><figcaption></figcaption></figure>

## Vulnerability Scoring System

When creating a new project, you can select a scoring system for the vulnerabilities.&#x20;

AttackForge supports following scoring systems:

* Manual&#x20;
  * manually select Priority (Critical / High / Medium / Low / Info)
  * manually select Likelihood of Exploitation  - 1 \[Difficult to Exploit] to 10 \[Trivial to Exploit]
* CVSS v3.1 Baseline
* CVSS v3.1 Baseline + Temporal
* CVSS v3.1 Baseline + Temporal + Environmental
* CVSS v4 (coming soon)

<figure><img src="/files/6K6S2hFcomCkBhxxBSJo" alt=""><figcaption></figcaption></figure>

## Vulnerability Code

Vulnerability code field is used to generate user-friendly unique vulnerability identifiers for all vulnerabilities on the project.

For example, if you set a vulnerability code as **SEC01** - the first vulnerability created on the project will have an alternate user-friendly unique identifier of **SEC01-1**. The next vulnerability will be **SEC02-2** and so on.

You can update the vulnerability code on a project at any time, so long as it's a unique value (has not been used on ay other projects) and is between three (3) to eight (8) characters in length.

When you update a vulnerability code on a project - all of the existing IDs for any of the projects' vulnerabilities will also be updated to match, except for Linked Vulnerabilities. To manage the vulnerability code for a linked vulnerability, you need to update the settings from the source project.

> **!IMPORTANT**: Be careful if you update a vulnerability code on a project - as it may break your existing references to vulnerabilities in other external tools.

<figure><img src="/files/29loThdYaSShAj8lZgsb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6sr3WqkOIWpb36mOOdWp" alt=""><figcaption></figcaption></figure>

## Apply SLAs on Vulnerabilities

This field will become available if you have SLAs enabled in `Administration -> Vulnerabilities -> SLA`.

Usually SLAs will be automatically applied to any new vulnerabilities created or imported on your projects.

However you can opt-out of applying SLAs automatically, and instead apply them manually on selected vulnerabilities.&#x20;

This is useful if you want SLAs to applied only under certain conditions, for example:

* Apply SLAs only at the end of the project
* Apply SLAs only when the application team acknowledges the findings
* Apply SLAs only on certain projects, for example compliance/regulatory projects
* Apply SLAs only on certain vulnerabilities that require an SLA

<figure><img src="/files/BdxhBMr1BH9YgVI9Vq9I" alt=""><figcaption></figcaption></figure>

To disable automatic creation of SLAs on your project, select `Manual` option.

To apply SLAs manually on your vulnerabilities, please check [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/updating-vulnerabilities#re-apply-slas-on-vulnerabilities).

## Email Project Team on Events

To make vulnerability information flow faster to help reduce time-to-remediate - email notifications can be enabled on different events, such as discovery of a new critical or high vulnerability. You can opt into whichever email notifications are relevant for the project.

<figure><img src="/files/d175FtHNZqdWIbre4QAP" alt=""><figcaption></figcaption></figure>

### Notification Type For New Vulnerabilities

Emails for new vulnerabilities can be sent in two different ways:

* **Send Individual Email For Each New Vulnerability** - an email will be sent to each opt-in project team member (and additional recipients, if configured) for every new vulnerability discovered. That is, *one email will be sent per new vulnerability*.
* **Send One Email with Details For All New Vulnerabilities** - an email will be sent to each opt-in project team member (and additional recipients, if configured) which includes a summary for every new vulnerability discovered, in a single email.\
  That is, *one email will be sent with a summary for each new vulnerability.*

### Email Body for Vulnerabilities&#x20;

#### Send Individual Email For Each New Vulnerability

If you have opted into '*Send Individual Email For Each New Vulnerability*' (see above) - you can set a custom email template for each vulnerability.

When creating a custom email body, ensure to include all HTML tags as the emails will be sent in HTML format. You can adjust the standard template which is already pre-loaded for convenience.

The following meta tags will map to the following details when the email is sent:

* **{firstName}** - this will include the firstName of the project team member. For Additional email recipients who are not on the project team, this field will be skipped.
* **{consultant}** - this is the first name & last name of the consultant who is sending the daily email.
* **{projectName}** - this will be the name of the project.
* **{priority}** - this is the priority of the vulnerability i.e. Critical, High, Medium, Low, Info.
* **{title}** - this is the title of the vulnerability.
* **{asset}** - this is the affected asset for the vulnerability.
* **{likelihood\_of\_exploitation}** - this is the likelihood of exploitation for the vulnerability. It is a number between 1 to 10.
* **{is\_zeroday}** - this is either Yes or No depending on if the vulnerability is a Zero-Day (0-day) or not.
* **{description}** - this is the description of the vulnerability.
* **{attack\_scenario}** - this is the attack scenario of the vulnerability.
* **{remediation\_recommendation}** - this is the remediation recommendation for the vulnerability.
* **{proof\_of\_concept}** - this is the proof of concept / steps to reproduce the vulnerability. This is rendered in full HTML.
* **{notes}** - this is the notes for the vulnerability.
* **{tags}** - this is the tags for the vulnerability. It is presented as an unordered list.
* **{link}** - hyperlink to view the new vulnerability in AttackForge
* **{link.url}** - URL to view the new vulnerability in AttackForge
* Support has now been added for '{vulnerabiity.\<tag>}' Custom Email Meta Tags:  <https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags>

<figure><img src="/files/caIOZivtWza9u3kAft0W" alt=""><figcaption></figcaption></figure>

#### Send One Email with Details For All New Vulnerabilities&#x20;

If you have opted into '*Send One Email with Details For All New Vulnerabilities*' (see above) - you can set a custom email template with a summary for all new vulnerabilities.

When creating a custom email body, ensure to include all HTML tags as the emails will be sent in HTML format. You can adjust the standard template which is already pre-loaded in the form for you.

The following meta tags will map to the following details when the email is sent:

* **{firstName}** - this will include the firstName of the project team member. For Additional email recipients who are not on the project team, this field will be skipped.
* **{consultant}** - this is the first name & last name of the consultant who is sending the daily email.
* **{projectName}** - this will be the name of the project.

> **!IMPORTANT:** The following tags must be included within **{vulnerabilities}...{/vulnerabilities}** tags in your template - for example {vulnerabilities}\<li>{priority} - {title}\</li>{/vulnerabilities}

* **{priority}** - this is the priority of the vulnerability i.e. Critical, High, Medium, Low, Info.
* **{title}** - this is the title of the vulnerability.
* **{asset}** - this is the affected asset for the vulnerability.
* **{likelihood\_of\_exploitation}** - this is the likelihood of exploitation for the vulnerability. It is a number between 1 to 10.
* **{is\_zeroday}** - this is either Yes or No depending on if the vulnerability is a Zero-Day (0-day) or not.
* **{description}** - this is the description of the vulnerability.
* **{attack\_scenario}** - this is the attack scenario of the vulnerability.
* **{remediation\_recommendation}** - this is the remediation recommendation for the vulnerability.
* **{proof\_of\_concept}** - this is the proof of concept / steps to reproduce the vulnerability. This is rendered in full HTML.
* **{notes}** - this is the notes for the vulnerability.
* **{tags}** - this is the tags for the vulnerability. It is presented as an unordered list.
* **{link}** - hyperlink to view the new vulnerability in AttackForge
* **{link.url}** - URL to view the new vulnerability in AttackForge
* Support has now been added for '{vulnerability.\<tag>}' Custom Email Meta Tags:  <https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags>

### Email Body for Start/Stop Testing

When creating or updating a project, you can set a custom email body for the daily start & stop testing notifications which are sent to the project team. You can also send the emails to additional recipients which are not already on the project team, for example SOC teams.

When creating a custom email body, ensure to include all HTML tags as the emails will be sent in HTML format. You can adjust the standard template which is already pre-loaded for convenience.

The following meta tags will map to the following details when the email is sent:

* **{firstName}** - this will include the firstName of the project team member. For Additional email recipients who are not on the project team, this field will be skipped.
* **{consultant}** - this is the first name & last name of the consultant who is sending the daily email.
* **{started\_or\_stopped\_testing}** - this will be either 'Started Testing' or 'Stopped Testing' depending on the daily email action being performed.
* **{projectName}** - this will be the name of the project.
* **{projectStartDate}** - start date for the project.
* **{projectEndDate}** - end date for the project.
* **{key.\<custom\_field>}** - you can access your project custom fields using {key.\<custom\_field>} where \<custom\_field> is the key for your custom field. For example, if you had a custom field 'Out of Scope' and it had a key 'out\_of\_scop&#x65;*'* - you can use {key.out\_of\_scope} to print the value of the custom field in this email.
* **{scope}** - this is the scope on the project. It is presented as an unordered list.
* **{link}** - hyperlink to view the project in AttackForge
* **{link.url}** - URL to view the project in AttackForge
* **{startTesting}...{/startTesting}** - information which will only be sent in email's when testing has started.
* **{stopTesting}...{/stopTesting}** - information which will only be sent in email's when testing has stopped.
* **{totalVulns}** - total number of vulnerabilities on the project.
* **{totalCriticalVulns}** - total number of critical vulnerabilities on the project.
* **{totalHighVulns}** - total number of high vulnerabilities on the project.
* **{totalMediumVulns}** - total number of medium vulnerabilities on the project.
* **{totalLowVulns}** - total number of low vulnerabilities on the project.
* **{totalInfoVulns}** - total number of informational vulnerabilities on the project.
* **{totalVulnsToday}** - total number of vulnerabilities that were discovered today.
* **{totalCriticalVulnsToday}** - total number of critical vulnerabilities that were discovered today.
* **{totalHighVulnsToday}** - total number of high vulnerabilities that were discovered today.
* **{totalMediumVulnsToday}** - total number of medium vulnerabilities that were discovered today.
* **{totalLowVulnsToday}** - total number of low vulnerabilities that were discovered today.
* **{totalInfoVulnsToday}** - total number of informational vulnerabilities that were discovered today.
* **{totalActionedTestCasesToday}** - total number of actioned test cases today.
* **{totalFailedTestCasesToday}** - total number of failed test cases today.
* **{totalRemainingTestCases}** - total number of remaining test cases on project.
* **{totalRemainingTestCases%}** - percentage of remaining test cases on project.
* **{totalCompletedTestCases}** - total number of completed test cases on project.
* **{totalCompletedTestCases%}** - percentage of completed test cases on project.
* Support has now been added for '{project.\<tag>}' Custom Email Meta Tags:  <https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-email-meta-tags>

> **!IMPORTANT:** The following tags must be included within **{assets}...{/assets}** tags in your template - for example {assets}\<li>{name} - {details}\</li>{/assets}

* **{name}** - name of the asset.
* **{type}** - asset type.
* **{details}** - details for the asset.
* **{external\_id}** - external id of the asset.
* **{key.\<custom\_field>}** - you can access your asset custom fields using {key.\<custom\_field>} where \<custom\_field> is the key for your custom field. For example, if you had a custom field 'Asset Owner' and it had a key 'asset\_owne&#x72;*'* - you can use {key.asset\_owner} to print the value of the custom field in this email.

Let's take a look at an example email template:

```
<p>Hi {firstName},</p>
<p>This is just a courtesy email to let you know {consultant} has {started_or_stopped_testing} for <br /><b>{projectName}</b></p>
{startTesting}
<p>As a reminder, testing for {projectName} will be performed between {projectStartDate} to {projectEndDate}.</p>
<p>The scope for testing is as follows: 
{scope}
</p>
{/startTesting}
{stopTesting}
<p>
Please find summary below for today's testing:
</p>
<h4>Testing Progress</h4>
<ul>
<li>Test Cases Actioned Today: {totalActionedTestCasesToday}</li>
<li>Test Cases Failed Today: {totalFailedTestCasesToday}</li>
<li>Total Test Cases Completed: {totalCompletedTestCases} ({totalCompletedTestCases%})</li>
<li>Total Test Cases Remaining: {totalRemainingTestCases} ({totalRemainingTestCases%})</li>
</ul>
<h4>Vulnerabilities Found Today</h4>
<ul>
<li>Total: {totalVulnsToday}</li>
<li>Critical: {totalCriticalVulnsToday}</li>
<li>High: {totalHighVulnsToday}</li>
<li>Medium: {totalMediumVulnsToday}</li>
<li>Low: {totalLowVulnsToday}</li>
<li>Info: {totalInfoVulnsToday}</li>
</ul>
{/stopTesting}
<p>For more information on testing progress please visit {link}.</p>
```

When the Daily Start Testing email is sent, it will appear as follows:

![](/files/fPVyLYUBAXd7rSjnq23b)

And when the Daily Stop Testing email is sent, it will appear as follows:

![](/files/4ZSw6ioB8FNPaQDVeKSF)

## Invite Project Team Members

You can invite your project team during the project creation process.&#x20;

You can invite as many users as you need, all in one go. You can define the following for each project team member:

* **Access Level**
  * Set the access level for the user on the project. This can be either View, Upload & Edit.
  * Full details for each access level are displayed in the info panel, and can also be found in the [Access Control Matrix](https://support.attackforge.com/attackforge-enterprise/access-control-matrix)
* **Project Role**
  * Set the users' project role on the project e.g. pentester, customer, developer, etc.
  * Project role is used for collaborative purposes only. Privileges are managed via access level (see above)
* **Notification Events**
  * Set the emails which the user will receive on the project.
* **Test Suites**
  * Assign the user to a test suite. The user will be assigned to each of the test cases loaded on the project for the given test suite.

<figure><img src="/files/Xqev0PDDJkSS3oSamtdH" alt=""><figcaption></figcaption></figure>

## Features

You can configure different features, and access to those features, on your projects. For example:

* **Test Cases** - You can configure the lowest project access level required in order to see [Test Cases](https://support.attackforge.com/attackforge-enterprise/getting-started/test-cases). You can also configure which application user roles can get access to Test Cases on the project.
* **Retesting** - You can enable or disable [Retesting Rounds](https://support.attackforge.com/attackforge-enterprise/getting-started/retesting#request-a-retest) on the project. You can also configure the lowest project access level required in order to see Retesting Rounds. You can also configure which application user roles can get access to Retesting Rounds on the project.
* **Attack Chains** - You can enable or disable [Attack Chains](https://support.attackforge.com/attackforge-enterprise/getting-started/attack-chains) on the project. You can also configure the lowest project access level required in order to see Attack Chains. You can also configure which application user roles can get access to Attack Chains on the project.
* **Reporting** - You can enable or disable [Reporting](https://support.attackforge.com/attackforge-enterprise/getting-started/reporting) on the project. This affects both reports which can be downloaded as well as the Executive Summary. You can also configure the lowest project access level required in order to see Reporting. You can also configure which application user roles can get access to Reporting on the project.

> !IMPORTANT: Test Cases cannot be disabled on projects (for now).
>
> !IMPORTANT: Project Coordinators and Administrators will always have access to all features on their projects. You cannot restrict features for these user roles.

## Reviewing & Creating Project

After you have completed setting up the project, you can review the project to ensure all the details are correct, then proceed to create the project. Once the project is created, you will be directed to the project dashboard page.

<figure><img src="/files/fghF1MiPfwEf5SJI3erE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/X8iSWVMgENTzG9zxo9bS" alt=""><figcaption></figcaption></figure>

## Updating a Project

After your project is created, you will be redirected to the Project Dashboard page. This is you control center for your projects. From here you can access all project areas.

If you would like to update your project, you can click on the cog in the top-right of the page to enter the settings.

<figure><img src="/files/kAh6bKSj4dbZ27gUkGzO" alt=""><figcaption></figcaption></figure>

## Place Project On-Hold / Off-Hold

A project may be set to On-Hold or Off-Hold at any time. This can be used when there are issues which are preventing the project from progressing further, and recording when these issues have been resolved.&#x20;

When a project is placed on-hold or off-hold, an email will be sent to the project team with the reason why, and the project status will be updated.

<figure><img src="/files/WUdHIsp7oemIRyzFEvBv" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/aAUyb0hFbM2fzsvIMxEv" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/RqkeSRYQVqgW1Fni1DXV" alt=""><figcaption></figcaption></figure>

Every time a project is placed on-hold or off-hold, the details are logged in the project tracking section. You can access this by clicking on `Tracking` button on your project dashboard page.

<figure><img src="/files/pcTycFmb3hWq9aGfxpzx" alt=""><figcaption></figcaption></figure>

## Cloning Projects

If you are an Administrator or Project Coordinator - you can clone an existing project. This is an effective way to:

* Prepare for a new round of testing
* Track vulnerabilities for specific assets across projects
* Focus retesting on open vulnerabilities

When cloning a project, the new project will get access to:

* Project settings, which can be adjusted for the new project - this includes name, codes, test suites, scope, email templates, portfolios, custom fields & project team
* Project workspace, included all notes & files previously uploaded / created
* Project notes previously created (excluding private notes)
* Executive summary, including uploaded files

You can also select which vulnerabilities (if any) you would like to carry forward into the new project.&#x20;

To start, you can select `Clone Project` option from project module actions menu.

<figure><img src="/files/b0njbU436hbmUOFVr67u" alt=""><figcaption></figcaption></figure>

### Step 1: Configure New Project

Review & adjust settings for new project.

<figure><img src="/files/rpU0nlqNd6xh53HYcRtr" alt=""><figcaption></figcaption></figure>

### Step 2: Select Vulnerabilities

Select any vulnerabilities from previous project you would like to bring into this new project.

> !IMPORTAN&#x54;**:** When cloning vulnerabilities, keep in mind:\
> \- ***Vulnerabilities are not transferred.*** Vulnerabilities will become available in the new project, and will also remain available in the source project.\
> \- ***Vulnerabilities are not copied.*** This means there will be no duplication of vulnerabilities.\
> \- ***Vulnerabilities are universal.*** Any changes to these vulnerabilities in the source project, will also apply to the new project, and vice-versa.\
> \- Assets assigned to each vulnerability will be added to the new projects' scope.

<figure><img src="/files/LpPVQpK5ajEbTeSdOUnV" alt=""><figcaption></figcaption></figure>

### Step 3: Select Options

Select from different cloning options available.

<figure><img src="/files/OI5BIlcOXUF4rS9PzEOa" alt=""><figcaption></figcaption></figure>

Once you are done, simply click `Review and Create Project` and your new cloned project will be ready.

## View Project Logs

If you are an Administrator or Project Coordinator - you can view project logs to see every recorded event on the project. This can help you to troubleshoot or report on project activities. You can view logs from the project settings page.

<figure><img src="/files/IajqOZkKxiw4fvDruzCs" alt=""><figcaption></figcaption></figure>

## Delete Project

If you are an administrator, you can delete your project at any time by first selecting `Archive Project` from the projects module actions menu. You will receive a confirmation prompt to authorise this.

<figure><img src="/files/3OIA2HqPHNLzzgJkmYJW" alt=""><figcaption></figcaption></figure>

All archived projects will be sent to the projects archive. You can access the project archive from Projects module page, by selecting `Archived Projects`.

Archived projects are hidden for non-admin users. Therefore any vulnerabilities inside archived projects will no longer be included in dashboards, analytics or other sections with AttackForge.

Archived projects can be restored at any time by selecting `Restore Project` from the actions menu.

You can also permanently destroy projects. This can be performed using the `Destroy Project Data` options from the action menu.

> **!WARNING** - once a project has been destroyed, there is no way to recover it or its data.

<figure><img src="/files/CWk6APuLUwiBHzEj4Lwv" alt=""><figcaption></figcaption></figure>


# Invite User To Project

## Overview

AttackForge lets you invite any person to your project who may need to collaborate with you or the project team; or to provide access to the vulnerabilities, reports, testing progress & tracking features.

In AttackForge, vulnerabilities are linked to one or more projects.&#x20;

With the exception of Administrators, a user can only see vulnerabilities for which they have access to the project(s) linked to those vulnerabilities.

Only an Administrator, Project Coordinator or delegated user can invite a user to a project and manage their access levels.

Start by clicking on the project settings button (cog in the top-right) then select `Access`.

<figure><img src="/files/1m1wDJgpzOLGZVGSezGh" alt=""><figcaption></figcaption></figure>

This page will show you all the users with access to your project, and their associated access level & project role.

You can update a user's access to your project at any time by clicking on their current access level (in the table) and selecting from any of the options in the drop-down menu.

You can update a user's project role also by clicking on the current role (in the table) and selecting from any of the options in the drop-down menu.

You can also remove access to your project for a user at any time by clicking actions menu and selecting `Remove Member`.

## Inviting Users

You can also click on `Add User` button to invite a new person to your project.

You can select from an existing user in the system. To help you with scheduling, you can view which consultants are available (not assigned on an overlapping project) during the project test window (start & end dates). You can also view the calendar for projects you have access to, and also view all projects assigned to any user.

<figure><img src="/files/FDVzSEhFND3bLP8Xmlrv" alt=""><figcaption></figcaption></figure>

If the user does not yet exist in your AttackForge, you can invite them by their email address.

<figure><img src="/files/g8y8S1JABg5aGWKHHCjS" alt=""><figcaption></figcaption></figure>

## **Access Roles**

Projects in AttackForge have three (3) levels of access control that can be assigned to any user. This controls the user's ability to see vulnerabilities & data from the project within AttackForge, and also how they can interact with that project.

For a detailed breakdown of every project-level privilege - please see [Access Control Matrix](https://support.attackforge.com/attackforge-enterprise/access-control-matrix)

### **View**

This is the typical privilege assigned to `Clients/Customers`. It provides them with all of the necessary permissions to view details about vulnerabilities, generate on-demand reports, flag vulnerabilities for retesting and request retests, and self-export vulnerabilities to tools.

### **Upload**

This is the typical privilege assigned to `Developers / Engineers`. It provides them with the same privileges as View, however also provides ability to upload files to the projects' workspace as well as create project notes. This is useful for users who would need to share information with the security team, for example test accounts, solutions documents, files, etc.

### **Edit**

This is the typical privilege assigned to `Pentesters / Security Consultants`. It provides them with the same privileges as Upload, however also provides ability to create vulnerabilities, manage scope, action test cases, create attack chains, update executive summary, and perform retests.

## Who Can I Invite To My Project?

You can invite any person to your project by selecting the user from the system or entering in their email address for new users. Please ensure you have consent from the person before inviting them.

The user will receive an email notification informing them you have now invited them to the project and they can access the project immediately.

## Project Roles

Project Roles can be assigned to any user on the project. The roles do not provide the user with any additional access rights. Privileges on the project are controlled via the Access Roles (see above).

Project Roles are displayed on the project dashboard so that other team members can see who is on their project and also their role on the project - to help with collaboration and contacting the right person.

<figure><img src="/files/Ffe5fVoEEKdlguKr0sb4" alt=""><figcaption></figcaption></figure>

Project Roles are also included in the reports, alongside the project team member's name.

## Email Notifications

Project Team members can receive emails related to events on the project, for example testing has started/stopped or new vulnerabilities have been found.

The following email notifications can be configured per project team member:

* All Emails
* No Emails
* Daily Start/Stop Testing Email
* New Critical Vulnerability Discovered
* New High Vulnerability Discovered
* New Medium Vulnerability Discovered
* New Low Vulnerability Discovered
* New Info Vulnerability Discovered
* Vulnerability Ready for Retesting
* Vulnerability Re-Opened
* Vulnerability Closed
* Project Role Has Been Updated
* Project is On-Hold / Off-Hold
* Retest Has Been Completed

> **!IMPORTANT**: make sure you have enabled the relevent email notifications first in the `Project Settings`.

## Group Access

Users can inherit access to projects automatically via Groups. In order for this to apply, the group needs to first be linked to the project either during project creation or when editing the project's settings.

Once a group has been linked to the project, any of the access settings applied to the group members will become available. This can be managed via the group settings.

In the example below, if the *Globex Corp.* group was linked to the project - the following access will become available:

* **Project Manager** - will gain Edit access to the project
* **Joe Pentester** - will gain Upload access to the project
* **Client User** - will gain View access to the project
* **Account Manager** - will not receive access to the project via this group

<figure><img src="/files/LFuTQgA1fAVXfDgJhIkC" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/LpFnrTYieCMt229J5gZf" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/1RUcQb1hKmzFbXYgOYkb" alt=""><figcaption></figcaption></figure>

## Member Admins

Member Admins are users or groups who have been delegated privileges on this project to be able to manage the project team.

This can help to:

* Empower customers and engineering teams to provide access to the project and vulnerabilities on a needs basis.
* Delegate project team management to persons better suited for the role.
* Save time and effort on project team management.

<figure><img src="/files/0qSHwDoTSvzfA62mEloi" alt=""><figcaption></figcaption></figure>

When assigning a user or a group as a member admin on the project, you can configure the following:

* **Access Level Limit**: Set the highest level of access the user or group is allowed to assign to project team members.
* **Add User Method**: Configure how project team members can be added - either by selecting an existing user from a drop-down list, or by entering in an email address.
* **Allow User Invite**: Allow user to invite new persons to your AttackForge tenant.

Member Admins will inherit View access to the project by default.

<figure><img src="/files/gkSxvCnzwapeUDUXh7do" alt=""><figcaption></figcaption></figure>


# View Project Team

## Overview

After you have invited all the persons to your project, you can view your project team from the project dashboard page in the `Details` section. You can click on the users name to see their profile information.

<figure><img src="/files/d2p0G7bfeBW4UZ6cfLAQ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/vD7A8p9xDd827URFcKM4" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/7chqZBW3dgO1TtaEVNaD" alt=""><figcaption></figcaption></figure>

You can also view additional information on the project team via the `Access` section in the project settings.

<figure><img src="/files/LUiEuUEbC0QGkNHCetcr" alt=""><figcaption></figcaption></figure>


# Project Scope

## Overview

Project Scope is where all of the assets assigned to the project can be viewed or modified. You can also add new assets to the project.

Only Administrators, Project Coordinators or users with Edit access to the project can make updates to the scope on the project.

You can access the project scope by selecting `Scope` from the project menu.

<figure><img src="/files/knxslqYkvuxLwJVZ8WEV" alt=""><figcaption></figcaption></figure>

### Add Assets

To create a new asset (or multiple assets) on the project, select `Add Assets`.&#x20;

<figure><img src="/files/G9qxOGpeGyOesuu0byOT" alt=""><figcaption></figcaption></figure>

If you are using the Assets module, you will have the option to select existing assets from the library. You can also create new assets in the library from here as well.&#x20;

<figure><img src="/files/dECBY8ltgdo89uaMRxuu" alt=""><figcaption></figcaption></figure>

If you are not using the Assets module, you can enter your assets directly and configure your delimiter settings to handle multiple assets.&#x20;

<figure><img src="/files/6S2NalAGck7BJBaI6vmc" alt=""><figcaption></figcaption></figure>

### Modify Assets

If you are not using the Assets module, you can modify assets by clicking on `Edit` in the actions menu. A box will be displayed around the asset name allowing you to change it.&#x20;

<figure><img src="/files/TgkDBKvF6nSlWAHnxbCW" alt=""><figcaption></figcaption></figure>

If you are using the Assets module, you must first delete the asset, then add a new asset.

### Delete Assets

You can delete assets by clicking on the `Remove` button. You can also bulk delete assets by selecting them then clicking on `Actions -> Remove`

> **!IMPORTANT**: You can only delete assets which are not already linked to an existing vulnerability on the project. If you attempt to do so, you will see an error message displayed. You must first either delete the vulnerabilities, or re-assign the vulnerabilities to a different asset, then you can delete the asset once it's no longer assigned to any vulnerabilities on the project. To re-assign vulnerabilities to different assets - please see [Updating Affected Assets](https://support.attackforge.com/attackforge-enterprise/getting-started/updating-vulnerabilities#update-affected-asset-on-a-vulnerability).

> **Tip:** You can select all assets using the menu, and then delete all. This will remove any assets which are not already associated with a vulnerability. This can help to clean up your scope to focus on IPs which only have vulnerabilities - if required.

### Import Assets

You can import assets directly to your projects from your `NMAP` and `Masscan` files, as well as generic JSON and CSV options.

<figure><img src="/files/9or16o2PGPYYOeeONRov" alt=""><figcaption></figcaption></figure>

You can also take advantage of the additional Hostnames and Ports fields if you are using the Assets Module.

These fields will be stored against the Asset in the module, so you can monitor and manage Hostnames and Ports centrally (outside of your projects).

<figure><img src="/files/EO5n9RzLq7fFz1l0gSYq" alt=""><figcaption></figcaption></figure>

You can also view and modify the data prior to importing.

<figure><img src="/files/GWBYl83TnwJh9FUzPAA4" alt=""><figcaption></figcaption></figure>

### Download Assets

You can download all of the assets assigned to the project in CSV format. This may help to get this data into other tools such as vulnerability scanners. You can perform this by clicking on `Export as CSV`.

<figure><img src="/files/NOE0PCrpsXn9oKB6mQlv" alt=""><figcaption></figcaption></figure>


# Project Workspace

## Overview

Every project in AttackForge has a dedicated workspace where notes can be entered and files can be uploaded. The workspace allows project teams to consolidate all entry criteria required for an assessment, and place to store testing logs once the assessment has completed.

You can access the workspace from the project menu by clicking on `Workspace`.

<figure><img src="/files/DcrP0UrH3lOKY5zH3vGJ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/BjLVF5rct3ZBoI1i3tj3" alt=""><figcaption></figcaption></figure>

Only users with `Upload` or `Edit` permissions on a project can access the workspace.&#x20;

Edit users can see all uploaded files & notes. Upload users can only see the uploaded files.

Authorized users can create free-form notes in the workspace, which are relevant to the assessment.

For example:

* Test credentials
* Link's to solution interfaces or API specifications
* Notes relating to system functionality
* Steps on how to extract & set up solution
* Etc.&#x20;

You can assign each of the notes to project scope assets and also upload files directly to the notes.

Authorized users can also upload files to the project workspace and testing logs section.&#x20;

For example:

* Binaries for mobile applications
* API test harnesses
* Solution design and architectural documentation
* Codebase for source code review
* Etc.


# Project Notes

## Overview

Project Notes allows to create & store notes on your project. You can use notes to capture information relating to your pentest. You can consolidate all your project notes in one place, to make it easy to track & record information as you go.

The notes can include:

* **Private notes** - these are notes which are only visible to you.
* **Public notes** - these notes are available to project team members with Edit access to the project (pentesters/consultants).
* **Report notes** - these notes are included in the downloaded reports.

Project Notes are only available to users with `Edit` or `Upload` permissions to the project.

Edit users are able to see all public notes. Upload users can only see notes they created.

You can access project notes from the project by clicking on `Notes`.

<figure><img src="/files/Bs4Gz9ca0u0nFxzIay72" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Ub152jkLF128vK9jq1B1" alt=""><figcaption></figcaption></figure>


# Project Pages

Project Pages can be used to create, group and manage custom content on your projects.

Pages are a dynamic way to interact with your project teams. Their content can also be used in reports and in the Self-Service APIs.

Here's some examples for how you can use Pages:

* Create dedicated places for project teams to enter and track information
* Provide additional options for teams to collaborate
* Group content into topics

> Each page comes with its own access controls to manage who can View, Upload or Edit content on the page.

## Summary Page

The Summary Page can be used to capture, track and manage additional information related to the project. For example:

* Customer Goals for the testing team, and Testing Team goals for the customer
* Assumptions, Constraints and Limitations
* Scope-related notes such as Out-of-Scope

You can enable this page from the `Administration` module, and set the default access levels for new projects:

<figure><img src="/files/iay39R9M5GkkfrXR8HIE" alt=""><figcaption></figcaption></figure>

Start creating **Sections** and **Fields** you want to use on your projects.

<figure><img src="/files/qjq5nhdZvKV5z69uAI3b" alt=""><figcaption></figcaption></figure>

Now on your existing or new project, enable the **Summary** page.&#x20;

Add access levels based on the needs of your project.

<figure><img src="/files/o3bNO174aKPz80Fzng2w" alt=""><figcaption></figcaption></figure>

Access the Summary page from the menu.

<figure><img src="/files/SCsCAkWfjYjbOFYs8SXb" alt=""><figcaption></figcaption></figure>

Enter information and upload files based on your configuration.

<figure><img src="/files/pj9yACNwsElbPR9FDFio" alt=""><figcaption></figcaption></figure>


# Test Cases

## Overview

When creating a new project, you can select one or more testing methodologies or checklists to apply to the project - referred to as Test Suites.  Each test suite has a collection of test cases which gets assigned to the project.

<figure><img src="/files/jwGHhzw1J72NypsoGL3C" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/tyDMpMH4oai3D2zdivEa" alt=""><figcaption></figcaption></figure>

A test suite helps:

* Clients understand exactly what was tested on the project;
* Developers/Engineers link test cases to vulnerabilities;
* Pentesters structure their testing in a methodical, consistent & standardized way;
* Organizations create repeatable, standardized & comparable assessments - independent of whom was actually performing the assessment.

Test cases can provide valuable insight into a penetration test or audit. It shows:

* What was tested
* When it was tested
* Who tested it
* What was the status
* Supporting external (customer facing) notes
* Supporting internal (security team) notes
* Supporting evidence
* Whether test case passed, failed or remediated

To view the test cases assigned to the project, click on `Test Cases` from the project menu.

## Updating a Test Case

It is the function of pentesters on the project to update the test cases as they work through the assessment. Therefore only users with Edit permissions on the project can update a test case.

Test cases by default are set to `Not Tested`.

Authorized users can update the status of a test case to any of the following:

* Not Tested
* Testing In Progress
* Tested
* Not Applicable

You can create your own sub-status for test cases using custom fields via `Administration -> Projects -> Test Cases`

You can update a test case by clicking on the status of the test case and selecting an option from the drop-down menu.

<figure><img src="/files/WVSItITbDXBaMZYTGThy" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/hh8HDizAh5qir7uzVKT9" alt=""><figcaption></figcaption></figure>

You can also update multiple test cases in bulk. Select multiple test cases, then select the desired option.

<figure><img src="/files/fFVw8k0XIybE8PFD2Edj" alt=""><figcaption></figcaption></figure>

## Evidence

Each test case can have supporting files which contains the evidence of assessment.

<figure><img src="/files/Uxmv9FkpZ8yUe4UpJLQz" alt=""><figcaption></figcaption></figure>

## External Notes

Each test case can have supporting external (customer facing) notes & files which contains the observations from the pentester.

<figure><img src="/files/a1i87R0ipClcepxqha2I" alt=""><figcaption></figcaption></figure>

## Internal Notes

Each test case can also have supporting internal (security team) notes & files which contains the observations and artefacts from the pentester.

Internal notes are only visible to users with Edit access to the project.

> For example, if the test case required to perform a scan using a tool - the results of the scan can be uploaded to the internal notes

<figure><img src="/files/eUR72ZuWwlSroa1utUde" alt=""><figcaption></figcaption></figure>

## Execution Flows

Execution flows can be assigned to each test case.

Execution flows can have many uses such as:

* Document steps and procedures guiding a person in how to perform the test case
* Document which tools should be used to perform the test case
* Document internal processes and procedures required by the test case
* Provides links to external resources

<figure><img src="/files/DCDHmgzBZBZZlHMRrjhZ" alt=""><figcaption></figcaption></figure>

## Tailoring Test Cases to Projects

You can tailor your test cases to projects using Custom Fields.

Here's an example of a test case for a `Web Application` pentest:

<figure><img src="/files/I7dSgHos7I5ttWnzDaoo" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/iUpwSZO7oHkMgX75zMwI" alt=""><figcaption></figcaption></figure>

However for a `Purple Team` assessment, the test cases will need to be structured differently to accomodate fields for `MITRE ATT&CK` mappings, `Red Team` sections and `Blue Team` sections.

<figure><img src="/files/hk0On3ouTxvYaaKWEXQN" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/GCYygeyDgsYExZFWJbVB" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TKISLOZAn2H3KFB7eMZC" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/5OXvJCv5Zs5Xunl8UIQw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/W5oS9Ebd2UbDstjUviSN" alt=""><figcaption></figcaption></figure>

You can define your Red Team and Blue Team sections and custom fields in `Administration -> Projects -> Test Cases`

<figure><img src="/files/X7tQUEDkNYcjlHufOWTU" alt=""><figcaption></figcaption></figure>

You can use `Hide Expressions` to configure when those sections and fields should be visible, for example only on Purple Team projects:

<figure><img src="/files/f9gKRQFT1xOYEHPx8swM" alt=""><figcaption></figcaption></figure>

You can also configure `Custom Field Access Controls` to determine who can View or Edit the field.

<figure><img src="/files/SneqAizLsjo1Brww1BbH" alt=""><figcaption></figcaption></figure>

## Assign to a User

Administrators and Project Coordinators can assign test cases to any project team members with Edit permissions on the project. This helps to delegate tasks to team members to maximise efficiency during testing, as well as accountability for certain tasks.

You can assign test cases to a user by selecting the test cases and using the actions menu to assign them to a user.&#x20;

<figure><img src="/files/RITCTanKDUXyGxLmSkqo" alt=""><figcaption></figcaption></figure>

## Assign to Assets

Administrators and Project Coordinators can assign assets to test cases. This helps to delegate tasks to individual assets to increase testing coverage and traceability.

You can assign one or more assets to the test case.

<figure><img src="/files/hDUvC1FgdzW4J3cCKy9r" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/JJDfsFdugzMD8dLFltIz" alt=""><figcaption></figcaption></figure>

## Passed, Failed & Remediated

Test cases will automatically show as Passed, Failed or Remediated.

* **Passed test cases** are test cases which have no linked vulnerabilities (no findings)
* **Failed test cases** are test cases which have one or more open linked vulnerabilities (findings discovered and haven't been closed)
* **Remediated test cases** are test cases which have one or more linked vulnerabilities, and all of them are closed (findings discovered and have all been addressed)

<figure><img src="/files/yIySw51JohVUtr4R5fiw" alt=""><figcaption></figcaption></figure>

You can fail a test case by linking a vulnerability to a test case.

When creating or updating a vulnerability on a project, select the failed test case(s) to link them.

<figure><img src="/files/9fgb9ao7CY7wrqtZKmiY" alt=""><figcaption></figcaption></figure>

You can add a vulnerability directly from the test cases page, to quickly link the test case to the new vulnerability.

<figure><img src="/files/jDC63Cfv4hAOxTngq5JJ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/9lLRvqlfOnZMSnWR4YwC" alt=""><figcaption></figcaption></figure>

When a vulnerability is linked to a test case, the test case will be automatically marked as failed.

You can click on the Vulnerabilities tab to see all linked vulnerabilities.

If all vulnerabilities linked to a failed test case have been Closed, the test case will be considered Remediated.

<figure><img src="/files/B8vTFyo6Cx1B6hjHbrkV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/HlZnDTlb7X3yOlsfNbtc" alt=""><figcaption></figcaption></figure>

## Locking & Unlocking Test Cases

As an Admin or Project Coordinator, you have the ability to Lock and Unlock test cases on a project at any given time.

Locking test cases is useful if you need to prevent test cases from being altered or tampered with.

When a test case is locked, it cannot be updated. You cannot add any new notes or evidence either. This provides greater assurance from an auditing perspective.

Locked test cases will not show up on or affect the project status and percentage completion.

Locked test cases will not show up in the reports as reporting is focused on the active test cases.

To lock a Test Case - select the test cases then click on `Lock` from the actions menu.

<figure><img src="/files/dUrGzNaWpULvdde5dvtE" alt=""><figcaption></figcaption></figure>

To unlock a Test Case - click on `Show Locked` button, then select test cases and click on `Unlock` in the actions menu.

## Adding Test Suites

As an Administrator or Project Coordinator, you can add additional test suites to a project after the project has been created.

To add new test suites on a project, click on `Add -> Test Suites`.

<figure><img src="/files/8XSmN4bFaJfdKhD8uDbo" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/1rpohRpznu83CZY6nEky" alt=""><figcaption></figcaption></figure>

By default, the test cases loaded on to the project will be set to Unlocked/Active status.

You can automatically lock the previous test cases if required. This will ensure the previous test cases can’t be tampered with or changed accidentally. It will also reset the project status to *Waiting to Start* and progress will be set to 0% (based on the new test suites added).

## Deleting Test Cases

As an Administrator or Project Coordinator, you can delete test cases on a project. This can help if you need to remove test cases which do not need to be actioned on the project.

To delete test cases on a project, select the test cases then click on `Delete` from the actions menu.

<figure><img src="/files/hBvIuBGOwArWXDqdJ0jo" alt=""><figcaption></figcaption></figure>

## Creating Abuse Cases

Abuse cases are project-specific test cases. They are unique test cases which apply to the project only, or objective of the assessment.&#x20;

For example, consider a web application pentest for a reverse auction website. Typically the pentest may cover the standard OWASP ASVS test cases, however the customer also requires that business logic tests are performed against the bidding functionality to determine whether it can be cheated or not. Abuse cases can be created to specifically test this functionality and provide higher level of assurance beyond standard test cases.

To create abuse cases on the project, you must be either an Administrator or Project Coordinator.

Click on `Add -> Abuse Case` andEnter in the details for the project specific test case.

<figure><img src="/files/KDh9FdBxaYKrSQMfDYAC" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/u0PUaLP5en29LujZYTj9" alt=""><figcaption></figcaption></figure>

You can access all abuse cases for each project via the `Test Suites` module. You can make changes to the abuse cases from here. If you need to delete an abuse case, you can perform this directly from the project (see `Deleting Test Cases` above).

<figure><img src="/files/9TASwvlipqTD2sNm4nJM" alt=""><figcaption></figcaption></figure>


# Creating Vulnerabilities

## Overview

Vulnerabilities in AttackForge get created on projects. A vulnerability is linked to either one or more projects.

With the exception of Administrators, a user can only see vulnerabilities for which they have access to the project(s) linked to those vulnerabilities.

Every project can have up to 10,000 vulnerabilities. This limit helps to ensure reports can be created for the project.

When creating a vulnerability in AttackForge, we assign it to an asset on the project scope.

There are two (2) methods you can use to create a vulnerability:

1. **Add Vulnerability**
2. **Import Vulnerabilities**

Considering that adding vulnerabilities is part of the role for a pentester - only users with Edit access to the project can perform this function.

<figure><img src="/files/UFS8g9GXkgksd0PdAPIj" alt=""><figcaption></figcaption></figure>

## Adding Vulnerabilities

To add a vulnerability on a project, click on `Create Vulnerability` from the project menu.

<figure><img src="/files/psLHSY0n5sOrQSwIbcIA" alt=""><figcaption></figcaption></figure>

### 1. Writeups Library

Writeups are knowledge-bases for your vulnerabilities. Writeups make it easy to search and reuse vulnerability definitions that you or your teams have previously created in a Writeups Library - saving you time and effort on recreating what has already been done before.

When creating a vulnerability, you will be able to select from Writeups in Writeups Libraries for which you have access.

* **All Writeups** - contains all the Writeups you have access to across all your libraries.
* **Main Vulnerabilities** - contains the majority of the vulnerability writeups.
* **Imported Vulnerabilities** - contains writeups for vulnerabilities that have been imported from tools and scanners.
* **Project Vulnerabilities** - is where you can define and create project-specific writeups. This is useful if the vulnerability contains information you do not want shared with other users outside of your project. It also helps to create derivatives of other writeups which are tailored towards your project.
* **Custom Libraries -** any custom writeups libraries which you have access to.

<figure><img src="/files/XCh4qDVyWHMt4C3nU8VW" alt=""><figcaption></figcaption></figure>

### 2. Writeup

Select the writeup from the chosen library. Hovering over an entry in the library will show you the details in the right-hand side. You can use keywords to search your library.

<figure><img src="/files/G6LOqhbowFfPbaM1YQjJ" alt=""><figcaption></figcaption></figure>

If you cannot find a writeup you wish to use, you can create a new entry in the relevant libraries by clicking on `Create Writeup` button.

<figure><img src="/files/PFGJJVPKtGWHBEk2l773" alt=""><figcaption></figcaption></figure>

### 3. Affected Assets

Select one or more assets which are affected by the vulnerability.&#x20;

You can choose from `Individual` or `Grouped` options:

* Create *unique vulnerabilities* on every project, and assign relevant affected assets to each unique vulnerability
* Create individual vulnerabilities for every asset
* Create a combination of unique vulnerabilities and individual vulnerabilities – for ultimate flexibility!

> Individual option will create a seperate vulnerability for every affected asset

> Grouped option will create a single vulnerability and link all affected assets to that vulnerability

Using grouped option can help you to:

* Increase efficiency when working on infrastructure penetration tests
* Reduce the overall number of vulnerabilities whilst preserving affected asset data
* Reduce effort required for quality review cycles on vulnerabilities

<figure><img src="/files/MStSddz1r8qLV0wgQKm5" alt=""><figcaption></figcaption></figure>

When using grouped option, every asset can have its own notes, tags, and affected components.

Components can be used to track which part(s) of the asset has the vulnerabilities, for example you can include URLs, ports, line of code, etc.

Every component can also have its own notes and tags.

<figure><img src="/files/Vr2ckFEZGOYPfxe2xpSo" alt=""><figcaption></figcaption></figure>

When using the grouped option, every asset can be individually tracked and actioned.

This is useful for monitoring the progress against assets on a vulnerability.

<figure><img src="/files/zHeLq4ettjQe4S10rshB" alt=""><figcaption></figcaption></figure>

### 4. Visibility

By default, vulnerabilities are set to be immediately visible. This means any project team member can see the vulnerabilities right away. This is by design, to help information flow faster to the right people and to reduce Time-To-Remediate (TTR).&#x20;

However, you can choose to set visibility to `Pending` which will place the vulnerability in the draft/pending state.&#x20;

Only users with Edit access on the project will be able to see pending vulnerabilities - for quality review, tech review, peer review.

<figure><img src="/files/tDw03O4CyhooViY1k63Y" alt=""><figcaption></figcaption></figure>

### 5. Scoring

If you are using `manual scoring` - you will have option to manually select the Likelihood of Exploitation (from 1 \[*difficult to exploit*] to 10 \[*trivial to exploit*]) and Priority (Critical / High / Medium / Low / Info).&#x20;

If you are using the `CVSS scoring`, you will see an in-app calculator which will help you easily determine the CVSS score which will automatically set the Likelihood of Exploitation and Priority accordingly, including adding CVSS scores + CVSS vector string as tags.

<figure><img src="/files/3VC8Acp2THRBrBUZArKV" alt=""><figcaption></figcaption></figure>

### 6. Steps to Reproduce (Proof-of-Concept)

Include detailed steps to reproduce the issue. This is a rich-text field so you can include HTML payloads.

<figure><img src="/files/T7X5X6RyqnyEkUOnpxS9" alt=""><figcaption></figcaption></figure>

### 7. (Optional) Notes

You can include additional notes which relate to the finding. This is an optional field. You can add as many notes as needed.

<figure><img src="/files/8pSFgSUplnd5YDRyPScn" alt=""><figcaption></figcaption></figure>

### 8. (Optional) Custom Fields

Depending on how you have configured your vulnerability form for the current project, you may have other custom fields or section that need to be completed. You can customize your vulnerability form from the `Administration` module.

<figure><img src="/files/qkSBApAyN9PEAZ5XlGIG" alt=""><figcaption></figcaption></figure>

### 9. (Optional) Modify Tags

Tags will be assigned based on the writeup. However you can add additional tags if required.&#x20;

If using CVSS scoring - additional tags related to CVSS are automatically created for you.

You can also create custom tags which support arbitrary key/value pairs.

<figure><img src="/files/b9Sdadn9LeQpc4h2cXlw" alt=""><figcaption></figcaption></figure>

### 10. (Optional) Link Test Cases

Where possible, link test cases to the vulnerability. This will help developers / engineers better understand what you were testing when it lead to discovery of this issue, which in turn provides knowledge transfer to help them avoid making same mistakes in the future.

You can link multiple test cases to a vulnerability.

<figure><img src="/files/aCJEqpTQNg38Q5s6H3ng" alt=""><figcaption></figcaption></figure>

### 11. (Optional) Evidence

Upload any files and supporting evidence. You must first create the vulnerability.

<figure><img src="/files/4D7FwuQqVALg8P5bbixH" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/IsecIrofYMfJ9Fds9Erj" alt=""><figcaption></figcaption></figure>

If you want the screenshots to appear in-line when you view the vulnerabilities or in reports, you can click `Add to Steps to Reproduce` button or use shorthand syntax:

`{{{FILE_NAME}}}`

<figure><img src="/files/j3RgIkYYWcdiVrKfpf1Y" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Yyu36v1Bso5vEVYEK5gj" alt=""><figcaption></figcaption></figure>

The vulnerability will now be created and assigned to the affected assets on the project.

<figure><img src="/files/aVrosKaxK7ZJnUOvitA8" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/iD95pH10KgvVFwlkp6el" alt=""><figcaption></figcaption></figure>

## Project-Specific Vulnerability Writeups

Sometimes you might want to create a vulnerability using a writeup which is specific to the project. This is useful in the following scenarios:

* You have a 'boilerplate' writeup, however you don't want to change it for everyone - you just want to create a tailored version for your project. For example, the Description or Remediation Recommendation needs to be tailored to the specific finding on the project.
* You want to create a writeup that only people on your project can see i.e. it's not shared in any other libraries for people outside your project to access.

The fastest and easiest way to do this is as follows:

1. Select the 'boilerplate' writeup that you wish to use as the starting point for this writeup

<figure><img src="/files/3MIRpDu4Jvzh0CkV0c8F" alt=""><figcaption></figcaption></figure>

2. Select `Duplicate > Project Library`

<figure><img src="/files/OVLnYJOsRMqYPPcBC3BM" alt=""><figcaption></figcaption></figure>

3. Add your modifications to the Writeup. Click `Update`.

<figure><img src="/files/jRpEpB2HNGrFuDoaU9Zz" alt=""><figcaption></figcaption></figure>

4. Observe the new writeup has been created against the project, and it has your project-specific details.

<figure><img src="/files/UiWOmE4s9EmMsPj1G86w" alt=""><figcaption></figcaption></figure>

You can of course create a new writeup from scratch directly into the Project Library if you do not have an existing boilerplate writeup to start from.

## Custom Vulnerability Form

You can tailor your vulnerability form to match the needs of your project.

For example, you can create a custom vulnerability form depending on the type of testing being undertaken on the project:

* Web Application
* Infrastructure
* Code Review
* Red Team
* Purple Team
* PCI DSS
* Etc.

The vulnerability will display the custom fields when viewing the vulnerability. For example, on a Red Team project - we can show extra Red Team related section and fields for the vulnerability:

<figure><img src="/files/rDUDb3KuFxkLZtscOYBj" alt=""><figcaption></figcaption></figure>

### 1. Configure Testing Types Project Custom Field

In your `Project Custom Fields` settings, create a multi-select custom field that will be used to assign the various types of testing on the project.

<figure><img src="/files/1J32fuDtwpEyaVxX15Zv" alt=""><figcaption></figcaption></figure>

### 2. Configure Conditional Custom Fields on Vulnerability Form

In your `Vulnerability Custom Fields` settings, create new fields and sections that will have a `Hide Expression` applied to only show those fields or sections under certain conditions, for example when the relevent testing type is selected on the project.

<figure><img src="/files/I1tyU6VcLoETFvFmBgZB" alt=""><figcaption></figcaption></figure>

Using the example above, we can set the following `Hide Expression` to only show this section when the `Red Team` testing type is selected on the project:

{% code overflow="wrap" %}

```
project.custom.testing_types === undefined OR project.custom.testing_types.length < 1 OR project.custom.testing_types.length > 0 AND project.custom.testing_types.indexOf("Red Team") < 0
```

{% endcode %}

This condition states the following:

* Hide this field when
  * No selection has been set for `testing_types` multi-select field on the project
  * The `testing_types` multi-select field on the project is currently empty (had selections before, but now all selections have been removed)
  * The `testing_types` multi-select field has values AND one of those values does not include the selection `Red Team`

Therefore, this field should only show when a `Red Team` selection has been made in the `testing_types` field.

### 3. Select Testing Type on Project

Create or Edit your project and select a matching `testing type` in your new project custom field, for example `Red Team`.

<figure><img src="/files/whHCIbwSZWQ8QS582I5G" alt=""><figcaption></figcaption></figure>

### 4. Create Vulnerability with Custom Form

Now when you create a vulnerability on the project, a new section will appear for `Red Team` related information.&#x20;

You can use this same approach to tailor your form to match the needs of your project.

<figure><img src="/files/1gBQCXBGQ6gjbeZimEOV" alt=""><figcaption></figcaption></figure>

## Importing Vulnerabilities

To import a vulnerability on a project, select Import Vulnerabilities from the project quick actions menu.

<figure><img src="/files/QlXtO8X1AA8gr008ru5e" alt=""><figcaption></figcaption></figure>

Select a tool you wish to import from, for example `Nessus`, `BURP`, `Qualys`, etc.

<figure><img src="/files/Htu9yKnXHkSCc9C1WET8" alt=""><figcaption></figcaption></figure>

After you select a tool, you will be prompted to select the output file from the tool in order to parse the data. See example below for Nessus.

<figure><img src="/files/cRRTsmmi2fQYoK6RaseJ" alt=""><figcaption></figcaption></figure>

Once the data has been parsed, you can then select the vulnerabilities you wish to import into your project.

<figure><img src="/files/YJxTan8mhm6ElEIYzDHH" alt=""><figcaption></figcaption></figure>

You will have a choice between selecting `Individual` or `Grouped`.

* **Individual** will allow you to import one vulnerability per affected asset.
* **Grouped** will allow you to automatically group affected assets for each vulnerability.

In the example above, there was a **94% reduction in vulnerabilities when choosing Grouped option**, whilst preserving the same amount of data.

This means you can focus your attention on the important vulnerabilities and track their affected assets much more efficiently.

You can view all of the affected assets, and for each asset – see related data for its affected components.

<figure><img src="/files/4L9kNYwVY9lvI0WBq2hg" alt=""><figcaption></figcaption></figure>

You can configure the Grouping options to adjust the rules for how the grouping is performed.

Once you have made your selection, you can move to the `Edit and Review` step.

Here you can see the final set of vulnerabilities for selection and make any remaining adjustments as needed prior to import.

<figure><img src="/files/uJ6lOVx9F5kcsFSeTJJU" alt=""><figcaption></figcaption></figure>

You can still choose to configure your import options such as dynamic parser actions and Writeup library for mapping.

Once your import begins, you will be kept update to date with its progress.

<figure><img src="/files/jK3mgNJqDX5WwdyDohnQ" alt=""><figcaption></figcaption></figure>

And once it’s finished, you will see a summary of the import and option to view the vulnerabilities.

<figure><img src="/files/j2kIul3ski3swCRZ3wVN" alt=""><figcaption></figcaption></figure>

When importing vulnerabilities, if a writeup does not exist in the library or cannot be mapped - it will be automatically created for you.

Similarly if the affected asset does not exist on the project, it will be automatically created for you.&#x20;

Vulnerabilities with grouped assets will now show in your vulnerability tables, with option to expand each vulnerability to see its affected assets data.

<figure><img src="/files/gtzxmdOVdld4OZYpyksU" alt=""><figcaption></figcaption></figure>

## Custom Import Mapping

AttackForge supports the ability to define a custom import mapping expression. This can be used to map your vulnerabilities to specific entries in your vulnerability libraries.

This powerful utility helps you to:

* Combine vulnerabilities into a single unique writeup
* Ensure your imported vulnerabilities are matched against known good and customer-ready write-ups&#x20;
* Speed up your quality assurance and review process
* Reduce the amount of duplicate and similar entries in your vulnerability libraries

For example, say you have three (3) vulnerabilities your wish to import from `Nessus` using the `Individual` option:

* **SSL Version 2 and 3 Protocol Detection**
* **TLS Version 1.0 Protocol Detection**
* **TLS Version 1.1 Protocol Detection**

However, you want to map these against one (1) single known-good writeup in your library which covers all various TLS related configuration issues:

* **Weak TLS Implementation**

You can achieve this as follows:

### Example 1: Custom mapping on Tags

```
vuln.tags.pluginID $in writeup.tags.pluginID
```

This expression works as follows:

* For every imported vulnerability (**vuln**), do the following
  * For every writeup in the selected library (**writeup**), do the following
    * Does the vulnerability have a tag 'pluginID' (**vuln.tags.pluginId**)?&#x20;
    * Does the writeup have a tag 'pluginID' (**writeup.tags.pluginID**)?&#x20;
    * Is one in the other? (vuln.tags.pluginID **$in** writeup.tags.pluginID)&#x20;
    * If Yes, map this imported vulnerability to the writeup in the selected library&#x20;
    * Otherwise, move to the next writeup or proceed to import vulnerability as per normal if no match found

Add the expression:

<figure><img src="/files/NUn0LKomnCIwBLP4uCYv" alt=""><figcaption></figcaption></figure>

Compare the pluginID tag in the vulnerability:

<figure><img src="/files/4HqOnqaARZW70L3mLLzL" alt=""><figcaption></figcaption></figure>

With the pluginID tag in the write-ups within the selected library:

<figure><img src="/files/icOys99Wma86hbZiUpOL" alt=""><figcaption></figcaption></figure>

The imported vulnerabilities which matched are now grouped by Weak TLS Configuration writeup:

<figure><img src="/files/L0Rw5WDUGSXVKVIys9aE" alt=""><figcaption></figcaption></figure>

### Example 2: Custom mapping on Custom Tags

```
vuln.custom_tags.tls_weakness === writeup.custom_tags.tls_weakness
```

This expression works as follows:

* For every imported vulnerability (**vuln**), do the following
  * For every writeup in the selected library (**writeup**), do the following
    * Does the vulnerability have a custom tag 'tls\_weakness' (**vuln.custom\_tags.tls\_weakness**)?&#x20;
    * Does the writeup have a custom tag 'tls\_weakness' (**writeup.custom\_tags.tls\_weakness**)?&#x20;
    * Do they match? (vuln.custom\_tags.tls\_weakness **===** writeup.custom\_tags.tls\_weakness)&#x20;
    * If Yes, map this imported vulnerability to the writeup in the library&#x20;
    * Otherwise, move to the next writeup or proceed to import vulnerability as per normal if no match found

<figure><img src="/files/3C2FBUCPKM4EUfpClkkW" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/gSCJXnIkI0F3SJ7kHs4C" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/jB6WAV7BffIgb1fdr4kI" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6qHDqAyU1k3h3vH61A6g" alt=""><figcaption></figcaption></figure>

### Datapoints - Imported Vulnerabilities

* **vuln.priority** - <*string*> priority of the vulnerability. Is either Critical / High / Medium / Low / Info
* **vuln.title** - <*string*> title of the vulnerability&#x20;
* **vuln.tags.\<tag>** - *\<string>* a specific tag listed within tags. Requires the tag to have a colon-separator within the tag e.g. ke&#x79;**:**&#x76;alue
* **vuln.custom\_tags.\<tag>** - *\<string>* a specific custom tag listed within custom tags. Replace \<tag> with the name of your custom tag
* **vuln.likelihood\_of\_exploitation** - <*integer*> likelihood of exploitation for vulnerability. Is a number between 1 to 10
* **vuln.description** - <*string*> description of the vulnerability
* **vuln.attack\_scenario** - <*string*> attack scenario of the vulnerability
* **vuln.remediation\_recommendation** - <*string*> remediation recommendation of the vulnerability
* **vuln.custom\_field.\<key>** - <*string*> vulnerability custom field. Replace \<key> with the key of your custom field
* **vuln.proof\_of\_concept** - <*string*> proof of concept / steps to reproduce for the vulnerability&#x20;
* **vuln.import\_source** - <*string*> import source for the vulnerability&#x20;
* **vuln.import\_source\_id** - <*string*> import source id for the vulnerability

### Datapoints - Writeups

* **writeup.title** - <*string*> title of the vulnerability writeup
* **writeup.tags.\<tag>** - *\<string>* a specific tag listed within tags. Requires the tag to have a colon-separator within the tag e.g. ke&#x79;**:**&#x76;alue
* **writeup.custom\_tags.\<tag>** - *\<string>* a specific custom tag listed within custom tags. Replace \<tag> with the name of your custom tag
* **writeup.likelihood\_of\_exploitation** - <*integer*> likelihood of exploitation for vulnerability writeup. Is a number between 1 to 10
* **writeup.description** - <*string*> description of the vulnerability writeup
* **writeup.attack\_scenario** - <*string*> attack scenario of the vulnerability writeup
* **writeup.remediation\_recommendation** - <*string*> remediation recommendation of the vulnerability writeup
* **writeup.custom\_field.\<key>** - <*string*> vulnerability writeup custom field. Replace \<key> with the key of your custom field
* **writeup.import\_source** - <*string*> import source for the vulnerability&#x20;
* **writeup.import\_source\_id** - <*string*> import source id for the vulnerability

### Operators

Expressions support multiple operators which can be used to build more complex expressions:

* **NOT** or **!** - used to negate an expression. For example *!(vuln.title === "TLS")*
* **AND** or **&&** - used to *and* multiple expressions. For example *vuln.title === "TLS" AND writeup.title === "TLS Weakness"*
* **OR** or **||** - used to *or* multiple expressions. For example (*vuln.title === "TLS" OR vuln.title === "SSL") AND writeup.title === "TLS Weakness"*
* **==** - used to check for equivalency. For example *vuln.title == "TLS"*
* **===** - used to check for equality. For example *vuln.title === "TLS"*
* **!=** or **!==** - used to check for not equivalency. For example *vuln.title !== "TLS"*
* **>** - used to check for greater-than comparison. For example *vuln.likelihood\_of\_exploitation > 5*
* **<** - used to check for less-than comparison. For example *vuln.likelihood\_of\_exploitation < 5*
* **>=** - used to check for greater-than-or-equals comparison. For example *vuln.likelihood\_of\_exploitation >= 5*
* **<=** - used to check for less-than-or-equals comparison. For example *vuln.likelihood\_of\_exploitation <= 5*
* **( )** or **!()** - used to group statements together. For example (*vuln.title === "TLS" OR vuln.title === "SSL") AND writeup.title === "TLS Weakness"*
* **$in** - used to check if a value is in a list of values. For example *vuln.tags.pluginID $in writeup.tags.pluginID*
* **$nin** - used to check if a value is not in a list of values. For example *vuln.tags.pluginID $nin writeup.tags.pluginID*
* **=\~ /RegEx/i** - used to check for a regular expression. For example *writeup.title =\~ /SSL/i* checks whether the vulnerability writeup has 'SSL' within its title.

### Examples

```
writeup.title $in vuln.title
```

This expression will match a writeup if its title partially or fully matches the title of the vulnerability. For example, if your vulnerabilities were *Out-of-date Version (React)* & *Out-of-date Version (Bootstrap)* - you can match them to a writeup with the title *Out-of-date Version.*

```
vuln.tags.pluginID $in writeup.tags.pluginID
```

This expression will match a vulnerability that has a tag 'pluginID' which is also found in the writeups' tags.

```
vuln.custom_tags.tls_weakness === writeup.custom_tags.tls_weakness
```

This expression will match a vulnerability and a writeup that both have a custom tag 'tlsweakness' which has the same value.

{% code overflow="wrap" %}

```
(vuln.tags.pluginID $in writeup.tags.pluginID) OR (vuln.tags.someID $in writeup.tags.pluginID)
```

{% endcode %}

This expression will match a vulnerability that has either a tag 'pluginID' or 'someID' which is also found in the writeups' tags.


# Updating Vulnerabilities

## Overview

Once a vulnerability has been created, you can then update the vulnerability in a number of ways.

Considering updating vulnerabilities is part of the role for a pentester, only users with Edit access to the project can perform this function.

However, any project team member can add remediation notes & mark a vulnerability as `Ready For Retesting`. This allows customers, developers & engineers to track remediation performed for a given issue, and inform once the vulnerability is ready to be retested.

<figure><img src="/files/vuRfBrow76PERzZxmKcE" alt=""><figcaption></figcaption></figure>

## Update Vulnerability

From the vulnerability page, click on `Edit`.

<figure><img src="/files/2SkxbnCL5lYVEcMc2maE" alt=""><figcaption></figcaption></figure>

## Upload Evidence

If you need to upload further evidence to a vulnerability, you can upload it from the `Evidence` section.

<figure><img src="/files/QByR6uJ7B4Mb4MLbnj8t" alt=""><figcaption></figcaption></figure>

## QA Reviews

`Review Notes` in AttackForge can be created against `Vulnerabilities` and `Project Executive Summary`.

Review notes help teams keep track of the changes needed, and all communication in one place.

You must have Edit permissions on the project to view and create review notes.

<figure><img src="/files/RUByFVGvv0N0txPoqJO5" alt=""><figcaption></figcaption></figure>

If you need to perform QA on multiple vulnerabilities, or would like to review each vulnerability one-by-one (from one screen) - you can select the vulnerabilities then click on `Actions -> Edit`.

<figure><img src="/files/ZTVEHiS27svM7AVZ105I" alt=""><figcaption></figcaption></figure>

Here you can review and update each vulnerability individually, and cycle through each vulnerability as needed.

<figure><img src="/files/pveF1FMeHikFp4Jl34tH" alt=""><figcaption></figcaption></figure>

## Overwrites

If you need to perform a bulk overwrite on selected fields across many vulnerabilities - you can select the vulnerabilities then click on `Actions -> Overwrite` .

<figure><img src="/files/iaeP8J6FifQoOzrYytYu" alt=""><figcaption></figcaption></figure>

You can opt into each field you would like to update, at one time for all selected vulnerabilities.

<figure><img src="/files/w5SU1AWtjChSQV2cT6Ee" alt=""><figcaption></figcaption></figure>

## Remediation Plan

If the `Remediation Plan` field is enabled (see Administration module), project team members can update the remediation plan for any of the vulnerabilities. This is useful to help get vulnerabilities acknowledged by technical teams, and plan for when those vulnerabilities will be fixed.

<figure><img src="/files/THX2kwzW7HKcTAdqw4ar" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4srDd3y7Dx0yEfFZRVAz" alt=""><figcaption></figcaption></figure>

The vulnerability will now track `Target Remediation Date`.

<figure><img src="/files/gWf9lBfMGlFrxcKmaPQJ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/lVMGRtGgkfW4WkXNGscx" alt=""><figcaption></figcaption></figure>

You can use the `Target Remediation Date` to create `Custom Time-Based Emails` which automatically follow up on vulnerabilities for you.

<figure><img src="/files/T9lcg7UHjwFuXVE0S8mC" alt=""><figcaption></figcaption></figure>

## Ready for Retesting

Once a vulnerability is ready for retesting, any user on the project can mark the vulnerability as `Ready for Retesting` from the vulnerability page or using bulk actions.

The audit trail for the vulnerability will also get updated to reflect the change in status.

<figure><img src="/files/9XaDT1LXOA3tnbFNWZnM" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/FAy79GOkoycu1dbSZZvf" alt=""><figcaption></figcaption></figure>

## Remediation Notes

Project team members can update the vulnerability remediation history and create remediation notes. &#x20;

<figure><img src="/files/74aziY80TpAOra5tAf17" alt=""><figcaption></figcaption></figure>

## Closed or Re-Opened

During remediation testing, vulnerabilities can be `Closed` and `Re-Opened` depending on the outcome.

<figure><img src="/files/YZdRbew4f3MPz524KFRS" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/KDvN9XV0j6CxyaSXVHUG" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/AV7DiuYP0SVax0tuXVCt" alt=""><figcaption></figcaption></figure>

## Bulk Add Tags

You can bulk add new tags for each selected vulnerability if the tag does not already exist.

<figure><img src="/files/1O24RAbGwOqDg8B3BJ8A" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/iX4VYFoU0ZdedJ65cOnc" alt=""><figcaption></figcaption></figure>

## Bulk Add Custom Tags

You can bulk add new custom tags for each selected vulnerability if the custom tag does not already exist. Otherwise if the custom tag already exists, it will update its value.

<figure><img src="/files/jXQHMZOm8a9FC1CLnsq8" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/mFrZcUztGMMQRG0ifS7E" alt=""><figcaption></figcaption></figure>

## Update SLA

You can bulk-update `Remediation SLA` on vulnerabilities to a new future date.

> **!IMPORTANT:** Only Admins and Project Coordinators are allowed to perform this operation.

<figure><img src="/files/PG8B9PGYwMHG8unMesDj" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/qtaU4Eyd0zniFDQOUDcF" alt=""><figcaption></figcaption></figure>

## Re-apply SLA

You can re-apply the `Remediation SLA` on vulnerabilities. This will remove the existing SLA, and replace it with a new SLA from the SLA ruleset defined in `Administration` module.

If no SLA exists on the vulnerability, a new SLA will be applied.&#x20;

> **!IMPORTANT:** Only Admins and Project Coordinators are allowed to perform this operation.

<figure><img src="/files/LgWMI2JldtUd8jPkMxWv" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/9rsPeIfWVxxe50STqo2R" alt=""><figcaption></figcaption></figure>

## Duplicating Vulnerabilities

Duplicating vulnerabilities will clone a vulnerability. This means you will end up with two (2) of the same vulnerability. As the clone is a unique vulnerability, it will be treated as such from a dashboard/analytics/reporting perspective.

<figure><img src="/files/eWStvY9XoyyJbiq7539b" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/dsp9YatHXb6XhOenkHpP" alt=""><figcaption></figcaption></figure>

## Linking Vulnerabilities

Vulnerabilities in AttackForge get created on projects. A vulnerability is linked to either one or more projects.

With the exception of Administrators, a user can only see vulnerabilities for which they have access to the project(s) linked to those vulnerabilities.

Linking vulnerabilities will make vulnerabilities available on other projects. Users on the linked projects will have view and edit access to the linked vulnerabilities, depending on their access level on the linked projects.

Linking vulnerabilities is useful when consolidating vulnerabilities into projects for remediation or tracking.

> **!IMPORTANT:** When linking vulnerabilities, keep in mind:\
> \- ***Vulnerabilities are not transferred.*** Vulnerabilities will become available on the new project, and will also remain available in the current project. You can link a vulnerability to many projects.\
> \- ***Vulnerabilities are not copied.*** This means there will be no duplication of vulnerabilities in your dashboards, analytics, tables, etc.\
> \- ***Vulnerabilities are universal.*** Any changes to these vulnerabilities in either project will universally apply.\
> \- Assets assigned to each vulnerability will be added to the new projects' scope.

If a user deletes a linked vulnerability, it will only be deleted from its project. It will not be deleted on other linked projects for that vulnerability.

<figure><img src="/files/33Qie09FabUFX98NINPa" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/0obDpQR7vVJRkEiRL8Rq" alt=""><figcaption></figcaption></figure>

## Re-Assigning Vulnerabilities

You can re-assign a vulnerability to another project. Once a vulnerability is re-assigned, it will no longer be available on the current project. All remediation notes, review notes & evidence will also be relocated to the new project.

<figure><img src="/files/vnxeUXndRFzGT3wsI3O8" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/pZcNX9MuCOjqFmgmWRbu" alt=""><figcaption></figcaption></figure>


# Review & QA

## Overview

Quality assurance is a very personal and bespoke process for every organization.&#x20;

For example, internal security teams usually have different QA workflows when compared to consultancies and MSSPs.

Some teams are small, highly experienced and co-located; where as others might be large, with a mixture of senior and junior testers, and geographically dispersed - making QA workflows unique to the organization.

Areas you might consider in your QA workflows include:

* [Configuring QA fields](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms), for example:
  * *QA Status* (not ready / ready / passed / failed / etc.)
  * *QA Requestor* (user / users / group / groups)
  * *QA Reviewer* (user / users / group / groups)
  * *QA Approver* (user / users / group / groups)
  * *QA Approved Datetime*
  * etc.
* [Configuring business logic](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#hide-expressions-conditions) on when those QA fields should show or hide
* Access control on each field i.e. who can view, who can edit
* [Capturing QA comments](#review-notes) and discussions
* Notifying QA members when QA review is requested
* Permitting authorized QA members to perform QA review, and to update QA status fields
* Transition statuses according to custom business logic relating to QA fields

You can also adjust the [project status calculation logic](https://support.attackforge.com/attackforge-enterprise/afscript%22%20%5Cl%20%22project-status-calculations) to factor in your QA fields. For example, you may only consider a project to be “Completed” when your custom QA criteria has been met.

All QA fields can be used in reports, for example you may need reports which only show vulnerabilities when your QA has passed, or include information on the QA reviewers. There are examples for this on our [ReportGen GitHub](https://github.com/AttackForge/ReportGen) repository.

You can use the [Custom Time-Based Emails](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications%22%20%5Cl%20%22custom-time-based-emails) to configure additional custom emails to be sent as part of your QA processes.

You can also configure custom workflow automations relating to your QA processes using the [Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows) workflow automations engine.

## Review Notes

Review Notes in AttackForge are designed to help with quality control and quality assurance. They help to keep track of review information in one place.

Review Notes can be created by editors, and are available in the following sections:

* Vulnerabilities
* Writeups
* Project Test Cases
* Reporting
* Summary page
* Test Suite Test Cases

<figure><img src="/files/nUKJxdvFUZtxITHuvrEn" alt=""><figcaption></figcaption></figure>

## Creating Review Notes

When creating review notes for vulnerabilities, project test cases, reporting or summary page - you must have Edit permissions on the project to view and create review notes.

You can start a review thread on a topic by clicking on the `Add Review Note` button next to the field:&#x20;

<figure><img src="/files/WG7G38AXZCu0KDkPleRj" alt=""><figcaption></figcaption></figure>

You can also click on the `Add Note` button in the Review pane:

<figure><img src="/files/GuDVadu4UvGrzO90t9Qq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/65dIn1t2PG7oc2Ilw5r5" alt=""><figcaption></figcaption></figure>

You can optionally set topics to focus the review:

<figure><img src="/files/vRSVRPchixZBVIoQASWP" alt=""><figcaption></figcaption></figure>

Set your notification preferences. You can also include additional persons to be notified, if required.

<figure><img src="/files/daiBbGiQBK0RKzrn5WJR" alt=""><figcaption></figcaption></figure>

Reply to an existing note in a thread. An email will be sent to the person you are replying to. You can include additional persons to be notified, if required.

<figure><img src="/files/cM2K5GV3Q1wIV94Oeb4i" alt=""><figcaption></figcaption></figure>

## Resolving Review Threads

You can resolve a thread by clicking on the `Resolve` button on the top-level review note:

<figure><img src="/files/KCI9j2nx0zasS7FNDGPC" alt=""><figcaption></figcaption></figure>

You can also filter on resolved notes:

<figure><img src="/files/6uWWXK4AI0BDPJsCD5M2" alt=""><figcaption></figcaption></figure>

## Bulk Review/QA on Vulnerabilities

To perform efficient QA reviews, you can select multiple vulnerabilities that you wish to review, and then review each vulnerability one-by-one - all from one screen. You can access all information, including evidence & review notes.

If you need to perform QA on multiple vulnerabilities, or would like to review each vulnerability one-by-one (from one screen) - you can select the vulnerabilities then click on `Actions -> Edit`.

<figure><img src="/files/zo9wYbVX5Fzm85ksX0GB" alt=""><figcaption></figcaption></figure>

Click on the `Review` tab, then enter your review comments. Once you are finished, click on the next vulnerability using the directional arrows.

<figure><img src="/files/opFfOGCoKewcor3h8J3k" alt=""><figcaption></figcaption></figure>

Once you have finished reviewing all vulnerabilities, you will see the `Next` option is no longer available - meaning you have reached the end of the review.


# Attack Chains

## Overview

Attack Chains help demonstrate exactly what an attacker is doing at every step of the way - in a simple and easy to understand visual story. It helps clients understand attack paths and focus remediation where it's needed.

> Building Attack Chains can provide extra information to help customers and developers prioritise focus areas for remediation, without relying on traditional risk ratings or scores.&#x20;

Attack Chains help to identify ways to block attacks from being chained together, and prioritise core issues with least effort & resources.

<figure><img src="/files/r1jhy1IU4TS8bdiMXDJM" alt=""><figcaption></figcaption></figure>

## Creating Attack Chains

To create an attack chain, you must have Edit permissions on your project.&#x20;

From your project dashboard, select Attack Chains then click on `Add`.

### Step 1: Define Attack Objective

This is where you define the objective an attacker can achieve as part of this attack chain. Ensure to keep this is high-level as possible and relatable to the business or customers.

<figure><img src="/files/ZpOXIyOKbr8JVwkK7Ibz" alt=""><figcaption></figcaption></figure>

### Step 2: Define Initial Attack Vector

The first link in the attack chain usually starts with the attacker. You can select from an External Attacker e.g. outside customer network/environment; or Internal Attacker e.g. inside customer network/environment.

You also need to define how the attacker is initiating the attack. This is to help organisations better understand the context of where the attacker is coming from e.g. opportunistic (stumbled across a web application); rogue employee or insider threat; etc.

<figure><img src="/files/wUF0MPUNyEvcML4An1yu" alt=""><figcaption></figcaption></figure>

### Step 3: Add Links to the Chain

You can add any combination of links to the chain. This is where you build the chain to reach the objective defined in Step 1.

The links currently supported include:

* **Attacker** - Either internal or external attacker. Can be used to demonstrate how an external attacker breaches perimeter and becomes internal attacker.
* **Victim** - Can be used if introducing social engineering component to your attack chain.
* **Defender** - Can be used if introducing blue-team component to your attack chain.
* **Action** - Used when defining what actions are being performed by an actor in the chain.
* **Vulnerability** - This is used to link to a vulnerability discovered on the project.
* **Device** - Can be used to reference a device which is targeted or leveraged as part of the attack chain.
* **Server** - Can be used to reference a server which is targeted or leveraged as part of the attack chain.
* **Database** - Can be used to reference a database which is targeted or leveraged as part of the attack chain.&#x20;
* **Flag** - Used to identify that an attacker has reached end objective (defined in Step 1); or an interim objective on the way to reaching the end goal.

<figure><img src="/files/Hti3xMT7gwdd5jBwx46b" alt=""><figcaption></figcaption></figure>

Once your links have been added, you should have a completed attack chain that you can save.&#x20;

Attack Chains can be viewed by any team members on the project by clicking on Attack Chains from the project menu. They are also included in the reports.

## Updating Attack Chains

Your attack chain can be modified by clicking on `Edit`. You will have the option to make changes to the attack objective as well as any links in the chains. You can add new links or remove any existing links, at any point in the chain.

You can also duplicate attack chains using the `Duplicate` button & also delete attack chains using the `Delete` button.

<figure><img src="/files/C7mQ2VyzcEtaStnXyCyq" alt=""><figcaption></figcaption></figure>

## Map Attack Chains to MITRE ATT\&CK Framework

You can map your attack chains to `MITRE ATT&CK Framework`.&#x20;

MITRE ATT\&CK Framework is a knowledge base of adversary tactics and techniques based on real-world attack patterns. It provides threat models and methodologies to help you better plan, prepare & defend against real-world attacks.

<figure><img src="/files/dDIhdKnvGGxRsqqwiIQU" alt=""><figcaption></figcaption></figure>

## Re-Ordering Attack Chains

You can re-order & prioritise how you attack chains are displayed in the application and also in the reports.

To re-order your attack chains, click on any of the `Move Up` or `Move Down` buttons at the top of your attack chain.

<figure><img src="/files/SlZ70cvwtFgbxDq4ibfA" alt=""><figcaption></figcaption></figure>


# Reporting

## Overview

AttackForge provides high-quality reports on-demand when you or your customers need them.&#x20;

Any team member on your project can download reports in custom templates. These reports are dynamic and will display the most current data on your project.

> Every project can have an unlimited number of reporting templates available.

There is also a JSON export which contains all of the data for the reports. This is useful for creating reports offline or for backup purposes.

The ZIP archive contains all evidence which has been uploaded to the vulnerabilities on the project. It is useful if the customer needs high-resolution screenshots, or access to evidence which is not an image format and as such not already included in the reports - for example scripts, videos, etc.

You can download any of the on-demand custom reports, JSON export or ZIP archive - directly from your project.

<figure><img src="/files/kAXU1dpXzHo4DzTKQxzb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/rkO3D9DL3F5e86q7Bnjw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/piYcKWAPswJZphjcIgjo" alt=""><figcaption></figcaption></figure>

## Custom Reports

AttackForge has a custom built reporting engine we call `ReportGen` which helps you create custom DOCX reports, on-demand and in a variety of templates.&#x20;

For example - you can create:

* **Pentest reports**
* **Retesting reports**
* **Executive reports**
* **Testing summary reports**
* **Application and Infrastructure reports**
* **Mobile testing reports**
* **Red Team and Purple Team reports**
* **Configuration Audit reports**
* **Compliance reports, to name a few.**

Custom reports can be generated by any user, on demand, from the project dashboard or reporting section on their projects.&#x20;

Each user can only see reporting options which have been made available to them.

<figure><img src="/files/OWjiSP5fKbV2KqrPmZb7" alt=""><figcaption></figcaption></figure>

Every report template can be configured with access controls in the `Report Templates` module to restrict visibility and usage of templates for `Roles, Groups and Users`.

<figure><img src="/files/MygUQ5tjn9tPPHUM9RWz" alt=""><figcaption></figcaption></figure>

You can also control when the `Executive Summary` and `Custom Reports` become available to users on the project, and who is allowed to create reports on the project. This is configure in the project settings under `Access`.

<figure><img src="/files/3x8A3flxpyGBh1ZTzDYh" alt=""><figcaption></figcaption></figure>

For more details on how to create reporting templates for custom reports, please see [Reporting](https://support.attackforge.com/attackforge-enterprise/modules/reporting).

## Custom Report on Selected Vulnerabilities

You can select one or more vulnerabilities to create a custom report with only that selection.

This is useful when you need to get a report out to different teams, with only the context for vulnerabilities which are relevant to that team.

Start by selecting the vulnerabilities from your project vulnerabilities tab, then select `Actions -> Custom Report`. Choose your custom report. The report will be tailored to only the vulnerabilities in your selection.

<figure><img src="/files/8K1wrvwSuaiBjXD4KG38" alt=""><figcaption></figcaption></figure>

## Update Executive Summary

The reports contain an `Executive Summary` section. This is where you can include:

* Objectives of the assessment
* Overall observations or notable findings determined during the assessment
* Positive security controls identified
* Assumptions
* Limitations

If you need to update the Executive Summary, you can do this by clicking on `Reporting` tab from the project.

Note you must have Edit permissions on the project in order to update the executive summary section.

<figure><img src="/files/EbQcAxKy6csJkRrSEvcJ" alt=""><figcaption></figcaption></figure>

## Custom Reporting Fields

You can create custom sections and custom fields to personalize your `Reporting` data to the project.

Some ideas for **Sections** and **Fields** you can create:

* Project Summary - including Executive Summary, Summary of Recommendations and Positive Security Observations
* Testing Overview - including Background, Approach and Methodology
* Document Control - including Author(s), Reviewer, Approver and Version History

To get started, go to `Administration` and click on `Reporting`.

<figure><img src="/files/YoenSZIw5pUSTQzBXSuP" alt=""><figcaption></figcaption></figure>

Start creating Sections and Fields for the Reporting data you want to capture on your projects.

> Set default values to make your reporting easier!

<figure><img src="/files/A6a1Owpkka94SZOt7mS4" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/xYOH5bjgAu93cJmYTDxn" alt=""><figcaption></figcaption></figure>

Now on your existing or new project, enable the **Reporting** section.&#x20;

Add access levels based on the needs of your project.

<figure><img src="/files/Dqgyp4iAf227WEsAvUfW" alt=""><figcaption></figcaption></figure>

Enter information and upload files based on your configuration.

> !IMPORTANT: Only users with Edit access on the project can edit the Reporting fields

<figure><img src="/files/oUatxEloGnRYZ1jlBqf8" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/9rwBM1N9nnY5zHpJdr8y" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/RqkzQbk6E6fsvoMHx4H4" alt=""><figcaption></figcaption></figure>

Add custom fields to your Reports. For more information how to do this, please visit <https://github.com/AttackForge/ReportGen/issues/25>

## Export JSON Project Data

There is also a JSON export which contains all of the data for the reports. This is useful for creating reports offline or for backup purposes.

<figure><img src="/files/uo1RMjQI0u7XWOY7qY5D" alt=""><figcaption></figcaption></figure>

## Evidence as ZIP

The ZIP archive contains all evidence which has been uploaded to the vulnerabilities on the project. It is useful if the customer needs high-resolution screenshots, or access to evidence which is not an image format and as such not already included in the reports - for example scripts, videos, etc.

You can download any of the on-demand custom reports, JSON export or ZIP archive - directly from your project.

<figure><img src="/files/mdBTlhKxdGS5qFOYveT2" alt=""><figcaption></figcaption></figure>

## Custom Project Report Name Tags

You can use the following tags to dynamically change the name of the custom report which is downloaded on a project.

<figure><img src="/files/g5GOPKWfkeX3uSYZet7r" alt=""><figcaption></figcaption></figure>

* **{project.id}** - project Id.
* **{project.name}** - project name.
* **{project.code}** - project code.
* **{project.organization\_code} -** project organization code.
* **{project.status} -** project status.
* **{project.start\_date} -** project start date.
* **{project.end\_date} -** project end date.
* **{project.custom\_field.\<key>} -** project custom field. Replace \<key> with the key on your custom field.
* **{now}** - an ISO 8601 standard date format for the time when the report is generated

### Custom Date Formats

The following tags support the [dateFormat](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateformat) filter which can be applied to format the timestamp to a desired date and/or time.

* {project.start\_date | dateFormat:\["fullDate"]}
* {project.end\_date | dateFormat:\["paddedShortDate"]}
* {now | dateFormat:\["dd-mmm-yyyy"]}


# Retesting & Remediation

## Overview

AttackForge tracks the remediation history for all vulnerabilities - from `Open`, to `Retesting` & `Closed`. This helps to understand the status of a vulnerability - at any point in time - when you or the customer needs it.&#x20;

Every vulnerability has its own history which contains `Remediation Notes`, to help track what remediation actions were performed, when and by whom.

AttackForge also tracks every `Round of Retesting` that has been requested or performed against the project to make the process simple & fast.

## Add Remediation Notes

Any project team member can add remediation notes to a vulnerability. This can be used by engineering teams when updating the remediation actions performed on the vulnerability, or by pentesters when documenting observations during a retest.

To view and create remediation notes, navigate to the vulnerability page and click on the `Remediation` icon on the right hand side.

<figure><img src="/files/oXXnME10vyb7fAd2nXPH" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/ImNZ790DEt6Ern0XOe4q" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MrBeBRNDNu9zQkYNSjcj" alt=""><figcaption></figcaption></figure>

To bulk add remediation notes, select the vulnerabilities and then click `Actions -> Add Remediation Note`.

<figure><img src="/files/5Gexu4mwvLODxwSWHEUU" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TiqHQi8lCbF8lYx82kLf" alt=""><figcaption></figcaption></figure>

## Remediation Plan

If the `Remediation Plan` field is enabled (see Administration module), project team members can update the remediation plan for any of the vulnerabilities. This is useful to help get vulnerabilities acknowledged by technical teams, and plan for when those vulnerabilities will be fixed.

<figure><img src="/files/THX2kwzW7HKcTAdqw4ar" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4srDd3y7Dx0yEfFZRVAz" alt=""><figcaption></figcaption></figure>

The vulnerability will now track `Target Remediation Date`.

<figure><img src="/files/gWf9lBfMGlFrxcKmaPQJ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/lVMGRtGgkfW4WkXNGscx" alt=""><figcaption></figcaption></figure>

You can use the `Target Remediation Date` to create `Custom Time-Based Emails` which automatically follow up on vulnerabilities for you.

<figure><img src="/files/T9lcg7UHjwFuXVE0S8mC" alt=""><figcaption></figcaption></figure>

## Assign Vulnerabilities for Retesting

Any project team member can assign a vulnerability for retesting. This can be used by engineering teams indicating that a vulnerability has been resolved and can now be retested.

You can assign a vulnerability for retesting by clicking on `Update Status -> Ready for Retesting` from the vulnerability page.

<figure><img src="/files/CtDw1Nkkw5fj3rgfVYi7" alt=""><figcaption></figcaption></figure>

You will be prompted to enter an optional remediation note which is useful to help the security team understand what fixes have been put in place in order to indicate vulnerability is now ready for retesting.

<figure><img src="/files/E2s0QYxKZOFAcwEKkOSF" alt=""><figcaption></figcaption></figure>

You can view `Remediation History` by clicking on `Remediation` button on the vulnerability page.

<figure><img src="/files/WZzxfNYCRLi1U7B8lUnr" alt=""><figcaption></figcaption></figure>

You can also bulk assign vulnerabilities for retesting.

<figure><img src="/files/ER8cGetTKX2Sv4DivyOg" alt=""><figcaption></figcaption></figure>

## Request a Retest

After you have marked vulnerabilities as `Ready for Retesting` on a project, you can request a `Retesting Round` to be performed.&#x20;

To request a round of retesting, click on `Retesting` from the project menu, then click on `Request Retest`.

Select the test window when the retest can occur.&#x20;

Select the vulnerabilities you would like to include in this round of retesting. You can only select from vulnerabilities which are currently marked as Ready for Retest.

Click `Request Retest` when ready.

<figure><img src="/files/0i65zVpTJIUmoRTQ7ccP" alt=""><figcaption></figcaption></figure>

After submitting the request, you will receive a confirmation email. The administrators will also be notified of your request.

You will also see your request registered as a new round.

<figure><img src="/files/7nYuBkbQ1oyuAFhnTeDl" alt=""><figcaption></figcaption></figure>

## Perform a Retest

Once a retest has been requested, the pentesters/consultants can commence the retesting. The vulnerabilities in-scope for the retest can be accessed from the `Retesting` section on the project.

<figure><img src="/files/RgC4SocBkh467GhDwcKM" alt=""><figcaption></figcaption></figure>

Click on a vulnerability to bring up a list of in-scope vulnerabilities for the given round.

<figure><img src="/files/yQwKGEuf5eFeT5LuOEEQ" alt=""><figcaption></figcaption></figure>

Perform your retesting for each vulnerability. Note you will need to have Edit permissions on the project in order to perform the retest.

* **Check the Workspace** - get all the information and context you need to set up for the retest.
* **View Remediation Notes** - to understand what steps have been taken to mitigate this vulnerability.
* **Upload Evidence** - upload screenshots and proof of the findings/observations from the retest.
* **Add Remediation Note** - add further remediation notes to include the findings/observations from the retest.
* **Re-Open Vulnerability** - re-open the vulnerability if it is deemed to be not fixed.
* **Close Vulnerability** - close the vulnerability if it is deemed to be fixed.

<figure><img src="/files/5z0GKkEyJhUUh7F2xgn9" alt=""><figcaption></figcaption></figure>

Once you have performed the retest for all the in-scope vulnerabilities, click on `Complete Round` from the actions menu for the round of retesting you are working on. An email notification will be sent to the project team to inform them that the retest is now completed. A record of the retested vulnerabilities will also be visible and also accessible in reports.

<figure><img src="/files/Usps1nPKs0q1hGk7dCvd" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/xkQj9aklQghhybEEvxzt" alt=""><figcaption></figcaption></figure>

## Tracking Retesting

You can track retesting history on a project by simply viewing the project dashboard page.

<figure><img src="/files/SjvgGg3rT2AlikJNs67X" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/dnUehW6XtlowFJrnEAzi" alt=""><figcaption></figcaption></figure>

You can keep track of all retests on your projects by clicking on `Retesting View` from the projects module.

<figure><img src="/files/2sAw2OaC8RwYgKWlP3f3" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TDRLZqeuIvnMNM70e92q" alt=""><figcaption></figcaption></figure>

Here you will find a list of all your projects, including columns at the end (far right) which you can sort by and includes:

* number of vulnerabilities flagged for retesting on the project;&#x20;
* number of round of retesting requested on the project;&#x20;
* number of rounds of retesting completed on the project.

You can click on any of the numbers in order to drill-down to the details.

## Cancelling a Retest

Administrators and persons who had requested a retest can cancel their retest if it is no longer required. Once a retest has been cancelled, no further actions can be taken for that round - however a new round can always be requested.

All in-scope vulnerabilities for the cancelled round of retesting will be reset back to Open status.&#x20;

A remediation note will also be created to track the status change due to cancelled retest.

You can cancel a retest by clicking on `Cancel Round` from the actions menu.

<figure><img src="/files/lADBdncLDcuWFvDotKwz" alt=""><figcaption></figcaption></figure>


# Notifications

## Overview

AttackForge includes email notifications to help keep everyone informed on testing progress, including retesting, as well as new vulnerabilities and changes on existing vulnerabilities.

## Daily Started / Stopped Testing

For project team members with Edit access e.g. pentesters/consultants, they can inform the project team by email every day when they start or stop testing.

The emails can be personalised for each project, to provide a contextual update for teams.

To do this, from the project dashboard menu - select `Daily Commencement Email` to inform the project team you have started testing; and `Daily Completion Email` to inform the project team you have stopped testing.

To configure the emails - see [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects#email-project-team-on-events)

<figure><img src="/files/2CFmizGdfRlsW2tr6gjO" alt=""><figcaption></figcaption></figure>

## New Vulnerabilities

To make vulnerability information flow faster to help reduce time-to-remediate, email notifications can be enabled on different events, such as discovery of a new critical or high vulnerability.&#x20;

These emails will only be sent when a vulnerability is set to be visible.

You can configure how much, or how little, vulnerability information you would like to share for each project.

You can opt into whichever email notifications are relevant for the project team or individual team members.

<figure><img src="/files/yyqVNAnZHFZZPIpVb5xL" alt=""><figcaption></figcaption></figure>

#### Notification Type For New Vulnerabilities

Emails for new vulnerabilities can be sent in two different ways:

* **Send Individual Email For Each New Vulnerability** - an email will be sent to each opt-in project team member (and additional recipients, if configured) for every new vulnerability discovered. That is, *one email will be sent per new vulnerability*.
* **Send One Email with Details For All New Vulnerabilities** - an email will be sent to each opt-in project team member (and additional recipients, if configured) which includes a summary for every new vulnerability discovered, in a single email.\
  That is, *one email will be sent with a summary for each new vulnerability.*

To configure the emails - see [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects#email-project-team-on-events)

## Custom One-Off Emails

You can send custom one-off emails to the project team, or individual team members. This is useful for one-off emails which need to be sent from AttackForge.

Custom one-off emails support [Project Email Meta Tags](#custom-email-meta-tags).

To send a custom one-off email, click on `Custom One-Off Email` from the project menu.

<figure><img src="/files/SlbmiNYClmdDFdWegnqC" alt=""><figcaption></figcaption></figure>

Include an email subject and body. The body supports limited HTML and CSS.

Select the recipients, or enter any additional email addresses and click `Send Email`.

<figure><img src="/files/jJcl4B7L6BbA7k3KDJEt" alt=""><figcaption></figcaption></figure>

## Custom Time-Based Emails

AttackForge has a powerful utility for generating custom emails on a schedule, leveraging our rules-based engine.

Custom emails extend the robust notifications that already come standard with AttackForge workflows. Custom emails allow you to create your own workflows for reminders, escalations, or reporting. These are just few examples of what custom emails can bring to your organisation.&#x20;

Administrators can configure custom emails from the `Administration -> Notifications -> Custom Emails` section.

<figure><img src="/files/qozN0T86hibGF3Vm6BuU" alt=""><figcaption></figcaption></figure>

Custom time-based emails allow you to create, configure and send emails on a repeating frequency.

Every custom email can have it's own unique set of configuration options, including:

* **Key -** this is used to reference this custom email rule.
* **Email Frequency -** this is where you define the repeating frequency for this custom email.
* **Email Time -** this is where you define which hour of the day you would like this custom email to be sent.
* **Type -** this is where you can configure the type of email you would like to send, and the data you will have access to for each recipient.
* **Filter -** this is where you can filter the data from the Type based on your unique requirements for this custom email.
* **Recipients -** this is where you configure the audience for this custom email.
* **Subject -** this is the email subject for the custom email.
* **Body -** this is the email body for the custom email.

### Email Frequencies

The following email frequencies are currently supported:

* **Daily -** emails will be sent each day during the selected hour
* **Weekly -** emails will be sent on the selected day, every week during the selected hour

### Email Time

Custom time-based emails can be configured to be sent at any time during the day, during a selected hourly block.&#x20;

For example, you can configure a custom email to be sent between 8AM and 9AM to ensure recipients have the information at the start of their day; or between 5PM and 6PM so that they have a summary from that day.

### Type&#x20;

Types will give you access to a particular set of data that can be used for the custom email.

The following types are currently supported:

* **Vulnerabilities -** this includes data relating to the vulnerabilities, their assets, projects, and users.
* **Projects -** this includes data relating to projects including statistics for vulnerabilities, assets and test cases
* **Project Requests -** this includes data relating to project requests.
* **Users -** this includes data relating to users.
* **Writeups** - this includes data relating to writeups.

The type will determine what data can be included in the Filter and also the Body.

### Filter

Filter is used to select the exact dataset you would like to use for the custom email. The filter works similar to a database query, where you can specify fields & operators - these help to narrow down the results to the data you would need for the custom email.

**!IMPORTANT:** After data is filtered, and before it is sent to recipients - it is verified against the recipient to ensure that the recipient has privileges to access the data.

Filters support the following operators:

**$and**

Can be used to AND two or more conditions.

*Example:*  Filter vulnerabilities which are Open and Critical

```
{    
    $and: [
        {
            status: { $eq: 'Open' }
        },
        {
            priority: { $eq: 'Critical' }
        }
    ]
}
```

**$or**

Can be used to OR two or more conditions.

*Example:*  Filter vulnerabilities which are Critical or High

```
{
    $or: [
        {
            priority: { $eq: 'Critical' }
        },
        {
            priority: { $eq: 'High' }
        }
    ]
}
```

**$eq**

Used to check that a field is equal to a value. A value can be a string, boolean, number, null or a function.

*Example:*  Filter vulnerabilities which are Critical

```
{
    priority: { $eq: 'Critical' }
}
```

**$ne**

Used to check that a field is not equal to a value. A value can be a string, boolean, number, null or a function.

*Example:*  Filter vulnerabilities which are not Informational

```
{
    priority: { $ne: 'Info' }
}
```

**$in**

Used to check that a value exists in a list. Supports an array of values.

*Example:*  Filter vulnerabilities which have a tag 'OWASP Top 10' (Top 10 Vulnerability)

```
{
    tags: { $in: ['OWASP Top 10'] }
}
```

**$nin**

Used to check that a value does not exist in a list. Supports an array of values.

*Example:*  Filter vulnerabilities which do not have a tag 'OWASP Top 10' (Not a Top 10 Vulnerability)

```
{
    tags: { $nin: ['OWASP Top 10'] }
}
```

**$gt**

Used to check that a field is greater than a value. Supports strings, numbers and functions.

*Example:*  Filter vulnerabilities which have a likelihood of exploitation greater than 7

```
{
    likelihood_of_exploitation: { $gt: 7 }
}
```

**$gte**

Used to check that a field is greater than or equal to a value. Supports strings, numbers and functions.

*Example:*  Filter vulnerabilities which have a likelihood of exploitation greater than or equal to 7

```
{
    likelihood_of_exploitation: { $gte: 7 }
}
```

**$lt**

Used to check that a field is less than a value. Supports strings, numbers and functions.

*Example:*  Filter vulnerabilities which have a likelihood of exploitation less than 7

```
{
    likelihood_of_exploitation: { $lt: 7 }
}
```

**$lte**

Used to check that a field is less than or equal to a value. Supports strings, numbers and functions.

*Example:*  Filter vulnerabilities which have a likelihood of exploitation less than or equal to 7

```
{
    likelihood_of_exploitation: { $lte: 7 }
}
```

#### $regex

Used to perform a regular expression for a field. Support Javascript regular expressions.

*Example:*  Filter vulnerabilities which have SQL in the title, using a case insensitive search.

```
{
    title: { $regex: /SQL/i }
}
```

#### $elemMatch

This operator matches documents that contain an array field with at least one element that matches all the specified query criteria.

*Example:*  Filter vulnerabilities by a specific custom field name and value.

```
{
    custom_fields: { 
        $elemMatch: { 
            name: { $eq: "is_pci" }, 
            value: { $eq: "Yes } 
        } 
    }
}
```

#### $all

Used to perform a regular expression for a field. Support Javascript regular expressions.

*Example:*  Filter vulnerabilities which both CWE-520 and Web App in the tags.

```
{
    tags: { $all: ["CWE-520", "Web App"] }
}
```

#### Functions

The following functions are currently supported:

* datetime()

#### datetime(timeValue, modifiers)

datetime can be used to construct a date & time and then modify it (if needed).

* timeValue - must be either:
  * now
  * YYYY-MM-DD
  * YYYY-MM-DD HH:MM
* modifiers - must be either:
  * +999 years
  * -999 years
  * +999 months
  * -999 months
  * +999 days
  * -999 days
  * +999 hours
  * -999 hours
  * +999 minutes
  * -999 minutes
  * start of year
  * start of month
  * start of day

Example 1: Filter vulnerabilities created greater than June 1st, 2022.

```
{
    created: { $gt: datetime('2022-06-01') }
}
```

Example 2: Filter vulnerabilities created greater than UTC 12:00 on June 1st, 2022.

```
{
    created: { $gt: datetime('2022-06-01 12:00') }
}
```

Example 3: Filter vulnerabilities created in last 7 days.

```
{
    created: { $gt: datetime('now', '-7 days') }
}
```

Example 4: Filter vulnerabilities with SLA greater than 7 days from now.

```
{
    sla: { $gt: datetime('now', '+7 days') }
}
```

Example 5: Filter vulnerabilities with SLA greater than 7 days + 1 year from now. Multiple modifiers will execute in order i.e. add 7 days, then add 1 year.

```
{
    sla: { $gt: datetime('now', '+7 days', '+1 year') }
}
```

### Vulnerability Fields

The following fields are supported in filters for the 'Vulnerability' type:

**id**

The id for the vulnerability.

*Example:* get vulnerability with id 62a190f7793b8ccd085e0d9d

```
{
    id: { $eq: ObjectId("62a190f7793b8ccd085e0d9d") }
}
```

**alternate\_id**

The alternate id for the vulnerability (set by the vulnerability code on the project).

*Example:* get vulnerability with alternate id GLOBEX-1

```
{
    alternate_id: { $eq: 'GLOBEX-1' }
}
```

**created**

The created date for the vulnerability.

*Example:* get vulnerabilities which have been created in the last 7 days.

```
{
    created: { $gte: datetime('now', '-7 days') }
}
```

**modified**

The modified date for the vulnerability.

*Example:* get vulnerabilities which have been modified in the last 7 days.

```
{
    modified: { $gte: datetime('now', '-7 days') }
}
```

**priority**

The priority for the vulnerability. Supports Critical, High, Medium, Low & Info.

*Example:* get vulnerabilities which are Critical.

```
{
    priority: { $eq: 'Critical' }
}
```

**title**

The title for the vulnerability.

*Example:* get vulnerabilities which have SQL in the title.

```
{
    title: { $regex: /SQL/i }
}
```

**zero\_day**

Whether the vulnerability is a zero day or not. Supports Yes or No.

*Example:* get vulnerabilities which are a zero day.

```
{
    zero_day: { $eq: 'Yes' }
}
```

**likelihood\_of\_exploitation**

The likelihood of exploitation for a vulnerability. Supports 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.

*Example:* get vulnerabilities which have a likelihood of exploitation greater than or equal to 7.

```
{
    likelihood_of_exploitation: { $gte: 7 }
}
```

**status**

The status for the vulnerability. Supports Open or Closed.

*Example:* get all open vulnerabilities.

```
{
    status: { $eq: 'Open' }
}
```

**status\_updated**

The date when the status was last updated for the vulnerability.

*Example:* get vulnerabilities which have had their status change in the past 7 days.

```
{
    status_updated: { $gte: datetime('now', '-7 days') }
}
```

**is\_retest**

Whether the vulnerability is flagged for retesting or not. Supports Yes or No.

*Example:* get vulnerabilities which are currently flagged for retesting.

```
{
    is_retest: { $eq: 'Yes' }
}
```

**sla**

The SLA date for the vulnerability.

*Example:* get open vulnerabilities which have exceeded/breached their assigned SLA by at least 7 days.

<pre><code><strong>{
</strong><strong>    $and: [
</strong>        {
            status: { $eq: 'Open' }
        },    
        {
            sla: { $lte: datetime('now', '-7 days') }
        }
<strong>    ]
</strong><strong>}
</strong></code></pre>

**target\_remediation\_date**

The target remediation date for the vulnerability.

*Example:* get open vulnerabilities which have target remediation date exactly 7 days from now.

```
{    
    $and: [
        {
            status: { $eq: 'Open' }
        },    
        {
            target_remediation_date: { $lte: datetime('now', '+7 days') }
        },
        {
            target_remediation_date: { $gte: datetime('now', '+6 days') }
        }
    ]
}
```

**release\_date**

The release date for the vulnerability.

*Example:* get vulnerabilities which have been released in the past 7 days.

```
{
    release_date: { $gte: datetime('now', '-7 days') }
}
```

**custom\_tags**

The custom tags for the vulnerabilities.

*Example:* get vulnerabilities which have a custom tag "is\_pci" and value "Yes".

```
{
    custom_tags: { 
        $elemMatch: { 
            name: { $eq: "is_pci" },
            value: { $eq: "Yes" }
        }
    }
}
```

**custom\_fields**

The custom fields for the vulnerabilities.

*Example:* get vulnerabilities which have a custom field "qa\_passed" and value "Yes".

```
{
    custom_fields: { 
        $elemMatch: { 
            name: { $eq: "qa_passed" },
            value: { $eq: "Yes" }
        }
    }
}
```

### Project Fields

The following fields are supported in filters for the 'Project' type:

**id**

The id for the project.

*Example:* get project with id 62a190f7793b8ccd085e0d9d

```
{
    id: { $eq: ObjectId("62a190f7793b8ccd085e0d9d") }
}
```

**created**

The created date for the project.

*Example:* get projects which have been created in the past 7 days.

```
{
    created: { $gte: datetime('now', '-7 days') }
}
```

**modified**

The modified date for the project.

*Example:* get projects which have been modified in the past 7 days.

```
{
    modified: { $gte: datetime('now', '-7 days') }
}
```

**name**

The name of the project.

*Example:* get projects which have Web in the name.

```
{
    name: { $regex: /Web/i }
}
```

**code**

The code for the project.

*Example:* get projects which have ACME in the code.

```
{
    code: { $regex: /ACME/i }
}
```

**status**

The status for the project. Must be one of the following: *Overrun, Waiting to Start, Testing, Completed, Retest, On Hold*

*Example:* get projects which are Overrun.

```
{
    status: { $eq: 'Overrun' }
}
```

**start\_date**

The start date for the project.

*Example:* get projects which have a start date in the next 7-days.

```
{
    $and: [
        {
            start_date: { $lte: datetime('now', '+7 days') }
        },
        {
            start_date: { $gte: datetime('now') }
        }
    ]
}
```

**end\_date**

The end date for the project.

*Example:* get projects which ended in the past 7-days.

```
{
    $and: [
        {
            end_date: { $gte: datetime('now', '-7 days') }
        },
        {
            end_date: { $lte: datetime('now') }
        }
    ]
}
```

**on\_hold**

Whether the project is on-hold. Supports Yes or No.

*Example:* get projects which are on-hold.

```
{
    on-hold: { $eq: "Yes" }
}
```

**organization\_code**

The project organization code.

*Example:* get projects with the organization code 'ORG123'.

```
{
    organization_code: { $eq: 'ORG123' }
}
```

**vulnerability\_code**

The vulnerability code assigned to the project.

*Example:* get projects which have 'PROJ' in the vulnerability code.

```
{
    vulnerability_code: { $regex: /PROJ/i }
}
```

**group\_id**

The linked groups on the project. Filter by group id.

*Example:* get projects linked to a group with the group id '6833f782b11edbf506b7c747'.

```
{
    group_id: { $in: [ ObjectId("6833f782b11edbf506b7c747") ] }
}
```

**group**

The linked groups on the project. Filter by group name.

*Example:* get projects linked to a group with the group name 'Pentest Team'.

```
{
    group: { $in: [ "Pentest Team" ] }
}
```

**custom\_fields**

The custom fields for the project.

*Example:* get projects which have a custom field "report\_released" and value "Yes".

```
{
    custom_fields: { 
        $elemMatch: { 
            name: { $eq: "report_released" },
            value: { $eq: "Yes" }
        }
    }
}
```

### Project Request Fields

The following fields are supported in filters for the 'Project Request' type:

**id**

The id for the project request.

*Example:* get project request with id 62a190f7793b8ccd085e0d9d

```
{
    id: { $eq: ObjectId("62a190f7793b8ccd085e0d9d") }
}
```

**created**

The created date for the project request.

*Example:* get project requests which have been created in the past 7 days.

```
{
    created: { $gte: datetime('now', '-7 days') }
}
```

**modified**

The modified date for the project request.

*Example:* get project requests which have been modified in the past 7 days.

```
{
    modified: { $gte: datetime('now', '-7 days') }
}
```

**name**

The name of the project request.

*Example:* get project requests which have Web in the name.

```
{
    name: { $regex: /Web/i }
}
```

**code**

The code for the project request.

*Example:* get project requests which have ACME in the code.

```
{
    code: { $regex: /ACME/i }
}
```

**status**

The status of the project request. Supports Pending Approval, Requested Information, Approved, and Rejected&#x20;

*Example:* get project requests which are Pending Approval.

```
{
    status: { $eq: 'Pending Approval' }
}
```

**start\_date**

The requested start date for the project request.

*Example:* get project requests which have a requested start date in the next 7-days.

```
{
    $and: [
        {
            start_date: { $lte: datetime('now', '+7 days') }
        },
        {
            start_date: { $gte: datetime('now') }
        }
    ]
}
```

**end\_date**

The requested end date for the project request.

*Example:* get projects requests which have a requested end date in the next 30 days.

```
{
    $and: [
        {
            end_date: { $lte: datetime('now', '+30 days') }
        },
        {
            end_date: { $gte: datetime('now') }
        }
    ]
}
```

**organization\_code**

The project request organization code.

*Example:* get project requests with the organization code 'ORG123'.

```
{
    organization_code: { $eq: 'ORG123' }
}
```

**custom\_fields**

The custom fields for the project request.

*Example:* get project requests which have a custom field "is\_urgent" and value "Yes".

```
{
    custom_fields: { 
        $elemMatch: { 
            name: { $eq: "is_urgent" },
            value: { $eq: "Yes" }
        }
    }
}
```

### User Fields

The following fields are supported in filters for the 'User' type:

**id**

The id for the user.

*Example:* get user with id 62a190f7793b8ccd085e0d9d

```
{
    id: { $eq: ObjectId("62a190f7793b8ccd085e0d9d") }
}
```

**created**

The created date for the user.

*Example:* get users which have been created in the past 7 days.

```
{
    created: { $gte: datetime('now', '-7 days') }
}
```

**modified**

The modified date for the user.

*Example:* get users which have been modified in the past 7 days.

```
{
    modified: { $gte: datetime('now', '-7 days') }
}
```

**first\_name**

The first name of the user.

*Example:* get users which have John in their first name.

```
{
    first_name: { $regex: /John/i }
}
```

**last\_name**

The last name of the user.

*Example:* get users which have McDonald in their last name.

```
{
    last_name: { $regex: /McDonald/i }
}
```

**username**

The username of the user.&#x20;

*Example:* get users which have attackforge in their username.

```
{
    username: { $regex: /attackforge/i }
}
```

**email**

The email address of the user.&#x20;

*Example:* get users which have attackforge.com in their email address.

```
{
    email: { $regex: /attackforge.com/i }
}
```

**last\_logged\_in**

The timestamp when the user last logged in.&#x20;

*Example:* get users which logged in in the past 7 days.

```
{
    last_logged_in: { $gte: datetime('now', '-7 days') }
}
```

**status**

The status of the user. Supports Active and Blocked.

*Example:* get users which have been manually blocked.

```
{
    status: { $eq: "Blocked" }
}
```

**role**

The role of the user. Supports client, consultant, librarymod, projectoperator and admin.

*Example:* get users who are administrators.

```
{
    role: { $eq: "admin" }
}
```

**failed\_logins**

The number of failed logins for a user.

*Example:* get users with at least 1 failed login.

```
{
    failed_logins: { $gte: 1 }
}
```

**mfa\_enabled**

Whether MFA is enabled for a user. Supports Yes or No.

*Example:* get users with MFA disabled.

```
{
    mfa_enabled: { $eq: "No" }
}
```

**last\_active\_on\_app**

The timestamp when the user was last active on the app.&#x20;

*Example:* get users which were active on the app in the past 7 days.

```
{
    last_active_on_app: { $gte: datetime('now', '-7 days') }
}
```

**last\_active\_on\_ssapi**

The timestamp when the user was last active on the self-service API.&#x20;

*Example:* get users which were active on the self-service API in the past 7 days.

```
{
    last_active_on_ssapi: { $gte: datetime('now', '-7 days') }
}
```

**account\_expiry**

The timestamp when the users account is set to expire.

*Example:* get users which are expiring in next 7 days.

```
{
    account_expiry: { $gte: datetime('now', '+7 days') }
}
```

### Writeup Fields

The following fields are supported in filters for the 'Writeup' type:

**id**

The id for the writeup.

*Example:* get writeup with id 62a190f7793b8ccd085e0d9d

```
{
    id: { $eq: ObjectId("62a190f7793b8ccd085e0d9d") }
}
```

**created**

The created date for the writeup.

*Example:* get writeups which have been created in the past 7 days.

```
{
    created: { $gte: datetime('now', '-7 days') }
}
```

**modified**

The modified date for the writeup.

*Example:* get writeups which have been modified in the past 7 days.

```
{
    modified: { $gte: datetime('now', '-7 days') }
}
```

**reference\_id**

The 15-digit reference Id for the writeup.

*Example:* get writeup with id 499319508518208

```
{
    reference_id: { $eq: "499319508518208" }
}
```

**title**

The title for the writeup.

*Example:* get writeups which have SQL in the title.

```
{
    title: { $regex: /SQL/i }
}
```

**description**

The description for the writeup.

*Example:* get writeups which have SQL in the description.

```
{
    description: { $regex: /SQL/i }
}
```

**attack\_scenario**

The attack scenario for the writeup.

*Example:* get writeups which have SQL in the attack scenario.

```
{
    attack_scenario: { $regex: /SQL/i }
}
```

**remediation\_recommendation**

The remediation recommendation for the writeup.

*Example:* get writeups which have SQL in the remediation recommendation.

```
{
    remediation_recommendation: { $regex: /SQL/i }
}
```

**likelihood\_of\_exploitation**

The likelihood of exploitation for a writeup. Supports 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.

*Example:* get writeups which have a likelihood of exploitation greater than or equal to 7.

```
{
    likelihood_of_exploitation: { $gte: 7 }
}
```

**severity**

The severity for a writeup. Supports 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.

*Example:* get writeups which have a severity greater than or equal to 7.

```
{
    severity: { $gte: 7 }
}
```

**custom\_library\_key**

The custom library key for the writeup.

*Example:* get writeups in the library 'external\_team'

```
{
    custom_library_key: { $eq: "external_team" }
}
```

**imported\_library**

Whether or not the writeup is in the imported library.

*Example:* get writeups in the imported library.

```
{
    imported_library: { $eq: "Yes" }
}
```

**project\_library**

Whether or not the writeup is in the project library.

*Example:* get writeups in the project library.

```
{
    project_library: { $eq: "Yes" }
}
```

**tags**

The tags for the writeup.

*Example:*  get writeups which have both CWE-520 and Web App in the tags.

```
{
    tags: { $all: ["CWE-520", "Web App"] }
}
```

**custom\_tags**

The custom tags for the writeups.

*Example:* get writeups which have a custom tag "is\_pci" and value "Yes".

```
{
    custom_tags: { 
        $elemMatch: { 
            name: { $eq: "is_pci" },
            value: { $eq: "Yes" }
        }
    }
}
```

**custom\_fields**

The custom fields for the writeups.

*Example:* get writeups which have a custom field "qa\_passed" and value "Yes".

```
{
    custom_fields: { 
        $elemMatch: { 
            name: { $eq: "qa_passed" },
            value: { $eq: "Yes" }
        }
    }
}
```

### Recipients

The recipients are the list of users who will be sent the custom email.

#### Vulnerability Recipients

The following vulnerability recipients are currently supported:

* Vulnerability Creator
* Project Team - Clients
* Project Team - Consultants
* Project Team - Library Moderators
* Project Team - Project Coordinators
* Project Team - Administrators
* Project Team - Everyone
* Project Group Members - Clients
* Project Group Members - Consultants
* Project Group Members - Library Moderators
* Project Group Members - Project Coordinators
* Project Group Members - Administrators
* Project Group Members - Everyone
* Clients
* Consultants
* Library Moderators
* Project Coordinators
* Administrators
* Individual Groups (select groups by name)
* Individual Users (select users by name/email)
* User or User(s) Custom Field on the Vulnerability
* Group or Group(s) Custom Field on the Vulnerability
* User or User(s) Custom Field on the Project linked to the Vulnerability
* Group or Groups(s) Custom Field on the Project linked to the Vulnerability

Every vulnerability recipient will receive a personalised vulnerability list based on vulnerabilities & projects for which they have access to.

#### Project Recipients

The following project recipients are currently supported:

* Project Creator
* Project Team - Clients
* Project Team - Consultants
* Project Team - Library Moderators
* Project Team - Project Coordinators
* Project Team - Administrators
* Project Team - Everyone
* Project Group Members - Clients
* Project Group Members - Consultants
* Project Group Members - Library Moderators
* Project Group Members - Project Coordinators
* Project Group Members - Administrators
* Project Group Members - Everyone
* Clients
* Consultants
* Library Moderators
* Project Coordinators
* Administrators
* Individual Groups (select groups by name)
* Individual Users (select users by name/email)
* User or User(s) Custom Field on the Project
* Group or Group(s) Custom Field on the Project

Every project recipient will receive a personalised project list based on projects for which they have access to.

#### Project Request Recipients

The following project request recipients are currently supported:

* Project Requestor Creator
* Clients
* Consultants
* Library Moderators
* Project Coordinators
* Administrators
* Individual Groups (select groups by name)
* Individual Users (select users by name/email)
* User or User(s) Custom Field on the Project Request
* Group or Group(s) Custom Field on the Project Request

Every project request recipient will receive a personalised project request list based on project requests for which they have access to.

#### User Recipients

The following user recipients are currently supported:

* Clients
* Consultants
* Library Moderators
* Project Coordinators
* Administrators
* Individual Groups (select groups by name)
* Individual Users (select users by name/email)

#### Writeup Recipients

The following writeup recipients are currently supported:

* Clients
* Consultants
* Library Moderators
* Project Coordinators
* Administrators
* Individual Groups (select groups by name)
* Individual Users (select users by name/email)
* User or User(s) Custom Field on the Writeup
* Group or Group(s) Custom Field on the Writeup

Every user recipient will receive a personalised writeup list based on writeups for which they have access to. Admins will see all writeups. Non-admins will only see writeups based on the libraries which they have access to.

#### Skip Access Check

This option will skip checking access for the recipient when filtering data for the email. This option is intended when the data needs to be sent to the recipient, regardless of whether they have access to the data i.e. vulnerability/project/project request/writeup/etc.

> **WARNING:** When this option is enabled, recipients may receive more data in the email than they have access to.

### Subject

This is the subject name/title for custom email that will be delivered to recipients.

### Body

This is the body/contents of the email that will be delivered to recipients. Emails are HTML enabled meaning you can include things like headings, lists, tables and images.

Every body will have different meta-tags which are supported based on the custom email type. For a full list of supported tags, please check below:

### Vulnerability Metatags

* **{user.firstName}** - recipient first name&#x20;
* **{user.lastName}** - recipient last name
* **{vulnerabilities}...{/vulnerabilities}** - execute a loop for every vulnerability. Place tags below in-between these tags.&#x20;
  * **{vuln.id}** - vulnerability id
  * **{vuln.created}** - vulnerability created date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.modified}** - vulnerability modified date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.priority}** - vulnerability priority. Either Critical/High/Medium/Low/Info
  * **{vuln.title}** - vulnerability title
  * **{vuln.description}** - vulnerability description
  * **{vuln.attack\_scenario}** - vulnerability attack scenario
  * **{vuln.remediation\_recommendation}** - vulnerability remediation recommendation
  * **{vuln.proof\_of\_concept}** - vulnerability proof of concept / steps to reproduce
  * **{vuln.status}** - vulnerability status. Either Open/Retest/Closed
  * **{vuln.status\_updated}** - vulnerability status last updated date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.tags}** - list of vulnerability tags. Displayed in unordered list format \<ul>\<li>\[tag]\</li>\</ul>
  * **{vuln.zero\_day}** - vulnerability zero day status. Either Yes or No
  * **{vuln.likelihood\_of\_exploitation}** - vulnerability likelihood of exploitation. Displayed in format 1/10...10/10.
  * **{vuln.notes}** - notes associated with the vulnerability. Each note is displayed in a new paragraph e.g. \<p>\[note]\</p>
  * **{vuln.test\_cases}** - list of vulnerability test cases. Displayed in unordered list format \<ul>\<li>\[test\_case]\</li>\</ul>
  * **{vuln.alternate\_id}** - vulnerability alternative id (from project vulnerability code)
  * **{vuln.sla\_date}** - vulnerability sla date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.target\_remediation\_date}** - vulnerability target remediation date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.release\_date}** - vulnerability release date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.cvssv3}** - vulnerability CVSSv3 score e.g. 7.6
  * **{vuln.cvssv3\_vector}** - vulnerability CVSSv3 vector e.g. CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:H
  * **{vuln.cvssv3\_base\_score}** - vulnerability CVSSv3 base score e.g. 7.1
  * **{vuln.cvssv3\_temporal\_score}** - vulnerability CVSSv3 temporal score e.g. 7.6
  * **{vuln.cvssv3\_environmental\_score}** - vulnerability CVSSv3 environmental score e.g. 6.9
  * **{vuln.custom\_tag\_\[name]}** - vulnerability custom tag. Replace \[name] with the name for your custom tag.
  * **{vuln.custom\_field\_\[key]}** - vulnerability custom field. Replace \[key] with the key for your custom field.
  * **{vuln.library\_id}** - linked vulnerability library write-up id
  * **{vuln.library\_created}** - linked vulnerability library write-up created date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.library\_modified}** - linked vulnerability library write-up modified date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.library\_code}** - linked vulnerability library write-up code
  * **{vuln.library\_tags}** - linked vulnerability library write-up tags. Displayed in unordered list format \<ul>\<li>\[tag]\</li>\</ul>
  * **{vuln.library\_import\_source}** - linked vulnerability library write-up import source
  * **{vuln.library\_import\_source\_id}** - linked vulnerability library write-up import source id
  * **{vuln.library\_custom\_tag\_\[name]}** - linked vulnerability library write-up custom tag. Replace \[name] with the name for your custom tag.
  * **{vuln.library\_custom\_field\_\[key]}** - linked vulnerability library write-up custom field. Replace \[key] with the key for your custom field.
  * **{vuln.asset}** - vulnerability asset name
  * **{vuln.asset\_id}** - vulnerability asset library id
  * **{vuln.asset\_created}** - vulnerability asset library created date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.asset\_modified}** - vulnerability asset library modified date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
  * **{vuln.asset\_external\_id}** - vulnerability asset library external id
  * **{vuln.asset\_type}** - vulnerability asset library type
  * **{vuln.asset\_details}** - vulnerability asset library details
  * **{vuln.asset\_custom\_field\_\[key]}** - vulnerability asset library custom field. Replace \[key] with the key for your custom field.
  * **{vuln.assets}...{/vuln.assets}** - execute a loop for every asset on the vulnerability. Place tags below in-between these tags.&#x20;
    * **{vuln.asset.name}** - vulnerability asset name
    * **{vuln.asset.id}** - vulnerability asset library id
    * **{vuln.asset.created}** - vulnerability asset library created date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
    * **{vuln.asset.modified}** - vulnerability asset library modified date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
    * **{vuln.asset.asset\_external\_id}** - vulnerability asset library external id
    * **{vuln.asset.asset\_type}** - vulnerability asset library type
    * **{vuln.asset.asset\_details}** - vulnerability asset library details
    * **{vuln.asset.asset\_custom\_field\_\[key]}** - vulnerability asset library custom field. Replace \[key] with the key for your custom field.
  * **{vuln.user\_id}** - vulnerability creator user id
  * **{vuln.user\_first\_name}** - vulnerability creator user first name
  * **{vuln.user\_last\_name}** - vulnerability creator user last name
  * **{projects}...{/projects}** - execute a loop for every linked project for this vulnerability. Place tags below in-between these tags.&#x20;
    * **{vuln.project\_id}** - vulnerability linked project id
    * **{vuln.project\_created}** - vulnerability linked project created date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
    * **{vuln.project\_modified}** - vulnerability linked project modified date in DAY MONTH DATE YEAR format e.g. Sat July 02 2022
    * **{vuln.project\_name}** - vulnerability linked project name
    * **{vuln.project\_code}** - vulnerability linked project code
    * **{vuln.project\_start\_date}** - vulnerability linked project start date
    * **{vuln.project\_end\_date}** - vulnerability linked project end date
    * **{vuln.project\_vuln\_link}** - link to the vulnerability page on the project in AttackForge. Displayed as a hyperlink with text View
    * **{vuln.project\_link}** - link to the project dashboard for the vulnerability on the project in AttackForge. Displayed as a hyperlink with text View
    * **{vuln.project\_custom\_tag\_\[name]}** - vulnerability linked project custom tag. Replace \[name] with the name for your custom tag.
    * **{vuln.project\_custom\_field\_\[key]}** - vulnerability linked project custom field. Replace \[key] with the key for your custom field.

### Project Metatags

* **{user.firstName}** - recipient first name&#x20;
* **{user.lastName}** - recipient last name
* **{projects}...{/projects}** - execute a loop for every project. Place tags below in-between these tags.
  * **{project.id}** - project id
  * **{project.created}** - created time for the project
  * **{project.modified}** - last modified time for the project
  * **{project.name}** - project name
  * **{project.code}** - project code
  * **{project.start\_date}** - project start date
  * **{project.end\_date}** - project end date
  * **{project.organization\_code}** - project organization code
  * **{project.on\_hold}** - whether project is on-hold or off-hold. Returns Yes or No
  * **{project.vulnerability\_code}** - project vulnerability code
  * **{project.custom\_tag\_\[name]}** - project custom tag. Replace \[name] with the name for your custom tag.
  * **{project.custom\_field\_\[key]}** - project custom field. Replace \[key] with the key for your custom field.
  * **{project.user.id}** - the user id for the user who created the project
  * **{project.user.first\_name}** - the first name for the user who created the project
  * **{project.user.last\_name}** - the last name for the user who created the project
  * **{project.link}** - a hyperlink to the project
  * **{project.total\_vulnerabilities}** - the total number of vulnerabilities on the project
  * **{project.total\_zero\_day\_vulnerabilities}** - the total number of vulnerabilities on the project
  * **{project.total\_easily\_exploitable\_vulnerabilities}** - the total number of easily exploitable vulnerabilities on the project&#x20;
  * **{project.total\_owasp\_top\_10\_vulnerabilities}** - the total number of OWASP Top 10 vulnerabilities on the project&#x20;
  * **{project.total\_cwe\_top\_25\_vulnerabilities}** - the total number of CWE Top 25 vulnerabilities on the project
  * **{project.total\_critical\_vulnerabilities}** - the total number of critical vulnerabilities on the project&#x20;
  * **{project.total\_high\_vulnerabilities}** - the total number of high vulnerabilities on the project&#x20;
  * **{project.total\_medium\_vulnerabilities}** - the total number of medium vulnerabilities on the project
  * **{project.total\_low\_vulnerabilities}** - the total number of low vulnerabilities on the project&#x20;
  * **{project.total\_info\_vulnerabilities}** - the total number of info vulnerabilities on the project&#x20;
  * **{project.total\_pending\_vulnerabilities}** - the total number of pending vulnerabilities on the project
  * **{project.total\_testcases}** - the total number of test cases assigned to the project
  * **{project.total\_not\_tested\_testcases}** - the total number of not tested test cases on the project
  * **{project.total\_tested\_testcases}** - the total number of tested test cases on the project
  * **{project.total\_in\_progress\_testcases}** - the total number of in progress test cases on the project
  * **{project.total\_not\_applicable\_testcases}** - the total number of not applicable test cases on the project
  * **{project.total\_open\_vulnerabilities}** - the total number of open vulnerabilities on the project&#x20;
  * **{project.total\_closed\_vulnerabilities}** - the total number of closed vulnerabilities on the project&#x20;
  * **{project.total\_retest\_vulnerabilities}** - the total number of ready for retest vulnerabilities on the project&#x20;
  * **{project.total\_retests\_requested}** - the total number of requested retests on the project
  * **{project.total\_retests\_completed}** - the total number of completed retests on the project
  * **{project.total\_assets}** - the total number of assets on the project
  * **{project.total\_critical\_open\_vulnerabilities}** - the total number of critical open vulnerabilities on the project&#x20;
  * **{project.total\_critical\_retest\_vulnerabilities}** - the total number of critical ready for retest vulnerabilities on the project&#x20;
  * **{project.total\_critical\_closed\_vulnerabilities}** - the total number of critical closed vulnerabilities on the project&#x20;
  * **{project.total\_high\_open\_vulnerabilities}** - the total number of high open vulnerabilities on the project&#x20;
  * **{project.total\_high\_retest\_vulnerabilities}** - the total number of high ready for retest vulnerabilities on the project&#x20;
  * **{project.total\_high\_closed\_vulnerabilities}** - the total number of high closed vulnerabilities on the project&#x20;
  * **{project.total\_medium\_open\_vulnerabilities}** - the total number of medium open vulnerabilities on the project&#x20;
  * **{project.total\_medium\_retest\_vulnerabilities}** - the total number of medium ready for retest vulnerabilities on the project&#x20;
  * **{project.total\_medium\_closed\_vulnerabilities}** - the total number of medium closed vulnerabilities on the project&#x20;
  * **{project.total\_low\_open\_vulnerabilities}** - the total number of low open vulnerabilities on the project&#x20;
  * **{project.total\_low\_retest\_vulnerabilities}** - the total number of low ready for retest vulnerabilities on the project&#x20;
  * **{project.total\_low\_closed\_vulnerabilities}** - the total number of low closed vulnerabilities on the project&#x20;
  * **{project.total\_info\_open\_vulnerabilities}** - the total number of info open vulnerabilities on the project&#x20;
  * **{project.total\_info\_retest\_vulnerabilities}** - the total number of info ready for retest vulnerabilities on the project&#x20;
  * **{project.total\_info\_closed\_vulnerabilities}** - the total number of info closed vulnerabilities on the project

### Project Request Metatags

* **{user.firstName}** - recipient first name&#x20;
* **{user.lastName}** - recipient last name
* **{project\_requests}...{/project\_requests}** - execute a loop for every project request. Place tags below in-between these tags.
  * **{project\_request.id}** - project request id
  * **{project\_request.created}** - project request created timestamp
  * **{project\_request.modified}** - project request last modified timestamp
  * **{project\_request.name}** - project request name
  * **{project\_request.code}** - project request code
  * **{project\_request.start\_date}** - project request anticipated start date
  * **{project\_request.end\_date}** - project request anticipated end date
  * **{project\_request.status}** - project request status
  * **{project\_request.organization\_code}** - project request organization code
  * **{project\_request.custom\_field\_\[key]}** - project request custom field. Replace \[key] with the key for your custom field.
  * **{project\_request.user.id}** - the user id for the user who created the project request
  * **{project\_request.user.first\_name}** - the first name for the user who created the project request
  * **{project\_request.user.last\_name}** - the last name for the user who created the project request
  * **{project\_request.link}** - a hyperlink to view the project request

### User Metatags

* **{user.firstName}** - recipient first name&#x20;
* **{user.lastName}** - recipient last name
* **{users}...{/users}** - execute a loop for every user. Place tags below in-between these tags.
  * **{user.id}** - user id
  * **{user.created}** - user created timestamp
  * **{user.modified}** - user last updated timestamp
  * **{user.first\_name}** - user first name
  * **{user.last\_name}** - user last name
  * **{user.last\_logged\_in}** - user last logged in timestamp
  * **{user.username}** - user username
  * **{user.email}** - user email address
  * **{user.status}** - user status. Returns Active or Blocked
  * **{user.role}** - user role. Returns client, consultant, librarymod, projectoperator, admin&#x20;
  * **{user.failed\_logins}** - user failed logins. Returns a number
  * **{user.mfa\_enabled}** - whether MFA is enabled for the user. Returns Yes or No
  * **{user.last\_active\_on\_app}** - user last active on the app timestamp&#x20;
  * **{user.last\_active\_on\_ssapi}** - user last active on the Self-Service APIs timestamp
  * **{user.account\_expiry}** - user expiry timestamp

### Writeup Metatags

* **{user.firstName}** - recipient first name&#x20;
* **{user.lastName}** - recipient last name
* **{writeups}...{/writeups}** - execute a loop for every writeup. Place tags below in-between these tags.
  * **{writeup.id}** - writeup id
  * **{writeup.created}** - writeup created timestamp
  * **{writeup.modified}** - writeup last updated timestamp
  * **{writeup.title}** - writeup title
  * **{writeup.reference\_id}** - writeup reference id. Returns 15-digit number.
  * **{writeup.description}** - writeup description
  * **{writeup.attack\_scenario}** - writeup attack scenario
  * **{writeup.remediation\_recommendation}** - writeup remediation recommendation
  * **{writeup.likelihood\_of\_exploitation}** - writeup likelihood of exploitation. Returns number between 1 to 10.
  * **{writeup.severity}** - writeup severity. Returns number between 1 to 10.
  * **{writeup.custom\_library\_key}** - writeup custom library key.
  * **{writeup.imported\_library}** - whether writeup is in Imported library. Returns Yes or No.
  * **{writeup.project\_library}** - whether writeup is in Project library. Returns Yes or No.&#x20;
  * **{writeup.tags}** - writeup tags. Returns an unordered list.
  * **{writeup.custom\_tag\_\[name]}** - writeup custom tag. Replace \[name] with the name for your custom tag.
  * **{writeup.custom\_field\_\[key]}** - writeup custom field. Replace \[key] with the key for your custom field.

### Examples

***Example 1***

The following example is used to notify customers/engineers, security team administrators & compliance team about vulnerabilities they have access to for which the SLA will be exceeded/breached in exactly 7-days from now.

#### Type

Vulnerabilities

#### Filter

```
{
    $and: [
         {
                status: { $eq: 'Open' }
         },
         {
                sla : { $lte: datetime('now', '+7 days') }
         },
         {
                sla: { $gte: datetime('now', '+6 days') }
         }
     ]
}
```

#### Recipients

* Project Team - Clients
* Project Group Members - Clients
* Administrators
* Compliance Team (Group)

#### Subject

```
Vulnerability SLA Breaches In 7-days
```

#### Body

```
<p style="text-align: center; font-size: 20px;"><b>Vulnerability SLA Breaches In 7-days</b></p>
<p>Hi {user.firstName},</p>
<p>The following vulnerabilities will breach their SLAs in 7-days.<p>
<p>Please ensure you have reached out to the security team to schedule a retest, or discuss remediation plan.</p>
<p>
<table style="border: 1px solid;">
<tr>
<th style="width: 35%; text-align: center; border: 1px solid; background-color: #0a1f33; color: white;">Vulnerability</th>
<th style="width: 10%; text-align: center; border: 1px solid; background-color: #0a1f33; color: white;">Priority</th>
<th style="width: 15%; text-align: center; border: 1px solid; background-color: #0a1f33; color: white;">SLA</th>
<th style="width: 40%; text-align: center; border: 1px solid; background-color: #0a1f33; color: white;">Projects</th>
</tr>
{vulnerabilities}
<tr style="vertical-align: text-top;">
<td style="border: 1px solid;">{vuln.title}</td>
<td style="border: 1px solid; text-align: center;">{vuln.priority}</td>
<td style="border: 1px solid;">{vuln.sla}</td>
<td style="border: 1px solid;">{projects}{vuln.project_name} - {vuln.project_vuln_link}<br/>{/projects}</td>
</tr>
{/vulnerabilities}
</table>
</p>
```

#### Output

The following email will be sent to customers/engineers, security team administrators & compliance team with personalised vulnerability list based on vulnerabilities & projects for which they have access to.

<figure><img src="/files/xtgn93NUfDEvD8JnLsYq" alt=""><figcaption></figcaption></figure>

### Dynamic CSS Styles

You can use inline CSS to configure custom dynamic styles, for example to change the color of your vulnerability priority in the email:

1. Configure the following inline CSS classes in your email `Header` template configuration:

{% code overflow="wrap" %}

```css
<style type="text/css">.critical{ background-color: maroon; }.high{ background-color: red; }.medium{ background-color: yellow; }.low{ background-color: blue; }.info{ background-color: green; }</style>
```

{% endcode %}

<figure><img src="/files/1QNXB2FcxCZ5z1Z5wlOL" alt=""><figcaption></figcaption></figure>

2. Reference the inline CSS classes dynamically using tags in your email Body:

{% code overflow="wrap" %}

```javascript
<td class="{vuln.priority}" style="border: 1px solid; text-align: center;">{vuln.priority}</td>
```

{% endcode %}

<figure><img src="/files/wW2pEeXN6pQm9sJnmXCI" alt=""><figcaption></figcaption></figure>

### Filter Examples

**Filter for Open Critical or Open High vulnerabilities that have exceeded their SLA by 3 days**

```
{
       $and: [
              {
                     status: { $eq: 'Open' }
              },
              {
                     $or: [
                            {
                                   priority: { $eq: 'Critical' }
                            },
                            {
                                   priority: { $eq: 'High' }
                            }
                     ]
              },
              {
                     sla : { $lte: datetime('now', '-2 days') }
              },
              {
                     sla: { $gte: datetime('now', '-3 days') }
              }
       ]
}
```

**Filter for Open Critical or Open High vulnerabilities that have a target remediation date in 7-days time**

```
{
       $and: [
              {
                     status: { $eq: 'Open' }
              },
              {
                     $or: [
                            {
                                   priority: { $eq: 'Critical' }
                            },
                            {
                                   priority: { $eq: 'High' }
                            }
                     ]
              },
              {
                     target_remediation_date : { $lte: datetime('now', '+7 days') }
              },
              {
                     target_remediation_date: { $gte: datetime('now', '+6 days') }
              }
       ]
}
```

## Custom Date/Time Formats

Email meta tags support the [dateFormat](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateformat) filter which can be applied to format the timestamp to a desired date and/or time.

## User/Group Custom Fields

You can iterate over user/group custom fields using the following syntax:

{% code overflow="wrap" %}

```
{<PROPERTY>.custom_field_<KEY>.each} User ID: {id} First Name: {first_name} Last Name: {last_name} Email Address: {email_address}{/<PROPERTY>.custom_field_<KEY>.each}
```

{% endcode %}

Where \<PROPERTY> is the property the custom field belongs to; and \<KEY> is the custom field key.

## Header and Footer

### Basic configuration

```html
<html>
  <body>
    <style type="text/css">
      .critical { background-color: maroon; color: #FFFFFF}
        .high { background-color: red; color: #FFFFFF}
          .medium { background-color: yellow; color: #FFFFFF}
            .low { background-color: blue; color: #FFFFFF}
              .info { background-color: green; color: #FFFFFF}
    </style>
    <table style='width:900px; background:#0a1f33; display:block;'>
      <tr>
        <td style='padding-top:30px;'>
        </td>
      </tr>
    </table>
    <table style="width:900px; background:#fff; color:#222222; font-family:Helvetica,Arial,sans-serif; text-align:left; font-size:14px; padding-top:10px; padding-left:10px; padding-bottom:10px;">
      <tr>
        <td>
          <p>
            {content}
          </p>
        </td>
      </tr>
    </table>
    <table style='width:900px; background:#0a1f33; display:block;'>
      <tr>
        <td style='padding-top:10px; padding-left:10px; padding-bottom:10px; color:#fff;'>
          <p style='font-size:12px;'>
            <i>This is a system generated email and reply is not required.</i>
          </p>
          <p style='font-size:14px; padding-top:10px;'>
            2018-{now | dateFormat:["yyyy"]} AttackForge&reg; Enterprise
          </p>
        </td>
      </tr>
    </table>
  </body>
</html>
```

<figure><img src="/files/c0K5mnTIXOhCRZXImIEt" alt=""><figcaption></figcaption></figure>

### Advanced Configuration

```html
<!doctype html>
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" style="color-scheme: light dark">
  <head>
    <meta charset="utf-8">
    <meta name="x-apple-disable-message-reformatting">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
    <meta name="color-scheme" content="light dark">
    <meta name="supported-color-schemes" content="light dark">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;700&display=swap" rel="stylesheet" media="screen">
    <style>
      .body-sub {
        margin-top: 25px;
        border-top-width: 1px;
        padding-top: 25px;
        border-top-color: #eaeaec;
        border-top-style: solid;
      }
      body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer {
        background-color: #1e293b !important;
        color: #fff !important;
      }
      p, ul, ol, blockquote, h1, h2, h3 {
        color: #fff !important;
      }
      .sm-w-full {
        width: 100% !important;
      }
      h1 {
        margin-top: 0; 
        text-align: left; 
        font-size: 24px; 
        font-weight: 700; 
        color: #333333
      }
      p {
        margin-bottom: 5px; 
        margin-top: 6px; 
        font-size: 16px; 
        line-height: 24px; 
        color: #51545e
      }
      .styled-table {
        width: 600px;
        border-collapse: collapse;
      }
      .styled-table th {
        border-collapse: collapse;
        border: 1px solid white;
      }
      .styled-table td {
        border-collapse: collapse;
        border: 1px solid white;
        padding-left: 20px;
      }
      .styled-button {
        border: none;
        border-collapse: collapse;
      }
      .styled-button td {
        border: 1px solid;
        border-radius: 5px;
        border-color: transparent;
        background-color: #469cf0 !important;
        padding: 15px 30px;
      }
      .styled-button a {
        background-color: #469cf0 !important;
        display: inline-block;
        font-size: 17px;
        color: #ffffff;
        text-decoration: none;
        font-family: sans-serif;
      }
    </style>
    <!--[if mso]>
      <style>
        .styled-button td {
          border: 1px solid;
          border-radius: 5px;
          border-color: #469cf0;
          background-color: #469cf0 !important;
          mso-padding-alt: 15px 30px;
        }
      </style>
    <![endif]-->
  </head>
  <body style="margin: 0; width: 100%; background-color: #f2f4f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
    <div role="article" aria-roledescription="email" aria-label lang="en">
      <table
        class="email-wrapper" style="width: 100%; background-color: #1e293b; font-family: &quot;Inter&quot;, ui-sans-serif, system-ui, -apple-system, &quot;Segoe UI&quot;, sans-serif;"
        cellpadding="0"
        cellspacing="0"
        role="none"
      >
        <tr>
          <td align="center">
            <table class="email-content" style="width: 100%" cellpadding="0" cellspacing="0" role="none">
              <tr>
                <td align="center" class="email-masthead" style="display: flex; justify-content: center; gap: 8px; padding-top: 25px; padding-bottom: 25px; text-align: center; font-size: 16px">
                  <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAABnCAYAAADc1TyNAAAEDmlDQ1BrQ0dDb2xvclNwYWNlR2VuZXJpY1JHQgAAOI2NVV1oHFUUPpu5syskzoPUpqaSDv41lLRsUtGE2uj+ZbNt3CyTbLRBkMns3Z1pJjPj/KRpKT4UQRDBqOCT4P9bwSchaqvtiy2itFCiBIMo+ND6R6HSFwnruTOzu5O4a73L3PnmnO9+595z7t4LkLgsW5beJQIsGq4t5dPis8fmxMQ6dMF90A190C0rjpUqlSYBG+PCv9rt7yDG3tf2t/f/Z+uuUEcBiN2F2Kw4yiLiZQD+FcWyXYAEQfvICddi+AnEO2ycIOISw7UAVxieD/Cyz5mRMohfRSwoqoz+xNuIB+cj9loEB3Pw2448NaitKSLLRck2q5pOI9O9g/t/tkXda8Tbg0+PszB9FN8DuPaXKnKW4YcQn1Xk3HSIry5ps8UQ/2W5aQnxIwBdu7yFcgrxPsRjVXu8HOh0qao30cArp9SZZxDfg3h1wTzKxu5E/LUxX5wKdX5SnAzmDx4A4OIqLbB69yMesE1pKojLjVdoNsfyiPi45hZmAn3uLWdpOtfQOaVmikEs7ovj8hFWpz7EV6mel0L9Xy23FMYlPYZenAx0yDB1/PX6dledmQjikjkXCxqMJS9WtfFCyH9XtSekEF+2dH+P4tzITduTygGfv58a5VCTH5PtXD7EFZiNyUDBhHnsFTBgE0SQIA9pfFtgo6cKGuhooeilaKH41eDs38Ip+f4At1Rq/sjr6NEwQqb/I/DQqsLvaFUjvAx+eWirddAJZnAj1DFJL0mSg/gcIpPkMBkhoyCSJ8lTZIxk0TpKDjXHliJzZPO50dR5ASNSnzeLvIvod0HG/mdkmOC0z8VKnzcQ2M/Yz2vKldduXjp9bleLu0ZWn7vWc+l0JGcaai10yNrUnXLP/8Jf59ewX+c3Wgz+B34Df+vbVrc16zTMVgp9um9bxEfzPU5kPqUtVWxhs6OiWTVW+gIfywB9uXi7CGcGW/zk98k/kmvJ95IfJn/j3uQ+4c5zn3Kfcd+AyF3gLnJfcl9xH3OfR2rUee80a+6vo7EK5mmXUdyfQlrYLTwoZIU9wsPCZEtP6BWGhAlhL3p2N6sTjRdduwbHsG9kq32sgBepc+xurLPW4T9URpYGJ3ym4+8zA05u44QjST8ZIoVtu3qE7fWmdn5LPdqvgcZz8Ww8BWJ8X3w0PhQ/wnCDGd+LvlHs8dRy6bLLDuKMaZ20tZrqisPJ5ONiCq8yKhYM5cCgKOu66Lsc0aYOtZdo5QCwezI4wm9J/v0X23mlZXOfBjj8Jzv3WrY5D+CsA9D7aMs2gGfjve8ArD6mePZSeCfEYt8CONWDw8FXTxrPqx/r9Vt4biXeANh8vV7/+/16ffMD1N8AuKD/A/8leAvFY9bLAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAJYoAMABAAAAAEAAABnAAAAAOLtLv4AADQ/SURBVHgB7Z0HvBXF9cfvAykC9oqiYgNjj738Y9eoqAkW1GgUC2JDjGKsMcbYS2woYAmKLfZeExVr7CViF0GsKFZEEIX7//6e78pl3+zu7N699Z3z+fze3p05c+bMb2d3z8zO7svlaljy+XwXsE4SF9GfH6yWpIzpGgPGgDFgDBgDxoAx0CYYIEjaBjwPbkrSYPR7ga/AhaBHkrKmawwYA8aAMWAMGAPGQEMyQFC0CrgRzASSK5M0FP2eYIoKIh+Bw0HXJDZM1xgwBowBY8AYMAaMgYZggCBoUXAW+BYUy7AkDaTgYuDLYgP8fhH0TWLHdI0BY8AYMAaMAWPAGKhbBgh8OoODwHjgkrOSNA4DWoP1icsQaXeCtZLYM11jwBgwBowBY8AYMAbqigGCnW3B0yBKjknSKAxpYfy7EQa/J0/rs5ZIYtd0jQFjwBgwBowBY8AYqGkGCG5WAzcBH9kvSWMw2AS0OD5OPkbhCGDrs5IQbLrGgDFgDBgDxoAxUFsMEMxondU5YDLwlT5JW4Hh+3yNo/cS2DFpHaZvDBgDxoAxYAwYA8ZAVRkggNE6q0PA+yCJzEB5zaTOU+ayJJW06N7Fdu2kdZm+MWAMGAPGgDFgDBgDFWeAoKUPeLYliEm6+ZoCiyd1mjLHJ62oRX8q26FgyaR1mr4xYAwYA8aAMWAMGANlZ4AgZXVwCyhF3qJwp6TOUqZfKZVSVm8hDgHdktZt+saAMWAMGAPGgDFgDGTOAEFJd3Au+A6UKvencZBK1wB6vFiqvIyBndL4YGWMAWPAGDAGjAFjwBgomQECkTnBoWACyErOSeMYlS8AJmblBHbuAeum8cXKGAPGgDFgDBgDxoAxkIoBgo/twXMga9kjjUM4oU81PJWxM9OwdwlYKo1PVsYYMAaMAWPAGDAGjAEvBgg2fg1uBeWQ6RhdxcsRhxJltVi9HKL1WUeBuRzVWpIxYAwYA8aAMWAMGAPpGCC40P/7+wfIYp0VZpwyltQu6TzM5Si7p9NqdomvYGqXtP5ZOWPAGDAGjAFjwBgwBpoZIKDQv6E5DHwAyi03lEI7zvUG+uxCueVeKlivFF+trDFgDBgDxoAxYAy0UQYIInYAPv+CJquA5qBSqMaJDkBvAVZCtD5rGOhZis9W1hgwBowBY8AYMAbaCAMEDfrkwe2gkvIDla1UKsXYuKCSTlPXp+BoYOuzSj14Vt4YMAaMAWPAGGhEBggSFgfngymg0vICFc5RKq/Y2LrSjrfU9ypbfey0qdQ2WHljwBgwBowBY8AYaAAGCAq0zupw8CGolpyaBZU4Py+oZjvup/71s2iL2TAGjAFjwBgwBoyBOmWAYOD3QLNH1ZSfqHydrCjE1shqNoa69bhzBFg6qzaZHWPAGDAGjAFjwBioAwa4+a8J7gC1IC/iRIesaMPWtrXQKHzQl+WPAfNk1TazYwwYA8aAMWAMGAM1ygA3/APB96BW5JgsqaJRXcE7tdI4/HgGWJCV5UE2W8aAMWAMGAPGQI0x0A5/VgNz1ohf3+HHjVn60tTUNCVrmyX615vytcJ3iU2x4saAMWAMGAPGgDHgYkAB1o+ujCql3UdA9F4Z6r4am1PLYDeNyZ8olE9T0MoYA8aAMWAMGAPGQH0woACrVkRBx6XlcIag7U3s3lcO22bTGDAGjAFjwBgwBoyBIAMlf2sqaLCE/WcoO7qE8nFFL0ahL7BvU8UxZfnGQJ0xwLrGbrjcFejxuwaOGrBp1vo7BlhaemDShhigP+g6z6FvmtmGml1zTeU4KMboDAqTOXpiNo3j0iae4tRSgHUBpOvxWbnkUQw/AX5Trgrakl1OHL3puYhnm3/i2H7qqetUa7lgqr5a6rNOX8uYqJvFp6XeNOByYex09PTzc+r7wVO3Imr4Px8Vae3oumB1oE+gqG/oPyh0AoUAazq/v0H/Y7avgafBU7RHM9qpBXsLUlg3jTj5lrq+jVNy5VOH/tH9/K68kLT2pH/g6hvYUrCxKJBONWQ6fn1Wjopp20LYXRGsCnqBJYD6h/hrT/40tgqwJ4JxQP3gVfAuPql/lE2oW0H/vGWoYAY2p6TtW2Xwp9kk7dV1RcdhDaD/xLIU0LnSfCzYSsT51+h+yPYN8AJ4nra8zzYzwb5eJKvmf1SZTJu+acKRC3FkUGYtS2foFYqth0M6GcomtHV7jN9Ztgr8DH+B2kq0VSd83QpcHo3zx3s2YCp6m9FmXdxSCfXphqYb5DKpDDRGoUk0Yx141DaVwKNuQE8C3wv/cOr7c6rKMiyE3woItwC7gk2B2pFG1BefA9eDW2jb50mN4Mu/KLOtR7nTsH+Gh95sKtjXjeEOsNZsGeE7GnTcDPajvlZrarGnWb1nQE9QDXkav7bKqmLaoxu5+O8L1gEKHpOIBgxvgf8A8Sb/Mp9Rwc8DsX0WyFo0EaGg8RMwBjwGHqENE9hWVGijglkdi52BPqq9CEgq31JA56SOxe2049OkBoL6+HUiaUOC6RXc/wftOCmHIxeCasvulWg4jZwDPFblxk6i/jSdsBIUedWB//OA8Ql5HOplPESJujqDsQnrbDR19R2N2FML5U9KSMrn6HdPXWGJBam7I+gPyvEB5AnYPQEskMRN9O8EPnJSErvSxagGvVf6GC/SeYnfof2CPP13jnGgWvJUUh5c+ji/FDgTZPnfOWZg7xGwI9DMZ2aCvcGgUvIlFV0N1s6sARGGqGdhoHNnHMhSPsHYWUCzX6mF8qdl6VQKW80Dq0w7VEo2NLK6xbfs8kPz6y4/Ij98+WH5x3oNz98Hjuh5Xt5rNE5Eqcj/78Cey/sS7tbrR3LSE2B3OmkPtznv1LZ+3EpqP/xrtLmvN9s/Ky7Ipn/CMpmo4+/GGHoEjAR67JC1aBZM14P/UtduCYznPXV99YrNHc3O3sUJMb8/IL8f17a4mbiS+k6MD3HZJdXNsdHgSrOozwJtF4+rMEG+7oGbAN2D9K/N1mKblaQ5/mnr1rm9J3icNpwP5k5rKKocdjUA2B8dHQudOz1BlqLZyKOAvhc5BOiRfxqpJPcu/5rrr3aApRPvr1wcvJ6FLzc0v39Tx9zodnPkBja1Zy1V+9zW7J/bsXPuwV7D8r4n3X+os9qPCV0HpC7SWjr8wSmcnZ8y+6QoZ0WyY0AzxWkerQ3guJflgu1qGnV1AH8j7wGwgUsn47TlsXc9dY4E6qdVEer+PRWfnKDyyej+gevnOwnK1JUqnKyIw+oHZ4KFy+z8lth/hDqPBE1lrqtc5hWQDAYP0oZls6wEe7rHKhC9DCyVpW2HLT3lORs8QL0rO/JrPam5/+jZfTXlNi4OOnlipfcl+VVntstdwLtBnWf+MEs9/yMrWjvn1p45jbyfnwPPynT8oj6OV/4vZOlk6upQsaRoBrYme/VoldDc/eH+Io7B16EallEWBuC9M4YPSml8acrtDP6Zsrx3MfzUbPQVYEfvQtkp9sfUqviwJ330jezMxluiTi0O1o2rQ7x2s8YM/g7Ezyc89auppvVziQVOtqWQ+lzckgo9mXgXvAbeAR8BBZ+aRZgTKDBTsKFgrTeIGix0I/8csDL1Hwy/U/mdtXyFQa9JBUfFmhTRuax1elGyLpl304ZtaMP4KEWfPOysid61QPzFyUQU/gfGgPfAZ0B3bQUd8luDvBWA7iO9QNQs1cbkP0z96uu38btUIWIo+3co1U71yaq+kaUTQIFOvDCamDksd2rTHLkueUe3JLjKkbfj8pfk+7xzcNM9cQY5UGM4YBejp+lmE08G4EwdZ5CnukttSRJ3BSNcmZZWVgb6YL2UkeAhHP9rOHccZ2A2fmN/ASxphKyLarVkDSrWBf23tFU3ibILdS1EJdcAPY71lePx73pf5RA9BSXHheRllaxrRtzjy1Z1wUk/EkeCLq0yZyW8wU9xcDd4Az64E4QLNuXLUmBT8AewGQh7itOfPK01/SN2p/A7SxmAsdEpDaoNmhjoATYAOwEFUy5REHMtbVBf/s6l4JNG+Q3R03m5SIS+OLoL/AvoTd3YY45dDSYUsP0W6L6wNnCJzo+b0O+PXZ0naUVB3u/B+2kNeJbTMZok3WrOYF0IWTpBYqXXcEhpn9vOFVwVFVajTltsRP6Rjwc2fV+UHvbzbDJ0UHXCmfgxoBN6Ez/VUK2DOVGu5NgXzUOG6lpGBgzAt24ipQTG8kKBx1ZAN7PMBR9109DNsprBVaFd4/jxZWGnnFvarZvM5WCVBPUM4/zRI7NSZRJ2birVSNbl4WRrbI4EYcHVWPLOAP/Cf+/AAd08ZcYD2R5JPRuxPQFsCVzSl8TL0NuLsj+5FFKmfY29L1KWVTHdvBUkPIlv57FVkCU+eoKg6Jp9LDg+mOGzj331S/WRsOBqJnlXg7Np02tsvQV9zSaNEajnQrZbgCFAgW9QNOun416K6Pi/Sb3jSzGSpGxY9J7ERhpdEXWuT8GVLs53y+dzpzRPLkYUyNP923XMrdrtJ7/HIJCsTvq3CJOW1ZqBQ0lq3zo5UYoehWybqMQs5WoOCGZ5Ub1fadv/G1wWSpXDuBBqIFMO0Y0i7EZXjvrCbD5Kxg5cHz4MU8g4/VTVl8Dmvej+KYF+lGq1rv+hPtG/fkXmVSAsuLqSvA05PpcD7+CKMq2E8o+RqGBO17XJrRR+TtidjYKwLCUz3mmDvjF4A85tCp4LcVJvM64QkheaTJkFyLwOdA9RGke6zhXNLCUKroL2KP8juI90XQP2BsXn31T29dj+v2xLlbTX0FT1VrSyIg+1sF0Raaz82JQbROC04szpsao5BVmM1Y9e5oL8Te8N9vomyDVY3QdkcfOJd7CONVoufEluBFGtHYS9O+gDGv34ikYf41uU9dtX1McXB3EXNdn8CGhUlVYUfKquuCBUdX0MPHo1Wj9LExuNeme07CfZDEI5rv0+9nQRXxc87aPsq0Nf2AvdAb76AT1xORa8Ct4BnwI9KtJxnx/0BLppC3ODKHmYTL2RJ57LLrRb1x6N2H3lJRT3xr+GnP2Fjzlp32VgYQchulYcS9vPcuSlTmq5Bl1M3eo/mkFdzGHsOPIfR/chR15NJOHbeHzcGWceBT0DTml2+GBwWCA9bleTICuHKD1F+h6qNyQ/VXLL8RhFWxT8qi9sAQ4h/YFUBqtcSBehSosuYoq4Y6X3RfmlCZiGzPScnM1zChKMLcRt+0SM7x9XAQftRw7kseg9AjrE6bfx/ANpf9ioMik1G1FAU9dP+BbkWP3AseqDfpJAQTdfvfb7PNDi6ShRsLMdeBc0RSlG5GmtgEaRGvlFyY9kai3AGyBJXWqPz+Nv1H4WONMFUrxlIbpeHAoyC7Dwrwf2zkzh3JeU0Q1ReJn+MSXMBnWI46XA5uCPYGMQFI2ed8fON8GMcuzj0/9hV49FfI//h+juhn+TyuFPjdjUQGDDEF+G0HbNcpZFsK3vI+qcvAssEqhE94bzyN8AvZJmzQJ2M93FN33X7QiM3gKC/aoveSei87VPpehqML1XiK6u232xVba+iG0FjDtSzyb81jGpS0lys8qigRp5HQdhXiHTjPa5v7F4ff4kX62aqVtX+9xeyw5rXpgX6zO+PInS1bGKbViBjq5R3R4eFIj9f3voaYZHF9NEwrH6HjT/bznPrW66k4ECEx+Rbf0LiiR1/KJLBeWuS775tqXQXo1cOxd2IrYKDD+KyC9k/Z7+0Luwk8H2BGwoCE4io1DWF+0PBU+C0OBKRsUZGA+uYHdToJvHK6Ag9/BDwUulgqslqU9t6FZwIGarm7oekbwdo1e32fQpBcBHhzRA63XLFlwV6qQOnQP9gWuGcBXSDwC1LnfgoO5pQdFAZu1gomufY6GB9CkgGKRJ/V1QkUCf46F/N1O3wZXIqnSANRLCnlHFcbLcsPwm7drldm8OmOKUi/N1+2mf69Aunzt9zRHNC0iLc8N+/42MskXjYZXWUXp/fF3Aw98x6AwAn3nobs+JvKKHXqkqSfp4El2XX0nKJ9F11RWbBr9LoLRbrOLPCprJvc5Dtys6Az30YlVajn/YKNlVfiqJB3AN0WOysS6FuDTKKdjSRXtjcA1QcKWZq2/Zll1os/gbBZb2rGwGegfi36Oe+vWqpsdX8zucf4E09c2KCDzfT0Vnh1R2OMdvvpC8mkjG/5k4on7tknVciY60XUlTQBkUBZ4DqMNnIBYs2yb3y36RL2J1Ir8VFcfKSjfmO9JNTiNQmsN77qHIqr6N1dQh95tvf/KaddEIdwLF0zymKKq1MX9yQZmblu3v2ToF0O+je5OH/pzoHOShZyrpGdiXoj43hFfRGw00k+savZM8m+xJv0g66zSbgZYdHX/1Ax+ZhpICq8t8lON0sKPZKgV3O/J7cpx+hvnnY0vBna/osc61vsr1qNfSl1yBtp50HEX7Ez0Wz4CDM7HxusOOBiz9HOm1lvQUDulpQlB6BROC+xyLDqRp1tslIzgWo10ZluZmoJIBlv7xqVfk++OkXP+mjrn1FSilFuL4pqbcSSsM9f4/Y8Oo63+p62vcgrvQNJ/R9kT0bmih4XK2WtMUJ3/ghF48TsnykzMAr/NSSgGWj+iNrBlAgdZDHgUWQmdvD71QFfxbkEz1LV85Bv98AndfexpYaTbLp59624xSpM1HkO87WJGpS/HvtCibDZLXl3aoPwTlHtr/SDCx3PvUqUeyZ4TUsxfHUUscalk+wTnXWiudt3GyHgprOJT0VCKME4e6JYmBSgVYL1KX18iTtVMLsyT1L0nWXbkOZZ6J9aZOuaVYx/VnV34wjZNqCmnHg3wwr63ucyHpSNsP8Wz/9XDY/GiQ7cuUedijnB4J7OOhZyrJGdiNIkt6FNMx+1eR3vCi31E/9e9z5opSiMnbkvxFYnQK2ffy46LCTj1u4aoPfp+ewPf70R2cQL8uVeGlCcd3djiv6/BQR3qlkm6honccla1F2q8c6bWUpFnoaQ6HOjnSgkk7keCKC67kuq7AzSQBAy4iExT3UmUuqfn1Wq2fiBXWTh3Lm4A9FCCVKvq0A7NYhxC0rexp6x707vDUbQtqW9PIX3s0VCfzFQE93xu1/n3OPIGytlsCA/DZmeJh0/xBy78Exi0Z/2Y7Jqjk2F+WNF2M08r2ngV13dCLMbqO1KVwPFbE8X8CDVh85BWU9DjUdZP0KV9POkvg7NoOh/WI7nFHekWS4P57KirMyBfXqWO4eXFCDf5WIKVrQFAiH//TT/V4cLNgIfZVbpQj3ZJiGKhEgHUznfXBGD+as5e/NL86sfNAn29e+dhrnotqn+tK0HZqzuMDifipUZNmsb7zst/ASpxsGlkO8mzig3AXvCnrmOsiGSdLodAvTsnyEzGwLdqreJRoFRhzHFulRdg5tOWiHKHSOosyXUhdt3WOM0WPiRRw1KP8RFvnwvFrwcKeDfgYPb2l9Zmnfr2rrUkDxFFQ7oeDyIAgWKAM+3dj0zXU36gMdWVpUjPDWiIQlLgXuZahQK9gIfafBz7XckfRtp1U7gDrK+hVwBIvJ+Xb8aHQ05ras+g1w4d0zQve58htv9yw3PbxTmjGq0kd6R8+ug2usx7t28SzjcOCevComYfgrFZQrbCvf5/jM31d0LdtCAPwqHN6UEh2MPnfHKdXg4ns65Hh5470YJJujlsFEz32Nfu1pIeeVEZ56tWimmY7hoLVPZ3TwE4fb3zTU78R1NSHXDLalVjhNA0aP3DUqX8EXcvXK80IajYqKGODCYF9Pelxtesh+mSGd+VArZXdrWg7yh1g/Z0Do+9mxMryi+T6tpsjt01JC9vDa9FszKmrjmp+RTpca1bOOfx03XhmaTT+L92k5/Bo5kvoPByidz3pcaMmFdUNSI8jTUpnYENM/MbTTKvAWOU4Zz9lc6OnjTT/Pqc3tn36lvx40tOPWlQ7AKf28nRsJnoHw/1oT/1GUVvR0ZDvSav6jAnHQutyX3P41500zRLVquwa4phmoqIkbG3Z01GF6iyvHcFxUzlRzIfPRa5YP8nv0Shf7FOg9xX5uXgseIqPbhod/Qud9h1zK0+d3Lxg+6w4G5xYkzkAWmD6AHCNBOJM1HU+bV+BBvzOsxGXwZfzTSzSP8GW3vw6yMOWbtR3UUY3GpP0DCgwbu9RXC8iPBShpzdBBwDNwkTJZmSuA56JUgrkLRPYD9t9lf7wZVhmHaQvmsDHv9LWqxPoZ6Fa0dF80GHOdw18lwimsz8RKLiuBXkLJ/SCQrF0Y0fHdkJxYi38htMt8EMIimaknw0mBvaXDuxrdxrwmiRxlK21JF3LbgXlfPSs4O38wrlcrgDraxqhryw7b7xB1mdOzQ1u1zm3QmZrr4IVsK9/t9PULvfnpYfmbxh3aPO3mhxas5Lw/RGIOo+UP89KbTO/BtLSLh6t/QSdG2P0dKPeD8TdqDdGZ31QzzMWuF89ob+uRO3beXoQGhirPP3/ZexpZnLrGHu6hhwKkgRY3WNsFrLfKPxo8K3OkVOr0MaOHOOFy1ivAqhp9KVvQuqYk/T5HHmfUeZ7R3o1klyPCNWuBavhTFSdHMvFyL8IuO7rd8OpgqwocfUFDXDiykXZZPlz/hgUVotU8s98inaojWlET+xWTlMwYRkdh2ZxHYhCXilbfbPGNbXayuZyI/LL8nT3CN//N9jKgG8C8yJ8W2uBOWbmTqLIPp7FTkZPN/51PfXrXo2TQTe/PT0bci3H+YsY3ZfIHw22itHTrItu1BZgxRAVkX0QebppxYkC4xvilMgfDuICLJnR/znrRV94WzseMo+HjlQ+8tSrZ7XHcV6PBqsxm7QKdXtdp0sg+C7K7htSXoMuV3/VAL1W5KsQR7qFpFclmfOvNxVfA/T0ISiasfEJSuYOFmT/W1BqsPtbbGwCshD1GZ+2ZFFXWhszCgUV0WUtV2PwUl+jTT/mTub/Dc5X6nevfOrTv92hrj16XZL3WqPCRW8KdvcH9fyYwoeaYp292fEZnU1F74rigq7fLTcO3ah9ZAcuFL/yUTSd2RmAtx6k7D57aujedRyXuMBYhR8APjfgrugNVAFP6eCpp/Ov0UWPyHRzrIZogK1zvZyYN6Jh6ge6YQblp2BCFfcVnLhEA8K0Mj1twWA5zvtFwZGkPwbWCua37OsL7BroxonrvFT7fwkY4gyE5IdxGKIemZwZd5G1ZJSpEyxL0bqOwS031Vi7fJ9qMz7LsGvi/zcYazlEQWPEdrkOM2c0/5/CTV8YSHgXI7RlDB1YMysaHZQjII3xoHLZtFMjmAGeNd4LN2966t6Pnh73xAVPeix5IBgMTJIxoFlZfbg1TrwCYxnh+E6jT/yTn+fGGSVf/z7nbMp86qHrO1tTyk3Mw42aUOmJFzfD3VZwN6EmPMrWiag1lQqkXMFULR33TiF0uPwOUW2VvAHHu3OrVP8EzZ4tA9YBmixYBITJM2ScGJYZSHedlw19zwu0P6vdXwLVLAMsPafdi4tE2JTqbM4vd2G+E5Pip/Mh0PZ512GdTTu7Hb2l2K5DbsNvpje/3RM7A6OaadP1nBCaTj82O09q0tLOeKUTN050xHxnpcTfVPjTjfrsOMPk74HumZT52EPXVGAAvjRLsL8nGffBrYJdX7keRfV7zXJEidZv7AXOilJqyfN95LCAh61GUNEM1g0cxz4cm7Y0W67ZCAX8QXE9qgrqVGo/bAbuuxIcOK2EskmKataqH30qbA1c0JbrWOgRrmYZXXnB8mH7rlnKMN1ypmtCRdyXtKYsxkGtz3uyoJNVgKUpwH05kK8WDMduO+T2JdBZp5wL20N9YEzFgvcTl/tn/s53941d+Fcwo1HAcmCXQkIjbbm46yQ4xLNNz6P3qKduQe06fhwD4m6ayu8PdCKY+DHQD7UlPVQTBcayxzmd5E1Q/fucYZSZHOOLz+NJmVg6xk6tZ2uWw/caux66V8HfLvA3rdYblsC/qBkQ3bRdN/+F4aFzjfDQw9FWnUeTHOm1lHQbzmhtn8+McsFvV3CvlxDmAqUEWCdT/rJCJZ5bcazrWl9PfR81PerUyz0f+ShnoeN78sfVdThO3x2nVMjvNSK/YH5m7rgs/h1OwWaSrept1ym35IxpuT9R7jifsrRPX2XW47Pu4P98ytSZzlb46/onn65m6J/Qxj5eLS6I/sfwdzNpA4vTQ37rRj2UMlpgaRLBADzpEcbBESrFWS+wM7o4wfO3Lo77gY4x+hqA7AiuitHzvcCtSPva0Q8YEtWlaIZ8dbCup/fboXcxbR5QoTa/SX0HefqWRk2j+c/CCqqNtFV9QRwVy6LsaEZ0QnFilX5rdjEomr2aGEyskX1NcpwLRsGvgpQk4jovNYO3OAg9jnEV4MfoOB1XPn1jedKzDLBUjWbkKiZZBFj6fsvwJB6zBmrT5v83WMXlavo2FkOrnXqOzJ88fh+/ESPt/IaDvhttvQ+skqTNtaxLm3QhHOTp44fo3eKpG1TTjXpf0CGYEdjvyb5GL5cH0m23NQPbkLRa62RnSuLAuMXKy2wfAb91Wp098RD6kxbRRwXg781eJHRvBXJ6Al/9UENVyhhPvWeAx4FrJoTkVqLzQzdvr4Ffq9LJEr5Oe/NLVk2k9lvk9glodGN/RVDVAIt+3AUfVgr4pt1PQK0EWHp69AH4L9Cs1QMc0+/ZppG3HYXak6bry0uOvHInxd0nyl1/yfaJMUoSLWo9OakFHs8txtqrqorWfYH5un7l9b2nX3ylvYryFVW7OuMvenX2QyPszTx9vgYOvvLUDaq9SMLoYGLIvv59TtyMSUjRtpEMPzp/D/NsrQJjzSAmFo63RsIjPAuujd4WMbrvku/zyKEregog61U6wd14nN8dfJOgEcdybAcn0E+rWur1P229xeV0TXDJJq7ECqcpuFrKUecYjqsCm7SimcOnwTMxkE7YzNEX5GkQuipYDX+0/vk2kDa4wkzoP3nfVJkmyRko5QQ7l+qOTl4lgc3M3DhQVSHIY4EJo5Aeyf+xM514LKV/B96paiOyq/xQTPnMZuq1+X+mrRbedKP2ne38Nbpbp62rjZTbgHZu5NlWfbMsbWCsKu4Hb3jWFffvcyZgZ7ynrX0INup6JAvvT9BWPWKNmtUL0nEO7dZseaPLczRwmqOR29D+ag+w+uCXZnCC8ngwIeH+YfSJ9cF6MVgfu7oGumbLtFZVA/13sVFKUIWJX+R1frnq2pxjoUeFJgkZSBtgncVBHQJ0w0wsM9vnHuVtvreaqnjZ5HtYklGv9fP72nywkbT9TdK0ZsL3phM0URP7nDi9ceT3ns7oa8ClBpW6UYs7HxmEf1We6/Rxs2o6eqzrugEEHSopMJYxjrtmnHyD683R1UyWU7A1nYynnJmtE9ckaZfWyfWVQpv1WP3IBF7rCnUZ/V9cNrJosKp1Q0FZmYQNg4mV2of3OalrV0d9CpIfdqQnSfK+b9JvXsLwnkDncFA0Mzo8q2skdWlWTAFvUBYnYftgou3HM5AmwPoLB+LoeNPhGu8NbPqGu+YAZrEmtuvMxZtLScVAUMcC91x+Wu6mqdOa/9N9uKMxOfCgx4TbAFenjCldM9kD8aSrhzcz0RnuoRepAmcabY2MVJqVqanp9Wbt2q8CA1xUtUbF96J3T0tfLRRPu72OgroIx4mGTpoVjZJ7ojIDeafS3u6BtLrb5RhchNNnJnC8G7rX0fbVE5SpK1U44ZWj3B0Op3Vv8n387ShecpKeUKzgsKKA5zVHepKkRINGOPoPxg8E4iooA0g4NZhYwv7tIWU12NV5bZKAgZ/ncfwKTENNHxG91E89Wuvtg5seX2ZofqNcPncQvW1tFr6zXoG9Mkpzr26X+3zGjNyt87TPjXr7iMiFuF6ewMf7dDxNJV8JtvUqVCNK+L0ormh05CPPovSEj6KHzrXoKEiP+zCmZmcGgf8Ck9kZOIhdjbLjJJPAWJXQ1/Um6E381MU+TvTvc5anTNiM5yMY+BD0iDNEfk9wOfZ2wp6uQ5kJNnthTIu9w9a6ZFZXi6Hj2S4G/uhpeGH0bsLPLfFxvGeZelO7EYePBcGB3na0exPaPbqSDaLOLi3+uKrVGlRXoOPSzSyNOq/BL80kneEwqjV7n6NzniMvaZIGPpNA8Lt3a5OmPus7i42qiT5SeCGIE30LZ1ujK5wB+JkTDIsjkvxJYJFwS5XLwY+jPfwtqOydpWcYHVEwHLOdQr5rJOnlDmX1ryS+jKlD2fpq+fJeRkOUKK/v93wO4uQHFFYMMRObTNnFwRdxlbTkP802yUAqsn5srQmmt9iO25wdZYzC58YZCOQr0MhsLQi2dAP/FDwMNFvkJejeAXzkry6DFOwCHvQxUKTzLL8XcNmLS6Oc6htbZKvw85m4spXKxyHN1LnkeRKDgVdZ3aK+E1yOkKb74EK+laOrtYgu2dLXRlAPY2H3a31GyDdoD5qdbR87F7mcJk0DrKVmUy7jDnWd7PDjep8qKadZ76Dog9fL+ZTPSsfnEeFzVLYF0fG9WVXaiHbgZyrQrMJgMLXW20hHmwsfD/D08330wqaOPU20UruMlB9bpbZO0GjSZ8akdcnGTdmHpsXN/hVar/9D9lNhJ4Ot3voa7WlnL/pZ1GBiOHYme9qS2s5AgckGCcq0UtVFFmgmXn1a/m0KriCtI9uyC8dDj8k1c/xygso0g6BZDJ0PjSjn0KgfHA1bk7TTHellSYJf9YXjQoxfwrH7PCSvUslHUtHNjso026/1WNs48pImXUgB13mpx/Q6T3xmzpPW6dLPuxLrKS0uwLqCxmhq+rV6alQ1fYUrdc7twDvV9MOj7p3QWcZDTyr6aF2S18x9zL6A0qM+iujo3+fU/Rocz7ZGqsHDPCjsF6k0K3MCP2+dtVv6L/qBLnoKjHxEj7dCR9XY0jlyuY+hIh0FGppxGgnWB16zc+h1Br8BIyivQeMAoJtSQfrx4wLymwoJ5dzS9s+wvyvQMfKVrVHULHmx375la1oPPhS4jwxxUut/hoTkZZZMHStj7GrgCiDGkj40s8pSGoInDUr3B084TCj4VhC+niPPO6nlvAxr6+YY0ssXnbwNpleMi0/SW65USYhyTTnqMcc+lfKhEeuBv+5A/8MwKFV/RIhDHYCm3n1kMkq+gViiQ4ndnX0caNE5NpHxFmXKNtQjQtqjr9z7yt/TcBZXhsr1OPwNTyfeRq9bmE3yFgLjQBqZQaGXgB5p7As2A6uDlcFqLfsHsNUN4XXgI6eE+VpIx0hJjwgLdrTF1gbgKx/HinTOKrYR95tyNf+IsIWLRfB1XFE7i3/OZCdsZimOgth8bK8LxgGXqG59EiGRUCbzR4QFB7DdA4SdgxPI+1VBN82W8vOAMSBMbiXD+3FpUh+wrc90qB1BqatHhK41WPfRopIOTlIyG1kfLvcDWudRkFoIsPoUnPHYnl+u40PduvC/6eGDVN4Dcyf1hTINE2DRlk5AAYWPaL1E2Wb9sH2UjxMtOntFHTd0tgNaQ5KFKOj6EZRi7/AYfzMLsFQPvvYFWpeXRPSoyEswWhcBlhqDr7qxTo8g4mryMu3X2FNwHrVOM2w2J5J/bJYtwGrhalXqKL63sPuLKDhaPNLBmEzKa4b4u18stv6hOraIMZMoG3vzgzNBWB9wPR5tVQfla2INljp0YUGbFvAdDBpu+rkV+xVOgNNlwQ1A8i2IWpdSVu+ouwncD3xEJ1dJC7/jGoP9o30cadHZN85eMJ9yjRRg/S4BV663jYL0pN7Hj8WA70L7Z9DtEFUZ+X8BtSIKzvYGzseFpGcaYIkXbOram0Tko9ZxxQp6dRNgqTH4e2QMEePJPwgkHnAVk0V5BRB3gij5N5mp1r1RrqwBVgtX+ghoWBD0JHnzFbc56W/K7w00aAkTDWZGgTWT2i7Wp/y8QLPNb4EwUV2Rg7WCTfTCAqySgs6Cfd/tHCjqme514ASevY7zLWh6/gzAq57f78pBv4mtFsFXM4hdh/r1HN1H9P2kd3wUS9C5lrJHA58LwSFwqNekp5dQX10Wpd1aj6BPVviIFlGHrWfxKR+rwzHQDJlGkz4vSmjdlPrc/RGG9WhOA49DInQqlaXzU6+8PwQ+rESl8HkJfC5GXcd71icfR1BGr+c/4FmmLtRoj94u1UscYY8ElyLvEvAn9G5kezd4lXJT2IYKugqYlwAbg93AliAq8H+K/D2wq/OpJgXfHqJdA3HuKqA+USwbsKPgpx96U4szfH9T7irKz4v+P4BrTZRiiD8C3d9Gs70VPAbGUXYa21BBX9f8VcH2QI9glwFh8gMZB2NzVJiCR7r42Z16J7J1Dp48bCRSETmn4fSkRKVMORUD8HwzB/ceCmf5VldSXw6lgI57nOj7SSPilErNh5MP4eQW7OzvYWsNdLYCuqC2NVmfBm/i2WgFxm956paidhmF9wFRNynZ18XsMBAaYOEv3aD5/+/pRjAEVFN0PRwAPq6wEydS3+Kgv2e9hUXNW8OfXhppGKE9x9MfvqNBp4B2IQ1bnnQFpArExqH/Btt3gI6b3oLLg45gQdAT6HMvvYFeFImTB1HYEz+q/dZgnJ/6Pt21tF395kyH8nakDSN/P/RmOPJjkyh3AeW/QfEi0C2kgHjeqgUKhsZR5l22E4A4LARb6rPdwbKgF5DfcaKA6AD8uDNOMSZf16mzY3QyzZ4Dp3UxqZpwEPQ2wtxAnV4RrX7PBeYEyksrChB0UHWS6mT7ugXqKJNpt06+igv1phpJZOEoXOuCpJGCjzyH0mM+ihnoXIaN/sAn8NO0uwKIqhy/DNqa1sShFNQILE7Ey4g4pYzydVN/FGzhYW8LjttaHLfnw3TJ0w1Aa7t0k9TNQiPnSsszVDgQX16pdMXUqcXUB1OvbkC/9axfwYO+D6a3vcd6lqkLNdpzOu3STfp8sFiE0wrgl2lBhJpXls4fvQl+LPVX7Vrt5WmREr6eBVcKVjSQCcreJCjIOSqY4buP/Suxr/NSM4erxpTTfVvBrFCq6PqimavXSzVUjfI+N7RM/OLgdMZQD6CodUUg8pcGOnEWAIqMpaOTpRyiWSNN9SrA+gx/PmCrDqNRj/AeB/Ezto0sA2lcV88GXgof4qwSUgjmNvOobFN01gVPe+g2hAp99Vc0ZAfPxohLXZTKLvQPXGv+7IFPgKXRo4LE/nGOYfdS7CrQOR1sE6efUf632NGN/Gzq16CsKkLdU2m7Hrlotk8ztj6i6+iNlNNMlm6kDSO0R8GjAvlTwG6gXPcHcfYq0FKZUmdKZKsaMoRKdT/d2VH5EHicRNvOdOR5JVFWa7o2RlmBmgYC5RwAqR+fAy6k3sLsF7smzQxwILqCtcFg8C/wBpgCalUUdD0Bzge7gJ6NdChpT3egNxh9RK/HlvPkaUUt9fXzcaxF5/pWBkIS0Nci9689bGsBpWb4Ugvl9SV3n4XfWqCsQYaXoFt4EYWfsbKfl9GMlPCmC4hamFrssM5/DbC8BF1FcDuCx0C5RC+dXAGSHI+7PJ05yauhDiXsLwfGedZTUNN6nFbnLWk6RnoLNyjPOqqu2SSc1yzoPWB6sCEl7uvepPuUnpxkJi02Xa7pUVpZhMrmBlHny4AsKqaO3kD3Sr0cl6VMxNh5QIOG1EL507J0Kq2tTGewcEJT2xuBrcH/AU3btgP1IAvhpLAh0EL0ybTnf2z/Ax4ALxJJ/8C2XkWPBvWI6UuPBlxOW7/20MtS5R6M6fGR+kycbMixWRYffR6J6FHxF0CPn6JExzZOJ6q88gp1xelNR8GrLtq5CLq6IPsct4/RuxVUTDgG3+PjcCo8waNSXQv6Ac1GxAq29bjmVuzfzlbXFc1gbAl8+ghqoaKZ2TFAdq+nnrdDNd0ZmgX3OR6aMU8l+PQu7VZ7NZjwWTOketYGB4IztBOQqewH/VFa3Qic6Fr8H3hZk636ke4zK4COIKnoXHkc3AQewHY5Zi3Fr6uf6Pwvi9AODRh2x/gdIBikaPbvb+SPRe/hUhyg/FuUPxxb6ms6DjuAdYAeUyYV3Ws0S3kbuB3bHyU14NCfQpqLe4dq+ZJKnm6FYD3e0yMCTUtuAhYEjSa60L8G7ga3gufpBEqrG+E4zY+zvheiL2jfj5VuHD7qRjKnR726UX+Fj7E3CGxKV31S2yjR8dQUulfg4zKUsC5xrBt9pGCzEwo6xxS8xck0bFY6MNZr9Rqo+Z73P+HjpLiGhOVTl2YZVgHrg18DzTouCgp9R77oWOt4it9pQAGRbqhvAM3a/BeMwY9UfRwf5qV8ZxAn31HHd3FKUfkJ6iqY+YE6vyrsaIsNXeeXAB20XyTqL1nczIpMVu4n7dL1rBdQP1gVLAvUF3R8Ctc6nc+62arPvQ9eBy8CHX8NvMom+NcF43M7KviSussWZKk+6u7KRudKUHRuqI9k3nbq1D1GAa/OTx2XJYGuC/JDg3uJzrnC+aiBzSsC/nzANjPBl24YE6oqqQMsGqBo9Y/gd0Anb1sRXbS1PuQacBsdY2Jbabi10xioRQa4FukmJuiCqsBHN1edp5qVnAy+4TzVRd2kDTBAf9Dx1w1d9zcNTKZz/H0GKKiaZM0Ax0PHoTDAncmxqKvJiVL4SBRgQZQuXtuBA8CmQKPFtiyf0PgbgB6paYbLxBgwBowBY8AYMAaMAb83MgisNNW5KxgENB1rMjsDmoK+FVxEoPXc7Fm2ZwwYA8aAMWAMGANtjYHIGSwCK63/0GLCIWDVtkZOivbqkcSN4BwCLS2QNzEGjAFjwBgwBoyBNshAaIBFcLUwfFwHNm+DvJTaZK3/GEyQdUmphqy8MWAMGAPGgDFgDNQfA1FrqL6kOScBvYXTB5j4MTAJtQvBbX7qpmUMGAPGgDFgDBgDjcZA6AxWcUOZzdqR/b+A1YvT7fdsDOj101HgdGauxs6WYzvGgDFgDBgDxoAx0KYY8AqwxAhBlr6rMRBoPVZ3YDKLgX/z82QCqydmJdkvY8AYMAaMAWPAGGirDHgHWAWCCLT0zas/g/2Az0chC0Ubcfs6jToF3EBwZd9ZacQjbG0yBowBY8AYMAZSMJA4wCrUQaC1Fr9PBNsX0trQ9nPaeh64hMDKPmDYhg68NdUYMAaMAWPAGPBhIHWAVTBOoNWX31qf1Ra+j6V/b3AVOIPA6j22JsaAMWAMGAPGgDFgDJSHAa3PAn8CH4FGlQdo2AblYdCsGgPGgDFgDBgDxoAxEMIAAUgPcAH4HjSKjKEhu4GSZ/tCaLNkY8AYMAaMAWPAGDAG4hkgGFkT3AHqWSbi/DFA/0TWxBgwBowBY8AYMAaMgdpggODkd+AFUE8yDWeHg6Vrg0XzwhgwBowBY8AYMAaMgQADBCpdwGDwIah1uR8H1w80wXaNAWPAGDAGjAFjwBioTQYIXBYH54MpoNbkfzjUD9g6q9rsPuaVMWAMGAPGgDFgDEQxQBCzBrgd1IJ8ihNHg7mifLY8Y8AYMAaMAWPAGDAG6oIBgpodwPOgGqJ1VsNAz7ogy5w0BowBY8AYMAaMAWPAlwECnDnBIPABqJTcS0Xr+fpoesaAMWAMGAPGgDFgDNQlAwQ8i4F/gO9AueQVDO8CbJ1VXfYSc9oYMAaMAWPAGDAGUjFA8PNrcCvIUj7B2FHA1lmlOipWyBgwBowBY8AYMAYaggGCoe3As6AUmUrhi8GSDUGKNcIYMAaMAWPAGDAGjIFSGSAw0vqsQ8EEkFTuocC6pfpg5Y0BY8AYMAaMAWPAGGhIBgiUuoNzgc/6rJfR26khibBGGQPGgDFgDBgDxoAxkDUDBE6rg1uASz4m8UjQLet6zZ4xYAwYA8aAMWAMGAMNzwBBVB/wDJB8D4aCJRq+4dZAY8AYMAaMAWPAGDAGyskAAVVnoC+wb1zOesy2MWAMGAPGgDFgDBgDaRj4fwiA0POb8QTHAAAAAElFTkSuQmCC" alt="AttackForge Logo" style="width: 200px">
                </td>
              </tr>
              <tr>
                <td class="email-body" style="width: 100%; background-color: #fff">
                  <table align="center" class="email-body_inner sm-w-full" style="margin-left: auto; margin-right: auto; width: 800px; background-color: #fff" cellpadding="0" cellspacing="0" role="none">
                    <tr>
                      <td style="padding: 45px">
                        {content}
                      </td>
                    </tr>
                  </table>
                </td>
              </tr>
              <tr>
                <td>
                  <table align="center" class="email-footer sm-w-full" style="margin-left: auto; margin-right: auto; width: 570px; text-align: center" cellpadding="0" cellspacing="0" role="none">
                    <tr>
                      <td align="center" style="padding: 45px; font-size: 16px">
                        <p style="margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff">
                          This is a system generated email and reply is not monitored.
                          <br>
                          For any queries please reach out to
                          <span style="text-decoration-line: underline">support@attackforge.com</span>
                        </p>
                        <p style="margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff">&copy; 2018-{now | dateFormat:["yyyy"]} AttackForge&reg;</p>
                      </td>
                    </tr>
                  </table>
                </td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </div>
  </body>
</html>
```

<figure><img src="/files/z4J8WgmybIIaGy6PsQAX" alt=""><figcaption></figcaption></figure>

## Custom Email Meta Tags

The following meta tags can be used for the following emails in either the subject or the body of the email:&#x20;

1. Project Requests
   1. Email Project Request Owner when request is created
   2. Email Project Request Owner when request is approved
   3. Email Project Request Owner when request is rejected
   4. Email Project Request Owner when information is requested
   5. Email when new Project Request is created
   6. Email when Project Request is updated
2. Groups
   1. Email User when invited to Group
   2. Email Group Member when project is added
3. Users
   1. Email User when Password Reset is requested
   2. Email User on change of IP address from last login
   3. Email User on New Registration
   4. Email New User with Welcome Message
   5. Email Admins when New User is created
   6. Email User on Invitation
   7. Email User on Daily Project Update
   8. Email User on Weekly Project Update
4. Projects
   1. Email User when invited to new project
   2. Email Project Coordinator when new Project is created
   3. Email Project Team when Project is placed On Hold
   4. Email Project Team when Project is placed Off Hold
   5. Email Vulnerability Owner when Review Note is posted
   6. Email User when someone replies to their Vulnerability Review Note
   7. Email Additional Team Members when Vulnerability Review Note is posted
   8. Email Additional Team Members when someone replies to a Vulnerability Review Note
   9. Email Review Note Owner when someone replies to an Executive Summary Review Note
   10. Email Additional Team Members when an Executive Summary Review Note is posted
   11. Email Additional Team Members when someone replies to a Executive Summary Review Note
   12. Email Project Team when Vulnerability Status is updated
   13. Email Team Member when Project Role is updated
   14. Email Project Team when a Retest is completed
   15. Email Admins when a new Retest is requested
   16. Email Admins when a Retest is cancelled
   17. Email Project Team with one-time Custom Email

&#x20;

<table><thead><tr><th width="380">Tag</th><th>Applies to</th></tr></thead><tbody><tr><td>{tenant_url}</td><td>All emails</td></tr><tr><td>{now}</td><td>All emails.</td></tr><tr><td>{now | dateFormat:["mm/dd/yyyy"]}</td><td>All emails. See <a href="https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters#dateformat">dateFormat</a> for all options.</td></tr><tr><td>{now | dateSubtract:[1, days] | dateFormat:["mm/dd/yyyy"]}</td><td>All emails.</td></tr><tr><td>{now | dateAdd:[7, days] | dateFormat:["mm/dd/yyyy"]}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.critical.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.critical.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.high.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.high.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.medium.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.medium.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.low.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.low.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.info.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.priority.info.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.status.closed.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.status.closed.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.status.retest.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.status.retest.color_dark}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.status.open.color_light}</td><td>All emails.</td></tr><tr><td>{config.vulnerability.status.open.color_dark}</td><td>All emails.</td></tr><tr><td>{link}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.2, 3.1, 3.3, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{link.url}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 2.2, 3.1, 3.3, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{invite_link}</td><td>3.6</td></tr><tr><td>{invite_link.url}</td><td>3.6</td></tr><tr><td>{recipient.first_name}</td><td>All emails</td></tr><tr><td>{recipient.project_role}</td><td>4.1, 4.13</td></tr><tr><td>{recipient.password_reset_link}</td><td>3.1</td></tr><tr><td>{recipient.password_reset_link.url}</td><td>3.1</td></tr><tr><td>{recipient.account_activation_link}</td><td>3.3</td></tr><tr><td>{recipient.account_activation_link.url}</td><td>3.3</td></tr><tr><td>{retest_round.date}</td><td>4.14, 4.15, 4.16</td></tr><tr><td>{retest_round.requestor.first_name}</td><td>4.15</td></tr><tr><td>{retest_round.requestor.last_name}</td><td>4.15</td></tr><tr><td>{retest_round.actioned_by.first_name}</td><td>4.14, 4.16</td></tr><tr><td>{retest_round.actioned_by.last_name}</td><td>4.14, 4.16</td></tr><tr><td>{retest_round.vulnerabilities.total}</td><td>4.14, 4.15, 4.16</td></tr><tr><td>{review_note.creator.first_name}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11</td></tr><tr><td>{review_note.creator.last_name}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11</td></tr><tr><td>{review_note.note}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11</td></tr><tr><td>{review_note.topic}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11</td></tr><tr><td>{review_note.original_note}</td><td>4.6, 4.8, 4.9, 4.11</td></tr><tr><td>{review_note.reply_note}</td><td>4.6, 4.8, 4.9, 4.11</td></tr><tr><td>{group.name}</td><td>2.1</td></tr><tr><td>{user.first_name}</td><td>3.5, 3.6</td></tr><tr><td>{user.last_name}</td><td>3.5, 3.6</td></tr><tr><td>{user.username}</td><td>3.5</td></tr><tr><td>{user.email_address}</td><td>3.5</td></tr><tr><td>{user.role}</td><td>3.5</td></tr><tr><td>{user.ip_address}</td><td>3.2</td></tr><tr><td>{user.created_by.first_name}</td><td>3.5</td></tr><tr><td>{user.created_by.last_name}</td><td>3.5</td></tr><tr><td>{project_request.requestor.first_name}</td><td>1.5, 1.6</td></tr><tr><td>{project_request.requestor.last_name}</td><td>1.5, 1.6</td></tr><tr><td>{project_request.requested_information.details}</td><td>1.4</td></tr><tr><td>{project_request.rejected.reason}</td><td>1.3</td></tr><tr><td>{project_request.name}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.id}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.created_datetime}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.last_updated_datetime}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.status}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.start_date}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.end_date}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.scope}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.groups}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.test_suites}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.testing_hours}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.onsite_testing}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.justification}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.organization_code}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.link}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project_request.custom_field.&#x3C;KEY>}</td><td>1.1, 1.2, 1.3, 1.4, 1.5, 1.6</td></tr><tr><td>{project.name}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.code}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.id}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.created_datetime}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.last_updated_datetime}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.start_date}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.end_date}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.organization_code}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.link}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.custom_field.&#x3C;KEY>}</td><td>2.2, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 4.15, 4.16, 4.17</td></tr><tr><td>{project.on_hold.status}</td><td>4.3</td></tr><tr><td>{project.on_hold.details}</td><td>4.3</td></tr><tr><td>{project.off_hold.status}</td><td>4.4</td></tr><tr><td>{project.off_hold.details}</td><td>4.4</td></tr><tr><td>{vulnerability.id}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.created_datetime}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.last_updated_datetime}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.discovered_by.first_name}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.discovered_by.last_name}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.actioned_by.first_name}</td><td>4.12</td></tr><tr><td>{vulnerability.actioned_by.last_name}</td><td>4.12</td></tr><tr><td>{vulnerability.title}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.priority}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.affected_assets}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.likelihood_of_exploitation}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.zero_day}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.description}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.attack_scenario}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.remediation_recommendation}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.proof_of_concept}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.status}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.code}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.sla_date}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.target_remediation_date}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.tags}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.cvssv3.base_score}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.cvssv3.temporal_score}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.cvssv3.environmental_score}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.cvssv3.vector}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.notes}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.custom_tag.&#x3C;KEY>}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.custom_field.&#x3C;KEY>}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulnerability.link}</td><td>4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12</td></tr><tr><td>{vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{critical_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{high_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{medium_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{low_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{info_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{closed_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{retest_vulns_count}</td><td>3.7, 3.8</td></tr><tr><td>{project_requests_count}</td><td>3.7, 3.8</td></tr><tr><td>{projects_in_progress_count}</td><td>3.7, 3.8</td></tr><tr><td>{projects_completed_count}</td><td>3.7, 3.8</td></tr><tr><td>{projects_overrun_count}</td><td>3.7, 3.8</td></tr><tr><td>{projects_waiting_to_start_count}</td><td>3.7, 3.8</td></tr><tr><td>{users_registered_count}</td><td>3.7, 3.8</td></tr></tbody></table>


# Custom Fields & Forms

{% embed url="<https://www.youtube.com/watch?v=vPZU3LxqvUE>" %}

## Overview

AttackForge supports ability to create custom fields & forms. This can help to capture information which is relevant to your organization and customers.

Custom fields can be accessed in the application, in reports and also via the Self-Service APIs.

You can create custom fields & forms for the following:

* Project Request
* Project
* Writeups
* Vulnerabilities
* Assets
* Portfolios
* Test Cases
* Project Test Cases
* Retesting Rounds
* Reporting
* Project Pages
* Groups
* Users

To set custom fields, you must be an Administrator. Start by clicking on `Administration` module, then click on any of the following sections:

* Portfolios
* Projects
* Project Requests
* Vulnerabilities
* Test Cases
* Reporting
* Pages
* Assets
* Writeups
* Test Suites
* Groups
* Users

<figure><img src="/files/WIvr095WTNOujUigGhvF" alt=""><figcaption></figcaption></figure>

AttackForge supports custom `Sections` which can be used to group custom fields together in the form.

<figure><img src="/files/cQqvkFq6kbPkmOaIqboO" alt=""><figcaption></figcaption></figure>

AttackForge supports the following custom field types:

* Input field
* Text Area
* Select
* Multi-Select
* List
* Datepicker
* Datetimepicker
* Table
* Rich-Text
* User Select
* User Multi-Select
* Group Select
* Group Multi-Select

## **Input Fields**

Input fields display a single-line input box within the relevant forms.

When creating an input field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Placeholder Value** - This is the default value that will be displayed in the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## **Text Area Fields**

Text area fields display multi-line input box within the relevant forms. Text area can be resized by the user within the form if additional space is needed. This option is useful if user is required to enter paragraphs of text.

When creating a text area field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Placeholder Value** - This is the default value that will be displayed in the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## **Select Fields**

Select fields display a drop-down menu with a single item select within the relevant forms. User can only select one option.

When creating a select field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Select Option** - This is the default option that will be selected on the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.
* **Add Select Option** - this button will create a new option for the menu. The menu must have at least one option.&#x20;
  * **Option** - The option is the text that will be displayed in the drop-down menu within the form.
  * **Value** - The value is the data that will represent this option when it is selected by a user.

## **Multi-Select Fields**

Multi-select fields displays a drop-down menu with a multi-item select within the relevant forms. User can select one or more options.

When creating a multi-select field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Select Options** - This is the default options that will be selected on the forms. You can specify multiple options by separating each option with a comma e.g. *pcidss,hipaa*
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.
* **Add Select Option** - this button will create a new option for the menu. The menu must have at least one option.&#x20;
  * **Option** - The option is the text that will be displayed in the drop-down menu within the form.
  * **Value** - The value is the data that will represent this option when it is selected by a user.

## **List Fields**

List fields display an input field with an option to add additional rows of input fields, similar to when typically adding tags.

When creating a list field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## Datepicker Field

The datepicker fields display a calendar where the user can select a single date.

When creating a datepicker field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Placeholder Date** - This is the default date that will be displayed in the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.

## Datetimepicker Field

The datetimepicker fields display a calendar where the user can select a single date and enter in a time.

When creating a datetimepicker field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Placeholder Date** - This is the default date that will be displayed in the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.

## Table Field

The table field displays ability to define columns, where the user can then create rows of data against these columns.

When creating a table field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Label** - This is the label that will be displayed in the form for this table.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the table is mandatory or optional in the forms.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

You can then add columns by clicking Add Column. Each column has the following options:

* **Type** - Input field, Text Area, Select, Multi-Select or Datepicker
* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Default Value / Selected Options** - depending on the Type, this will allow you to specify default selected options/value for this field.
* **Label** - This is the label that will be displayed in the form for this table.
* **Required** - This is used to determine whether the table is mandatory or optional in the forms.

The form will present all of the columns (fields) for the user to enter, and ability to add rows.

## **Rich-Text Fields**

Rich-Text fields display a multi-line WYSIWYG input box within the relevant forms.

When creating an rich-text field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Placeholder Value** - This is the default value that will be displayed in the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## **User Select Fields**

User Select fields display a drop-down menu with a single item select within the relevant forms.&#x20;

The user will be able to select a single user in the application from the drop-down list.

> **!IMPORTANT:** This field type is restricted to access controls. An access control must be applied in order for this field type to be set. Any user with View privileges to this field will be able to view the input user and their profile. Any user with Edit privileges will be able to see all users in the system, including their first name, last name and email address. This is required in order to be able to select a user from the system.

When creating a user select field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Select Option** - This is the default option that will be selected on the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## **User Multi-Select Fields**

User Multi-Select fields display a drop-down menu with a multi item select within the relevant forms.&#x20;

The user will be able to select multiple users in the application from the drop-down list.

> **!IMPORTANT:** This field type is restricted to access controls. An access control must be applied in order for this field type to be set. Any user with View privileges to this field will be able to view the input user and their profile. Any user with Edit privileges will be able to see all users in the system, including their first name, last name and email address. This is required in order to be able to select a user from the system.

When creating a user multi-select field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Select Option** - This is the default option that will be selected on the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## **Group Select Fields**

Group Select fields display a drop-down menu with a single item select within the relevant forms.&#x20;

The user will be able to select a single group in the application from the drop-down list.

> **!IMPORTANT:** This field type is restricted to access controls. An access control must be applied in order for this field type to be set. Any user with View privileges to this field will be able to view the input group name. Any user with Edit privileges will be able to see all groups in the system. This is required in order to be able to select a group from the system.

When creating a group select field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Select Option** - This is the default option that will be selected on the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## **Group Multi-Select Fields**

Group Select fields display a drop-down menu with a multi item select within the relevant forms.&#x20;

The user will be able to select multiple groups in the application from the drop-down list.

> **!IMPORTANT:** This field type is restricted to access controls. An access control must be applied in order for this field type to be set. Any user with View privileges to this field will be able to view the input group name. Any user with Edit privileges will be able to see all groups in the system. This is required in order to be able to select a group from the system.

When creating a group select field, the following options are available:

* **Key** - This the name of the field (e.g. database field name). This is the reference you will use when referring to this field in the JSON export, ReportGen or via the Self-Service API. The key must be unique, and is limited to alpha-numeric and underscores only.
* **Default Select Option** - This is the default option that will be selected on the forms.
* **Label** - This is the label that will be displayed in the form for this field, as well as in the data tables.
* **Info** - This is a custom information message that will display to the user when they have selected the field or are entering information into the field.
* **Required** - This is used to determine whether the field is mandatory or optional in the forms.
* **Display in Tables** - This is used to determine whether the field will be displayed as a new column in the relevant tables within the application.
* **Hide Condition** - This is used to create a condition to hide the field, until such condition is met. See [Hide Conditions](#hide-conditions) for more details.
* **Access Controls** - This is used to configure which Roles, Groups or Users will have access to see or modify this field and its data.

## Hide Expressions/Conditions

Hide expressions or conditions are used to hide a custom field or custom section until some condition is met.

Using the example below, let's say we have two (2) custom fields for a project request:

* **project\_type** - this field allows the person requesting the project to pick what type of project they are requesting, for example a web app pentest, vulnerability scan, code review, etc.
* **application\_url** - this field is used to capture the URL for the application IF the person is requesting a web app pentest.

Let's say we want to hide the **application\_url** field until the user has selected 'Web App Pentest' from the **project\_type** select menu.

We would enter the following hide condition into the **application\_url** field:

```
custom.project_type !== "web_app_pentest"
```

This hide condition states: *Hide this field WHILE project\_type is NOT EQUAL to web\_app\_pentest*

As a result, the project request form will not show this field until the user has selected 'Web App Pentest' as the type of assessment they are requesting.

Hide conditions support additional logical expressions, such as:

{% code overflow="wrap" %}

```
custom.project_type !== "web_app_pentest" AND custom.project_type !== "vuln_scan"
```

{% endcode %}

This hide condition states: *Hide this field WHILE project\_type is NOT EQUAL to web\_app\_pentest AND project\_type is NOT EQUAL to vuln\_scan*

This is useful if we want this field to display on either a Web App Pentest or Vulnerability Scan.

```
custom.project_type == "web_app_pentest
```

This hide condition states: *Hide this field WHILE project*\_*type is EQUAL to web\_app\_pentest*

{% code overflow="wrap" %}

```
custom.project_type !== "web_app_pentest" AND custom.company_name !== undefined
```

{% endcode %}

This hide condition states: *Hide this field WHILE project\_type is NOT EQUAL to web\_app\_pentest AND company\_name is not empty i.e. user has entered a value for company name field*

Another example is showing a field "WEB APP Q1" only when a user selects "Web App" from another field. This example works for both Select & Multi-Select.

{% code overflow="wrap" %}

```
NOT ("web_app" $in custom.test_type)
```

{% endcode %}

### Referring to Fields

You can refer to fields using the following syntax:

* **core.\<key>** - refers to a system field with a well-known key (see sections below)
* **custom.\<key>** - refers to a custom field with a user-defined key

> **!IMPORTANT:** For Vulnerability Custom Fields - you can also refer to project custom fields. The syntax works as follows:

* **project.core.\<key>** - refers to a project system field from a vulnerability custom field hide expression.
* **project.custom.\<key>** - refers to a project custom field with a user-defined key from a vulnerability custom field hide expression.

> **IMPORTANT:** For Project Test Case Custom Fields - you can also refer to project and test case (from test suite) custom fields. The syntax works as follows:

* **project.core.\<key>** - refers to a project system field from a vulnerability custom field hide expression.
* **project.custom.\<key>** - refers to a project custom field with a user-defined key from a vulnerability custom field hide expression.
* **test\_case.core.\<key>** - refers to a test case system field from a project test case custom field hide expression.
* **test\_case.custom.\<key>** - refers to a test case custom field with a user-defined key from a project test case custom field hide expression.

> **IMPORTANT**: When Approving a Project Request, you can refer to both Project Request and Project fields as follows:

* **project\_request.core.\<key>** - refers to a project request system field from a project custom field hide expression.
* **project\_request.custom.\<key>** - refers to a project request custom field with a user-defined key from a project custom field hide expression.
* **project.core.\<key>** - refers to a project system field from a project custom field hide expression.
* **project.custom.\<key>** - refers to a project custom field with a user-defined key from a project custom field hide expression.

### Operators

* **NOT** or **!** - used to negate an expression. For example *!(core.\<key> === "...")*
* **AND** or **&&** - used to *and* multiple expressions. For example *core.\<key> === "..." AND core.\<key> === "..."*
* **OR** or **||** - used to *or* multiple expressions. For example *core.\<key> === "..." OR core.\<key> === "..."*
* **==** - used to check for equivalency. For example *core.\<key> == "..."*
* **===** - used to check for equality. For example *core.\<key> === "..."*
* **!==** - used to check for not equivalency. For example *core.\<key> !== "..."*
* **>** - used to check for greater-than comparison. For example *core.\<key> > 10*
* **<** - used to check for less-than comparison. For example *core.\<key> < 10*
* **>**= - used to check for greater-than-or-equals comparison. For example *core.\<key> >= 10*
* **<=** - used to check for less-than-or-equals comparison. For example *core.\<key> <= 10*
* **( )** - used to group statements together. For example ((*core.\<key>* === "...") AND (*core.\<key> === "..."*)) OR ((*core.\<key>* === "...") AND (*core.\<key> == "..."*))

### Field types

#### **string**

Refers to a string value.&#x20;

Example: *Project Request "name" System Field*

This rule will hide the field until the project name on the project request has at least 1 character filled in.

```
core.name.length < 1
```

This rule will show the field until the project name on the project request has at least 1 character filled in.

```
core.name.length > 0
```

This rule will hide the field until the project name on the project request is exactly "Project Name".

```
core.name !== "Project Name"
```

This rule will show the field until the project name on the project request is exactly "Project Name".

```
core.name == "Project Name"
```

#### **string\[]**

Refers to a string array / array of strings.

Example: *Project Request "testing\_to\_be\_performed" System Field*

This rule will hide the field until the user has selected a particular service / test type.

```
NOT ("62e0d2b6e326df35c2a4bdf4" $in core.testing_to_be_performed)
```

It works as follows:

* Hide the field when the Test Suite (by its System Id = 62e0d2b6e326df35c2a4bdf4) is NOT in the  core field "testing\_to\_be\_performed".
* The core field "testing\_to\_be\_performed" is a string array (string\[]) and it can hold 0-to-many Test Suites.
* Note you can get the ID of a test suite by checking the URL when visiting the test suite in the Test Suites module.

**ISO date time**

Refers to a datetime in ISO format e.g. YYYY-MM-DD-Thh:mm:ss.mmmZ

Example: *Project "start\_date" System Field*

This rule will hide the field until the user has selected a date greater than 1st of January 2023.

```
core.start_date == undefined OR core.start_date < "2023-01-01T00:00:00.000Z"
```

#### Number

Refers to an integer i.e. 1, 2 or 10

Example: *Vulnerability "exploitability" System Field*

This rule will hide the field until the user has selected a Likelihood of Exploitation greater than or equal to 5.

```
core.exploitability == undefined OR core.exploitability < 5
```

#### Boolean

Refers to a boolean i.e. true or false

Example: *Vulnerability "zero\_day" System Field*

This rule will hide the field until the user has selected Yes for Is Zero Day? field.

```
core.zero_day == undefined OR core.zero_day == false
```

### Project System Fields

* **name** (string | undefined)
* **code** (string | undefined)
* **org\_code** (string | undefined)
* **start\_date** (ISO date time | undefined)
* **end\_date** (ISO date time | undefined)
* **timeframe** (ISO date time | undefined, ISO date time | undefined)
* **linked\_groups** (string\[] | undefined)
* **group\_ids** (string\[] | undefined)
* **group\_names** (string\[] | undefined)
* **portfolio\_ids** (string\[] | undefined)
* **portfolio\_names** (string\[] | undefined)
* **portfolio\_stream\_ids** (string\[] | undefined)
* **portfolio\_stream\_names** (string\[] | undefined)
* **test\_suite\_ids** (string\[] | undefined)
* **test\_suite\_names** (string\[] | undefined)
* **vuln\_scoring\_system** ('CVSS v3.1 Baseline' | 'CVSS v3.1 Baseline + Temporal' | 'CVSS v3.1 Baseline + Temporal + Environmental' | undefined)
* **vuln\_code** (string | undefined)
* **sla\_apply\_method** ('Automatic' | 'Manual' | undefined)

### Project Request System Fields

* **name** (string | undefined)
* **code** (string | undefined)
* **linked\_groups** (string\[] | undefined)
* **onsite\_testing\_required** ('Yes' | 'No' | undefined)
* **reason\_testing\_is\_required** (string | undefined)
* **desired\_test\_window** ('Mon-Fri (Business Hours)' | 'Mon-Fri (Non-Business Hours)' | 'Weekends (Sat/Sun)' | undefined)
* **testing\_to\_be\_performed** (string\[] | string | undefined)
* **scope** (string\[] | undefined)
* **desired\_start\_date** (string | undefined)
* **desired\_end\_date** (string | undefined)
* **org\_code** (string | undefined)

### Vulnerability System Fields

* **writeup\_library** (object | undefined)
  * **name** (string)
  * **key** (string)
  * **type** ('system' | 'custom')
* **writeup** (object | undefined)
  * **id** (string)
  * **name** (string)
* **asset\_names** (string\[])
* **zero\_day** (true | false | undefined)
* **visibility\_to\_project\_team** (true | false | undefined)
* **priority** ('Critical' | 'High' | 'Medium' | 'Low' | 'Info' | undefined)
* **exploitability** (number | undefined)
* **steps\_to\_reproduce** (string | undefined)
* **notes** (string\[] | undefined)
* **tags** (string\[] | undefined)
* **associated\_test\_cases** (string\[] | undefined)
* **project.core.\<key> -** refer to a project system field
* **project.custom.\<key>** - refer to a project custom field

### Writeup System Fields

* **library** (object | undefined)
  * **type** ('system' | 'custom')
  * **key** (string)
* **import\_source** (string | undefined)
* **import\_source\_id** (string | undefined)
* **template** (string | undefined)
* **title** (string | undefined)
* **description** (string | undefined)
* **attack\_scenario** (string | undefined)
* **remediation\_recommendation** (string | undefined)
* **severity** (number | undefined)
* **exploitability** (number | undefined)
* **impact\_on\_confidentiality** ('High' | 'Medium' | 'Low' | 'None' | undefined)
* **impact\_on\_integrity** ('High' | 'Medium' | 'Low' | 'None' | undefined)
* **impact\_on\_availability** ('High' | 'Medium' | 'Low' | 'None' | undefined)
* **tags** (string\[] | undefined)

### Asset System Fields

* **name** (string | undefined)
* **type** (string | undefined)
* **id** (string | undefined)
* **details** (string | undefined)
* **linked\_groups** (string\[] | undefined)

### Portfolio System Fields

* **name** (string | undefined)
* **code** (string | undefined)
* **description** (string | undefined)
* **level1\_owner** (string | undefined)
* **level2\_owner** (string | undefined)
* **level3\_owner** (string | undefined)
* **tags** (string\[] | undefined)

### Test Case System Fields

* **test\_case** (string | undefined)
* **details** (string | undefined)
* **code** (string | undefined)
* **execution\_flow** ({ title: string, details: string }\[] | \[])
* **tags** (string\[] | \[])

### Project Test Case System Fields

* **status** ('Tested', 'Not Tested', 'Testing In Progress', 'Not Applicable' | undefined)
* **assigned\_to** (string | undefined)
* **project.core.\<key> -** refer to a project system field
* **project.custom.\<key>** - refer to a project custom field
* **test\_case.core.\<key>** - refer to a test case system field
* **test\_case.custom.\<key> -** refer to a test case custom field

### Retesting System Fields

* **window** (string, string | undefined)
* **scope\_ids** (string\[])
* **project.custom.\<key>** - refer to a project custom field

### Group System Fields

* **name** (string | undefined)
* **owner** (string | undefined)
* **primary\_contact** (object | undefined)
  * **name** (string | undefined)
  * **number** (string | undefined)
  * **email** (string | undefined)

### User System Fields

* **first\_name** (string)
* **last\_name** (string)
* **job\_title** (string | undefined)
* **organization** (string | undefined)
* **business\_group** (string | undefined)
* **office\_location** (string | undefined)
* **about\_me** (string | undefined)
* **tags** (string\[])
* **certifications** (string\[])
* **education** ({ institute?: string; qualification?: string; completed?: string }\[])
* **employment** ({ company?: string; job\_title?: string; duration?: string }\[])

## Project Request Custom Fields

From the `Administration` module, click on `Projects -> Project Requests` then click on Add Custom Field in Project Requests section.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Request a New Project / Edit Project Request forms
* Viewing a Project Request page
* Approve Project Request with Updates form
* Pending Requests table in Projects Module
* Actioned Requests table in Projects Module
* Accessing custom fields in APIs, Flows, Events

## Project Custom Fields

From the `Administration` module, click on `Projects` then click on Add Custom Field in Projects section.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Create New Project / Edit Project forms
* Approve Project Request form
* Project Dashboard
* Projects and Archived Projects tables in Projects Module
* Viewing projects in reports
* Accessing custom fields in APIs, Flows, Events

## Writeups Custom Fields

From the `Administration` module, click on `Writeups` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Create a New Writeup / Edit Writeup forms
* Viewing a writeup in the library
* Viewing a vulnerability on a project
* Writeups tables in Writeups Module
* Viewing writeups in reports
* Accessing custom fields in APIs, Flows, Events

## Vulnerability Custom Fields

From the `Administration` module, click on `Projects -> Vulnerabilities` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Create a New Vulnerability / Edit Vulnerability forms
* Viewing a vulnerability on a project
* Vulnerabilities tables
* Viewing vulnerabilities in reports
* Accessing custom fields in APIs, Flows, Events

## Asset Custom Fields

From the `Administration` module, click on `Assets` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Create or Update an Asset within Asset Module
* Viewing an Asset within Assets module
* Assets table
* Viewing assets in reports
* Accessing custom fields in APIs, Flows, Events

## Portfolio Custom Fields

From the `Administration` module, click on `Portfolios` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Create or Update a Portfolio within Portfolios Module
* Viewing a portfolio
* Portfolios table
* Accessing custom fields in APIs, Flows, Events

## Test Suite Test Case Custom Fields

From the `Administration` module, click on `Test Suites` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Create or Update a Test Case within Test Suites Module
* Viewing test cases on a test suite
* Viewing test cases on a project
* Viewing test cases in reports
* Accessing custom fields in APIs, Flows, Events

## Project Test Case Custom Fields

From the `Administration` module, click on `Projects -> Test Cases` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms, pages and tables:

* Update a Test Case on a project
* Viewing test cases on a project
* Viewing test cases in reports
* Accessing custom fields in APIs, Flows, Events

## Reporting Custom Fields

From the `Administration` module, click on `Projects -> Reporting` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms and pages:

* Reporting section on a project
* Viewing reporting fields in reports
* Accessing custom fields in APIs, Flows, Events

## Summary Page Custom Fields

From the `Administration` module, click on `Projects -> Pages` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms and pages:

* Summary page on a project
* Viewing summary page fields in reports
* Accessing custom fields in APIs, Flows, Events

## Retesting Custom Fields

From the `Administration` module, click on `Projects -> Retesting` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms and pages:

* Viewing a retest round on a project.
* Viewing retest round fields in reports
* Accessing custom fields in APIs, Flows, Events

## Group Custom Fields

From the `Administration` module, click on `Groups` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms and pages:

* Viewing a group within the Groups module
* Accessing custom fields in APIs, Flows, Events

## User Custom Fields

From the `Administration` module, click on `Users` then click on Add Custom Field.

Add the custom fields you would like displayed in the forms and tables. Set the options as required for each field, then click Update.

You can view the custom fields in the following forms and pages:

* Viewing a user within the application, where the user profile is accessible
* Viewing a user within the Users module
* Accessing custom fields in APIs, Flows, Events

## Linked Project Key

When setting up your project request custom fields & project custom fields, you may wish to have come data from the project request captured and stored on the project as well. This can be achieved using the Linked Project Key on the Project Request custom fields.

The following example will:&#x20;

* create a custom field on Projects to capture project status notes "project\_notes".&#x20;
* create a custom field on Project Requests to capture additional notes from the customer "customer\_notes".
* Map the additional notes from the customer "customer\_notes" to the project status notes "project\_notes" when the request is getting approved and project is getting set up.

#### Step 1:  Create a custom field for Projects "project\_notes"

<figure><img src="/files/Wn8Hgm5e0mPAdbse9UMy" alt=""><figcaption></figcaption></figure>

#### Step 2: Create a custom field for Project Requests "customer\_notes" and link the Project custom field

<figure><img src="/files/nL97cP83EGnTFJktVxQI" alt=""><figcaption></figcaption></figure>

#### Step 3: Create a new Project Request. Observe the new Additional Notes field

<figure><img src="/files/pg2BjxA70vXg1SAe9evi" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/YBdZ3cIWyGyWyFeEOwQG" alt=""><figcaption></figcaption></figure>

#### Step 4: Approve the Project Request and observe the Project Notes field has the data from the customer's Additional Notes field.

<figure><img src="/files/GbluSmlBLnUlu1HoDVxL" alt=""><figcaption></figcaption></figure>

## Using Custom Fields with APIs

You can create and update custom fields using the Self-Service APIs and the import vulnerabilities application API.&#x20;

> Custom fields do not need to be configured in the administration settings in order to be created or updated via the APIs. However, if the custom field Key matches one that is already defined in the admin settings, it will be automatically typed to that setting when presented in the user interface.

When importing custom fields, you must supply a Key and a Value for each custom field as follows:

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "something",
</strong><strong>         "value": "some value..."           
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

The Key must meet the following conditions:

* is lowercase letters, uppercase letters, numbers or underscores.
* cannot start with underscore.

The Value must meet the following conditions:

* is String, String Array, or Array of Objects

### String

Strings are used to store data for **Input**, **Text Area**, **Date-picker**, **Rich-Text** and **Select** fields.

An example of a custom field with a string is as follows:

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "something",
</strong><strong>         "value": "...lorem ipsum..."           
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

If intending to use the data for a Date-picker, you must supply it as a UTC string e.g. 2021-06-03T23:15:33.008Z

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "something",
</strong><strong>         "value": "2021-06-03T23:15:33.008Z"           
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

### String Array (string\[])

String Arrays are used to store data for **Multi-Select, User Select, User Multi-Select, Group Select, Group Multi-Select** fields.

An example of a custom field with a string array is as follows:

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "something",
</strong><strong>         "value": [
</strong><strong>              "hello",
</strong><strong>              "goodbye"
</strong><strong>         ]          
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

### Array of Objects

Array of Objects are used to store data in tabular format for **Table** fields.&#x20;

Every object is considered a *row of data.*&#x20;

Every key in the object is considered a *column of data*.&#x20;

Therefore a 2x2 table:

| column1 | column2 |
| ------- | ------- |
| hello   | goodbye |
| goodbye | hello   |

Can be represented as follows:

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "table1",
</strong><strong>         "value": [
</strong><strong>              {
</strong><strong>                   "column1": "hello",
</strong><strong>                   "column2": "goodbye"
</strong><strong>              },
</strong><strong>              {
</strong><strong>                   "column1": "goodbye",
</strong><strong>                   "column2": "hello"
</strong><strong>              }
</strong><strong>         ]          
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

The objects must have a Key and Value.

The Key must meet the following conditions:

* is lowercase letters, uppercase letters, numbers or underscores.
* cannot start with underscore.

The Value must meeting the following conditions:

* is String or String Array

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "something",
</strong><strong>         "value": [
</strong><strong>              {
</strong><strong>                   "column1": "...lorem ipsum...",
</strong><strong>                   "column2": [
</strong><strong>                        "hello",
</strong><strong>                        "goodbye"
</strong><strong>                   ]
</strong><strong>              }
</strong><strong>         ]          
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

### Deleting Custom Fields

You can also delete a custom field by passing **null** to the Value as follows:

<pre class="language-json"><code class="lang-json"><strong>"custom_fields": [
</strong><strong>    {
</strong><strong>         "key": "something",
</strong><strong>         "value": null           
</strong><strong>    }
</strong><strong>]
</strong></code></pre>

## Custom System Fields

You can set custom fields for well-known system fields which provide additional functionality in AttackForge.

## Sys Field - Template POC

The following system field can be used to create a template steps to reproduce / proof of concept that will be automatically copied to the POC field when creating a new vulnerability.

* **Vulnerability Library Custom Field - af\_sys\_steps\_to\_reproduce**

To set up this custom system field, go to `Administration -> Writeups -> Custom Fields` and create a Text Area field with a key `af_sys_steps_to_reproduce`

<figure><img src="/files/WJI40UYuv2pKl4gkMHsL" alt=""><figcaption></figcaption></figure>

Now when that writeup is selected when adding a new vulnerability on a project, the template steps to reproduce / proof of concept will be automatically copied to the Steps to Reproduce field.

<figure><img src="/files/hmZnc27jWfZ8BFwfjWUf" alt=""><figcaption></figcaption></figure>


# Advanced Filtering

Advanced filtering can help you to *quickly and easily* find the information that you need, and help you save your search filter so you can easily repeat your search time and time again.

<figure><img src="/files/pf20S8Pp1gRcv5lULllq" alt=""><figcaption></figcaption></figure>

Advanced filtering is available in the `Filters` section of your table, or when selecting an individual column filter.

When selecting which advanced filter to use, there are multiple options to choose from:

* **Contains**
* **Starts With**
* **Ends With**
* **Wildcard**
* **Regex**

## Contains

The Contains filter is used to check whether an input value is contained anywhere within the search. It performs a case insensitive search by default.

<div align="left"><figure><img src="/files/rltVZbPP3oEnrO1fWJ2j" alt=""><figcaption></figcaption></figure></div>

### Example

Filtering for **xss** in the *Vulnerability title* using a Contains filter will check if **xss** is contained anywhere within the title. The following results would apply:

1. Reflected XSS **\[**<mark style="color:green;">**MATCH**</mark>**]**
2. Reflected Cross-Site Scripting (XSS) **\[**<mark style="color:green;">**MATCH**</mark>**]**
3. Xss Identified in Application Login Page **\[**<mark style="color:green;">**MATCH**</mark>**]**
4. Stored xss Leads to Stolen Session Token **\[**<mark style="color:green;">**MATCH**</mark>**]**

### Additional Filters

You can add further filters to Contains including:

* **Does Not** - checks if the input value is NOT contained anywhere within the search.
* **Match Whole Word** - checks if the input value is contained anywhere within the search and also matches the entire word. Using the example above - 1, 3 and 4 would be a match. 2 would not be a match due to the `(` and `)` wrapping the word **xss**.
* **Match Case** - checks if the input value is contained anywhere within the search and the case sensitivity also matches. Using the example above - only 4 would be a match. 1, 2 and 3 all have different cases in use.

## Starts With

The Starts With filter is used to search records which begin with the characters specified within the input value. It performs a case insensitive search by default.

<div align="left"><figure><img src="/files/GfAVas7O0rHJ0ktJPDks" alt=""><figcaption></figcaption></figure></div>

### Example

Filtering for **xss** in the *Vulnerability title* using a Starts With filter will check if **xss** is included at the start of the title. The following results would apply:

1. Reflected XSS **\[**<mark style="color:red;">**NO MATCH**</mark>**]**
2. Reflected Cross-Site Scripting (XSS) **\[**<mark style="color:red;">**NO MATCH**</mark>**]**
3. Xss Identified in Application Login Page **\[**<mark style="color:green;">**MATCH**</mark>**]**
4. Stored xss Leads to Stolen Session Token **\[**<mark style="color:red;">**NO MATCH**</mark>**]**

### Additional Filters

You can add further filters to Starts With including:

* **Does Not** - checks if the search DOES NOT begin with the characters specified within the input value.
* **Match Whole Word** - checks if the search begins with the characters specified within the input value and also matches the entire word. Using the example above - 3 would be a match.
* **Match Case** - checks if the search begins with the characters specified within the input value and the case sensitivity also matches. Using the example above - there would be no matches.

## Ends With

The Ends With filter is used to search records which end with the characters specified within the input value. It performs a case insensitive search by default.

<div align="left"><figure><img src="/files/abr3CvcnHB8V0wSYaw31" alt=""><figcaption></figcaption></figure></div>

### Example

Filtering for **xss** in the *Vulnerability title* using a Ends With filter will check if **xss** is included at the end of the title. The following results would apply:

1. Reflected XSS **\[**<mark style="color:green;">**MATCH**</mark>**]**
2. Reflected Cross-Site Scripting (XSS) **\[**<mark style="color:red;">**NO MATCH**</mark>**]**
3. Xss Identified in Application Login Page **\[**<mark style="color:red;">**NO MATCH**</mark>**]**
4. Stored xss Leads to Stolen Session Token **\[**<mark style="color:red;">**NO MATCH**</mark>**]**

### Additional Filters

You can add further filters to Starts With including:

* **Does Not** - checks if the search DOES NOT end with the characters specified within the input value.
* **Match Whole Word** - checks if the search ends with the characters specified within the input value and also matches the entire word. Using the example above - 1 would be a match.
* **Match Case** - checks if the search ends with the characters specified within the input value and the case sensitivity also matches. Using the example above - there would be no matches.

## Wildcard

The Wildcard filter is used to perform Glob matching or [*globbing*](https://en.wikipedia.org/wiki/Glob_\(programming\)#Syntax) using wildcards (like `*` and `?`) to match values based on patterns. It performs a case insensitive search by default.

<div align="left"><figure><img src="/files/C5pWzsWl7llexuV9kKZk" alt=""><figcaption></figcaption></figure></div>

### Supported Globs

* Wildcards (`**`, `*.js`)
* Negation (`'!a/*.js'`, `'*!(b).js'`)
* [extglobs](#extglobs) (`+(x|y)`, `!(a|b)`)
* POSIX character classes (`[[:alpha:][:digit:]]`)
* [brace expansion](#brace-expansion) (`foo/{1..5}.md`, `bar/{a,b,c}.js`)
* [regex character classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class) (`foo-[1-5].js`)
* regex logical "or" (`foo/(abc|xyz).js`)

### Extglobs

| **pattern**  | **regex equivalent**                            | **description**                                        |
| ------------ | ----------------------------------------------- | ------------------------------------------------------ |
| `?(pattern)` | `(pattern)?`                                    | Matches zero or one occurrence of the given patterns   |
| `*(pattern)` | `(pattern)*`                                    | Matches zero or more occurrences of the given patterns |
| `+(pattern)` | `(pattern)+`                                    | Matches one or more occurrences of the given patterns  |
| `@(pattern)` | `(pattern)` <sup>\*</sup>                       | Matches one of the given patterns                      |
| `!(pattern)` | N/A (equivalent regex is much more complicated) | Matches anything except one of the given patterns      |

### Brace Expansion

* Supports lists - (aka "sets") `a/{b,c}/d` => `['a/b/d', 'a/c/d']`
* Supports sequences - (aka "ranges") `{01..03}` => `['01', '02', '03']`
* Supports steps - (aka "increments") `{2..10..2}` => `['2', '4', '6', '8', '10']`
* Supports escaping `a\\{d,c,b}e` => `a{d,c,b}e`

### Examples

1. Filtering for **xss OR cross site scripting** anywhere in the *Vulnerability title:*

```
*xss*|*cross site scripting*
```

2. Filtering for **NOT 'HTTP' AND NOT 'MIME'** anywhere in the *Vulnerability title:*

```
!(*HTTP*|*MIME*)
```

### Additional Filters

You can add further filters to Starts With including:

* **Does Not** - checks if the search DOES NOT match the glob specified.
* **Match Case** - checks if the search matches the glob specified and the case sensitivity also matches.&#x20;

## Regex

The Regex filter is used to perform [Regular Expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions) matching based on patterns. It performs a case sensitive search by default.

<div align="left"><figure><img src="/files/vhbOAzddlgCKz7V43ttU" alt=""><figcaption></figcaption></figure></div>

### Examples

1. Filtering for **xss OR cross site scripting** anywhere in the *Vulnerability title:*

<pre><code><strong>^xss|cross site scripting$
</strong></code></pre>

2. Filtering for **NOT 'HTTP' AND NOT 'MIME'** anywhere in the *Vulnerability title:*

```
^(?!(.*HTTP.*|.*MIME.*)).*$
```

### Case Sensitivity

You can perform a case insensitive search by clicking on `flags` and selecting `insensitive` from the options.

<div align="left"><figure><img src="/files/fuHn7bLQkFdLZujEKEOO" alt=""><figcaption></figcaption></figure></div>

### Additional Filters

You can add further filters to Starts With including:

* **Does Not** - checks if the search DOES NOT match the RegEx specified.&#x20;

## Considerations

### List-type Fields

Filtering on List-type fields such as Tags will match against ANY of the tags. That is, if any tag in the list satisfies the filter, the filter will be considered a match.

For example, if you are filtering Vulnerabilities for the tag OWASP TOP 10 - so long as any of the tags on the vulnerability has the tag OWASP TOP 10, that vulnerability will be considered a match.

If you are wanting to filter Tags by multiple tags - for example to determine if any vulnerability has both OWASP TOP 10 and CWE TOP 25 - you can add multiple filter entries:

<div align="left"><figure><img src="/files/HwRZqjkHzwcew5uLj06C" alt=""><figcaption></figcaption></figure></div>

## Custom Views

Custom Views allows you to save your favourite and frequently used table views.

You can easily configure and switch between different views providing flexibility in how you want to see your data.

Each view is saved against your own personal user settings, so you can tailor each view to *exactly how you want it*. And best of all - you can create as many views as you need!

> **COMING SOON!** Ability for Admins to create Views and make them available to all or selected users, including setting default views for other users.

<div align="left"><figure><img src="/files/kJfSdrN71YWoB2vXEpij" alt=""><figcaption></figcaption></figure></div>

<figure><img src="/files/tdPsNi4xASaMlXHOBxae" alt=""><figcaption></figcaption></figure>

Each view is unique. You can save the following information and table state for each view:

* Table filters, including Advanced filters
* Column visibility
* Column filters, including Advanced filters
* Column ordering
* Column sorting
* Column locking
* Column features
* Table scroll

Each view gets its own name. You can also duplicate views to create similar ways of seeing your data.

<figure><img src="/files/5w6e0qG4wazP14pwONoN" alt=""><figcaption></figcaption></figure>

We also support re-ordering your views, making it easy to switch between frequently accessed data.

### Creating a New View

You can create a new View by clicking on the `+` icon:

<div align="left"><figure><img src="/files/LrYvA5SpoGrJDX16VQWl" alt=""><figcaption></figcaption></figure></div>

Once you have finished making the adjustments to your filtering and table, you can save your view by clicking on the `Save` icon:

<div align="left"><figure><img src="/files/Ti0E8J0sMOzsJKAgw1lC" alt=""><figcaption></figcaption></figure></div>

### Updating a View

You can update an existing View after making changes by clicking on the `Save` icon:

<div align="left"><figure><img src="/files/4bQaLoPnu4gfnHhXx7Ye" alt=""><figcaption></figcaption></figure></div>

### Restoring a View

If you are on a View and you have moved away from what the View is configured to show, for example by further filtering or sorting, you can return back to the original results for the View by clicking on the `Restore` icon:

<div align="left"><figure><img src="/files/0Iv18ZCc3XsMV5MabMyq" alt=""><figcaption></figcaption></figure></div>


# Vulnerability Scoring Systems

<figure><img src="/files/EfpJXdknxgQXklwf2DCS" alt=""><figcaption></figcaption></figure>

AttackForge empowers security teams to prioritise and score vulnerabilities using any existing industry framework like CVSS, DREAD, OWASP Risk Rating Methodology; or a custom framework tailored to the needs of the organization; or multiple frameworks on the same project and vulnerability - for a complete perspective.

Using multiple vulnerability scoring systems together provides several important advantages in security assessments:

* **Complementary perspectives.** Each system emphasizes different aspects of risk. CVSS focuses heavily on the technical characteristics of a vulnerability - exploitability, impact on confidentiality/integrity/availability. DREAD brings in factors like discoverability and the number of affected users, which CVSS doesn't directly address. The OWASP Risk Rating Methodology layers in business impact and threat agent factors, tying vulnerabilities more closely to organizational context. Together, they paint a fuller picture than any single system alone.
* **Better prioritization.** A vulnerability might score high on CVSS due to its technical severity but rank lower when DREAD or OWASP factors reveal it's hard to discover, affects few users, or has minimal business impact. Conversely, a technically moderate vulnerability might become urgent when business context shows it affects a revenue-critical system. Cross-referencing scores helps teams avoid both over-reacting and under-reacting.
* **Audience-appropriate communication.** CVSS scores are widely recognized and useful for communicating with technical teams and in compliance contexts. OWASP's business-impact dimension makes it easier to explain risk to executives and stakeholders. DREAD's intuitive categories (how reproducible is it? how exploitable?) can be useful in quick triage discussions. Having multiple frameworks lets you speak the right language to each audience.
* **Reduced bias and blind spots.** Every scoring system has limitations. CVSS, for example, has been criticized for score inflation and for not accounting for environmental context well in its base score. DREAD is sometimes seen as too subjective. Using multiple systems creates a check against the weaknesses of any single one - if all three frameworks agree a vulnerability is critical, you can have higher confidence in that assessment.
* **Flexibility across contexts.** Some systems work better for certain situations. CVSS is standard for published CVEs and vendor advisories. OWASP's methodology suits application-level risk assessments during development. DREAD can be useful for rapid internal triage. Having fluency in all three lets a security team pick the right tool for the situation or combine them as needed.

The main trade-off is additional effort - maintaining multiple scoring workflows takes more time and can introduce confusion if scores conflict without clear guidance on how to reconcile them. Most mature security programs address this by designating a primary system (often CVSS for its industry adoption) and using the others as supplementary lenses during prioritization discussions.

## Default Scoring Systems

AttackForge includes the following vulnerability scoring systems out-of-the-box on new deployments:

* [**CVSS version 3.1**](https://www.first.org/cvss/calculator/3.1)
* [**CVSS version 4.0**](https://www.first.org/cvss/calculator/4.0)
* [**DREAD Threat Model**](https://satoricyber.com/glossary/threat-modeling-with-microsoft-dread/)
* [**OWASP Risk Rating Methodology**](https://owasp.org/www-community/OWASP_Risk_Rating_Methodology)
* [**Custom 4x4 Risk Scoring**](https://en.wikipedia.org/wiki/Risk_matrix)

## Creating a Custom Scoring System

You can create and manage your vulnerability scoring systems in `Administration > Vulnerabilities > Scoring`:

<figure><img src="/files/cltqiukMKKrj8CkR8se8" alt=""><figcaption></figcaption></figure>

Start by clicking on `Add Custom Scoring System`.

<figure><img src="/files/dOKzgsaFnnLv8GGUq45L" alt=""><figcaption></figcaption></figure>

The Scoring System is made up of the following components:

* **Name**
  * This is the name of the scoring system
* **Key**
  * This is the unique identifier for the scoring system, which is used in scripts, APIs, etc.
* **Info**
  * This is an optional field to help describe the purpose and function of the scoring system.
* **Form**
  * This is where you can create your own [Sections and Fields](/app/getting-started/custom-fields-and-forms) which relate to your scoring system.
  * These fields will show when a vulnerability is getting scored during vulnerability creation or modification.
  * These fields may optionally show when viewing a vulnerability, to provide context on how the vulnerability was scored.
* **Priority Script**
  * This is the logic used to calculate the vulnerability priority.

### Form

The Scoring Form is made up of [Sections and Fields](/app/getting-started/custom-fields-and-forms) which are presented to users to help them score vulnerabilities, and to help understand the context for how the vulnerability was scored.

<figure><img src="/files/lqcoEO0GMvPEyounq6K6" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/AO0QqEsS3zURaABCH17C" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/0BqcPpYAPsbiRPFYg8nj" alt=""><figcaption></figcaption></figure>

### Priority Script

The Priority Script is where you configure the logic to calculate the vulnerability priority.

The Priority Script can factor in context from the following locations:

* Current scoring system
* Other scoring systems in use on the same project
* Vulnerability system fields and custom fields
* Writeup fields
* Affected Asset fields
* Project system fields and custom fields

<figure><img src="/files/mfg7RurS9VflJceECGHI" alt=""><figcaption></figcaption></figure>

The Priority Script must return one of the following values:

* Critical
* High
* Medium
* Low
* Info

<figure><img src="/files/2A3fSZo1e3jfMT26KnEV" alt=""><figcaption></figcaption></figure>

## Applying Scoring Systems to Projects

When creating or modifying a project - you can select from the available scoring systems:

<figure><img src="/files/XVwcYbASrgu5VMkEbBdj" alt=""><figcaption></figcaption></figure>

Projects can have as many scoring systems assigned as desired.

Scoring systems can be marked as `Required`. This means the person scoring vulnerabilities *must* use that scoring system.

Scoring systems can also be ordered from top to bottom, which controls the order in which the scoring systems are presented and also the relevance to the Priority on the vulnerability. In the event where multiple scoring systems have conflicting ratings, the highest ordered scoring system takes precedence.

<figure><img src="/files/zB0SxCsfXvc0daY1yDaY" alt=""><figcaption></figcaption></figure>

### Access to Scoring Systems

Every Custom Scoring System assigned to the project can have an access level applied to it. This determines whether a user viewing the vulnerability can see how the vulnerability was scored.

<figure><img src="/files/0wdI3rp2uC7C5Felhge7" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/H7sfa0kH7F7Mlq3Au6NY" alt=""><figcaption></figcaption></figure>

In some cases, you may choose to withhold the details on how the vulnerability was scored using the custom scoring systems. In these cases, you can adjust the `Access Level` and `Roles` on the scoring system which sets the *lowest project access level that has access to the scoring system*.

<figure><img src="/files/6Oh6b37bVFwaYuQMRwJG" alt=""><figcaption></figcaption></figure>

## Using Scoring Systems on Vulnerabilities

When creating or modifying a vulnerability, the assigned scoring systems to the project will become accessible on the vulnerability form:

<figure><img src="/files/n8UTrtIsWg2BBqcIaCKt" alt=""><figcaption></figcaption></figure>

Mandatory scoring systems will have a *red dot* indicating a score is required.

When you click on any of the scoring systems, the relevent form will be presented where you can enter in the scoring values:

<figure><img src="/files/pYAIdqWCkoNDKtysm9M3" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oUR061lro9BvFq4w0ISn" alt=""><figcaption></figcaption></figure>

After you fill in the scoring details, the Priority will be updated. You can override the Priority manually. AttackForge will indicate where a Priority does not align with existing scoring systems:

<figure><img src="/files/KWnde8xQ31G7UK2Ka0Gm" alt=""><figcaption></figcaption></figure>

## Scoring System Priority Scripts

### DREAD Threat Model

This example assumes that you have the following fields configured on this scoring system:

<figure><img src="/files/xkXsruruno0C96q8zkc8" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/JxduLwV7hutysQWySl5a" alt=""><figcaption></figcaption></figure>

Each field is a SELECT field which has a value of 1 to 10.

```javascript
const dread = vulnerability?.scoring?.custom?.dread?.custom_fields;
const damage_potential = dread?.damage_potential;
const reproducibility = dread?.reproducibility;
const exploitability = dread?.exploitability;
const affected_users = dread?.affected_users;
const discoverability = dread?.discoverability;

const score = Number.parseInt(damage_potential)
  + Number.parseInt(reproducibility)
  + Number.parseInt(exploitability)
  + Number.parseInt(affected_users)
  + Number.parseInt(discoverability);

if (score >= 1 && score <= 10) {
  return 'Low';
}
else if (score >= 11 && score <= 24) {
  return 'Medium';
}
else if (score >= 25 && score <= 39) {
  return 'High';
}
else if (score >= 40 && score <= 50) {
  return 'Critical';
}
else {
  return 'Info'
}
```

### OWASP Risk Rating Methodology

This example assumes that you have the following sections and fields configured on this scoring system:

<figure><img src="/files/nZJklmxWJs1I2cVaeAzo" alt=""><figcaption></figcaption></figure>

Likelihood Factors:

<figure><img src="/files/vsyt84edjC801iuS2hxr" alt=""><figcaption></figcaption></figure>

Impact Factors:

<figure><img src="/files/AIIzJJkF25Re8YACDbzE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/NTHnU7rCpnmTfe6kgn5n" alt=""><figcaption></figcaption></figure>

Each field is a SELECT field which has a value of 0 to 9.

```javascript
const scoring = vulnerability.scoring.custom.owasp_risk_rating_methodology.custom_fields;
const entries = Object.entries(scoring);

let threat_agent_factor = 0;
let vulnerability_factor = 0;
let technical_impact_factor = 0;
let business_impact_factor = 0;

for (let x = 0; x < Array.length(entries); x++) {
  const key = entries[x][0];
  const value = Number.parseInt(entries[x][1]);
  
  if (String.startsWith(key, 'ta_')){
    threat_agent_factor = threat_agent_factor + value;
  }
  else if (String.startsWith(key, 'vf_')){
    vulnerability_factor = vulnerability_factor + value;
  }
  else if (String.startsWith(key, 'ti_')){
    technical_impact_factor = technical_impact_factor + value;
  }
  else if (String.startsWith(key, 'bi_')){
    business_impact_factor = business_impact_factor + value;
  }
}

const likelihood = ( threat_agent_factor + vulnerability_factor ) / 8;
const impact = ( technical_impact_factor + business_impact_factor ) / 8;

let likelihood_rating;
if (likelihood >= 0 && likelihood < 3) {
  likelihood_rating = 'Low';
}
else if (likelihood >= 3 && likelihood < 6) {
  likelihood_rating = 'Medium';
}
else if (likelihood >= 6 && likelihood <= 9) {
  likelihood_rating = 'High';
}

let impact_rating;
if (impact >= 0 && impact < 3) {
  impact_rating = 'Low';
}
else if (impact >= 3 && impact < 6) {
  impact_rating = 'Medium';
}
else if (impact >= 6 && impact <= 9) {
  impact_rating = 'High';
}

if (impact_rating === 'High' && likelihood_rating === 'Low') {
  return 'Medium';
}
else if (impact_rating === 'High' && likelihood_rating === 'Medium') {
  return 'High';
}
else if (impact_rating === 'High' && likelihood_rating === 'High') {
  return 'Critical';
}
else if (impact_rating === 'Medium' && likelihood_rating === 'Low') {
  return 'Low';
}
else if (impact_rating === 'Medium' && likelihood_rating === 'Medium') {
  return 'Medium';
}
else if (impact_rating === 'Medium' && likelihood_rating === 'High') {
  return 'High';
}
else if (impact_rating === 'Low' && likelihood_rating === 'Low') {
  return 'Info';
}
else if (impact_rating === 'Low' && likelihood_rating === 'Medium') {
  return 'Low';
}
else if (impact_rating === 'Low' && likelihood_rating === 'High') {
  return 'Medium';
}
```

### Custom 4x4 Risk Scoring

This example is based on a standard 4x4 risk matrix:

<figure><img src="/files/YdBLtAiguKe33Pjan9N6" alt=""><figcaption></figcaption></figure>

This example assumes that you have the following fields configured on this scoring system:

<figure><img src="/files/chYa1FsFxArOnPDaZWUx" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Ab9j029CMS6adphWoqBc" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/2nXarPsj6VblLhJd9p01" alt=""><figcaption></figcaption></figure>

```javascript
const scoring = vulnerability.scoring.custom.custom_risk_scoring.custom_fields;

const probability = scoring.probability;
const impact = scoring.impact;

if (probability === 'Rare' && impact === 'Low') {
  return 'Low';
}
else if (probability === 'Rare' && impact === 'Medium') {
  return 'Low';
}
else if (probability === 'Rare' && impact === 'High') {
  return 'Low';
}
else if (probability === 'Rare' && impact === 'Very High') {
  return 'Medium';
}
else if (probability === 'Unlikely' && impact === 'Low') {
  return 'Low';
}
else if (probability === 'Unlikely' && impact === 'Medium') {
  return 'Medium';
}
else if (probability === 'Unlikely' && impact === 'High') {
  return 'Medium';
}
else if (probability === 'Unlikely' && impact === 'Very High') {
  return 'High';
}
else if (probability === 'Possible' && impact === 'Low') {
  return 'Low';
}
else if (probability === 'Possible' && impact === 'Medium') {
  return 'Medium';
}
else if (probability === 'Possible' && impact === 'High') {
  return 'High';
}
else if (probability === 'Possible' && impact === 'Very High') {
  return 'Critical';
}
else if (probability === 'Probable' && impact === 'Low') {
  return 'Medium';
}
else if (probability === 'Probable' && impact === 'Medium') {
  return 'High';
}
else if (probability === 'Probable' && impact === 'High') {
  return 'Critical';
}
else if (probability === 'Probable' && impact === 'Very High') {
  return 'Critical';
}
```


# Vulnerability SLAs

Vulnerability Remediation SLAs are a powerful way to triage vulnerabilities to make vulnerability management more effective and efficient.

> Every vulnerability can be assigned a `Remediation SLA`. AttackForge helps to keep on top of vulnerabilities as they get closer to their SLA, by making it easy to filter, identify, action and export. Every SLA is color-coded and includes a countdown tracker.

AttackForge provides a rules-based engine to configure custom vulnerability SLAs. This powerful utility allows you to create SLAs which meet specific conditions based on vulnerability, asset, writeup and project data points.

Vulnerability Remediation SLAs can be enabled by Administrators via `Administration` module.

<figure><img src="/files/piHTtpWHlKzVgMv5e8y3" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Th6Z5tIp0Rq5GBisLkTr" alt=""><figcaption></figcaption></figure>

## Configuring SLA Rules

AttackForge provides a rules-based engine to configure custom vulnerability SLAs. This powerful utility allows you to create SLAs which meet conditions based on vulnerability, asset and project datapoints.

<figure><img src="/files/7ubwU6ZjOhPPKiNgOYaV" alt=""><figcaption></figcaption></figure>

Every rule will resolve to either `True` or `False`.&#x20;

If True, the SLA associated to the rule will be applied to the vulnerability. For example, if the SLA is 15 days - the vulnerability will receive an SLA of 15 days.

> Every rule is evaluated from top to bottom. This means any SLAs at the top of the list, will be evaluated and compared first. Once a rule has been matched (is True) - no further rules will be evaluated for that vulnerability.

Rules are made up of `Expressions`. Every expression has at least 1 Datapoint and 1 Operator. Multiple expressions can be grouped together to create more powerful SLAs. See examples below.

<figure><img src="/files/EoukxV98oZ1YKp8z1ffI" alt=""><figcaption></figcaption></figure>

### Datapoints

* **vuln.id** - <*string*> id of the vulnerability
* **vuln.created** - <*date*> created timestamp of the vulnerability
* **vuln.modified** - <*date*> modified timestamp of the vulnerability
* **vuln.priority** - <*string*> priority of the vulnerability. Is either Critical / High / Medium / Low / Info
* **vuln.title** - <*string*> title of the vulnerability
* **vuln.status** - <*string*> status of the vulnerability. Is either Open or Closed
* **vuln.tags** - <*string array*> list of tags associated on the vulnerability&#x20;
* **vuln.zeroday** - <*string*> whether vulnerability is a zero-day. Is either Yes or No
* **vuln.likelihood\_of\_exploitation** - <*integer*> likelihood of exploitation for vulnerability. Is a number between 1 to 10
* **vuln.ready\_for\_retest** - <*string*> whether vulnerability is ready for retest. Is either Yes or No
* **vuln.visible** - <*string*> whether vulnerability is visible or pending. Is either Yes or No
* **vuln.alternate\_id** - <*string*> user-friendly alternate vulnerability id
* **vuln.cvssv3\_vector** - <*string*> vulnerability cvss v3 vector string
* **vuln.cvssv3\_base\_score** - <*string*> vulnerability cvss v3 base score
* **vuln.cvssv3\_temporal\_score** - <*string*> vulnerability cvss v3 temporal score
* **vuln.cvssv3\_environmental\_score** - <*string*> vulnerability cvss v3 environmental score
* **vuln.custom\_tag\_\<key>** - <*string*> vulnerability custom tag. Replace \<key> with the name of your tag
* **vuln.custom\_field\_\<key>** - <*string*> vulnerability custom field. Replace \<key> with the key of your custom field
* **vuln.library\_id** - <*string*> id of the vulnerability writeup
* **vuln.library\_created** - <*date*> created timestamp of the vulnerability writeup
* **vuln.library\_modified** - <*date*> modified timestamp of the vulnerability writeup
* **vuln.library\_code** - <*string*> numerical code of the vulnerability writeup. Is a 15-digit number
* **vuln.library\_tags** - <*string array*> list of tags associated on the vulnerability writeup
* **vuln.library\_import\_source** - <*string*> tool for the imported vulnerability writeup
* **vuln.library\_import\_source\_id** - <*string*> plugin/external id for the imported vulnerability writeup
* **vuln.library\_custom\_tag\_\<key>** - <*string*> vulnerability writeup custom tag. Replace \<key> with the name of your tag
* **vuln.library\_custom\_field\_\<key>** - <*string*> vulnerability writeup custom field. Replace \<key> with the key of your field
* **asset.id** - <*string*> id of the asset
* **asset.created** - <*date*> created timestamp of the asset
* **asset.modified** - <*date*> modified timestamp of the asset
* **asset.name** - <*string*> name of the asset
* **asset.external\_id** - <*string*> external id of the asset
* **asset.type** - <*string*> asset type
* **asset.details** - <*string*> details for the asset
* **asset.custom\_field\_\<key>** - <*string*> asset custom field. Replace \<key> with the key of your field
* **project.id** - <*string*> id of the project
* **project.created** - <*date*> created timestamp of the project
* **project.modified** - <*date*> modified timestamp of the project
* **project.name** - <*string*> name of the project
* **project.code** - <*string*> code for the project
* **project.start\_date** - <*date*> start date for the project&#x20;
* **project.end\_date** - <*date*> end date for the project
* **project.scope** - <*string array*> list of asset names
* **project.organization\_code** - <*string*> organization code for the project
* **project.vulnerability\_code** - <*string*> user-friendly alternate vulnerability id prefix
* **project.custom\_tag\_\<key>** - <*string*> project custom tag. Replace \<key> with the name of your tag
* **project.custom\_field\_\<key>** - <*string*> project custom field. Replace \<key> with the key of your field

### Operators

* **NOT** or **!** - used to negate an expression. For example *!(vuln.priority == "Critical")*
* **AND** or **&&** - used to *and* multiple expressions. For example *vuln.priority == "Critical" AND asset.type == "Web App"*
* **OR** or **||** - used to *or* multiple expressions. For example *vuln.priority == "Critical" OR vuln.priority == "High"*
* **==** - used to check for equivalency. For example *vuln.priority == "Critical"*
* **===** - used to check for equality. For example *vuln.priority === "Critical"*
* **!==** - used to check for not equivalency. For example *vuln.priority !== "Critical"*
* **>** - used to check for greater-than comparison. For example *vuln.likelihood\_of\_exploitation > 5*
* **<** - used to check for less-than comparison. For example *vuln.likelihood\_of\_exploitation < 5*
* **>**= - used to check for greater-than-or-equals comparison. For example *vuln.likelihood\_of\_exploitation >= 5*
* **<=** - used to check for less-than-or-equals comparison. For example *vuln.likelihood\_of\_exploitation <= 5*
* **( )** - used to group statements together. For example ((vuln.priority == "Critical") AND (*asset.type == "Web App"*)) OR ((vuln.priority == "Critical") AND (*asset.type == "API"*))

### Examples

```
vuln.priority == "Critical"
```

This rule will evaluate whether the vulnerability has a priority of Critical, and if so, it will apply the related SLA to that vulnerability.

```
vuln.priority == "Critical" OR vuln.priority == "High"
```

This rule will evaluate whether the vulnerability has a priority of Critical or High, and if so, it will apply the related SLA to that vulnerability.

```
vuln.priority == "Critical" AND vuln.likelihood_of_exploitation > 5
```

This rule will evaluate whether the vulnerability has a priority of Critical and a likelihood of exploitation which is greater than 5, and if so, it will apply the related SLA to that vulnerability.

{% code overflow="wrap" %}

```
((vuln.priority == "Critical") AND (asset.type == "Web App")) OR ((vuln.priority == "High") AND (asset.type == "API"))
```

{% endcode %}

This rule will evaluate whether the vulnerability has a priority of Critical and affected asset is of type Web App, or whether the vulnerability has a priority of High and affected asset is of type API, and if so, it will apply the related SLA to that vulnerability.

## Custom Emails on SLAs

You can configure custom emails for SLAs in the `Custom Time-Based Emails`.

Check [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications#custom-time-based-emails) for more information.

<figure><img src="/files/VWM2XT1qo8oSV3KS0EeI" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/ypjGrE0QhBNtcH9h7yrs" alt=""><figcaption></figcaption></figure>


# User Settings

## Profile

You can access your profile by clicking on Profile from your user menu.

<figure><img src="/files/oM8oN2sINhWM7oWwdGss" alt=""><figcaption></figcaption></figure>

Here you can configure all of your profile information.

<figure><img src="/files/F4Iv1UKwTFAU0lMOpXXG" alt=""><figcaption></figcaption></figure>

## Account Settings

Account settings is where you can make changes to account related information.&#x20;

> Admins will be able to access more details than non-admin users.&#x20;

<figure><img src="/files/lGz6CxBifUiba4I7AS4r" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/X7yuUFXAZ4IcEpYigxQ7" alt=""><figcaption></figcaption></figure>

## Secrets

Secrets are any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

User Secrets make it easy to store and update that sensitive information in one place, for example rotating passwords and credentials without having to update other places where that information is used.

User Secrets can only be created and viewed by the user which created the secret.

You can also choose to share your secret with other users if you would need to.

<figure><img src="/files/uo3FtDM9PDMzHqMbGKLd" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/prL1aUV53h7lHOStHzYL" alt=""><figcaption></figcaption></figure>

### User API Key

Every user in AttackForge can generate one User API Key. This key can be used in multiple places, including:

* Authentication in the [RESTful Self-Service API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api)
* Authentication in the [Events API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api)
* Authentication in [HTTP Trigger Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#external-events)

The key is static and does not expire. You can regenerate your key at any time by clicking on the `regenerate` button.

<figure><img src="/files/jTO1WYUYdGQEwfEXlxZR" alt=""><figcaption></figcaption></figure>

## Notifications

Notifications is where you can opt-in and opt-out of certain types of email notifications.

<figure><img src="/files/b2yhUnxptLb2m2A6Aswv" alt=""><figcaption></figcaption></figure>

### Project Updates

Project Team Notifications are intended to help keep you informed throughout the lifecycle of a project. For example, you can choose to be notified when testing has commenced or stopped daily, when new vulnerabilities are discovered, or when a project is on-hold.

To receive these notifications:

* you should be a team member on a project; or you should have inherited access to the project via a group, and that group has emails enabled; and
* the project has email notifications enabled; and
* your email notifications are not disabled in the projects' settings

If you decide to disable certain types of emails, even when they are enabled for you on the project - you will not receive them. You ultimately control the project notifications you will receive.\
However, under certain circumstances - an administrator or project coordinator may decide to force an email to be sent, for example new critical vulnerability that you should be aware of. Your administrators and project coordinators will configure any forced emails, per project.

The Project Team Notifications include the following:

* **No Emails** - Under normal circumstances, you will not receive any email notifications for any projects you are a team member.
* **All Emails** - You will receive all enabled emails for you on all projects you are a team member.
* **Daily Start/Stop Testing** - You will receive notifications each time a team member starts or stops testing each day, where this option is enabled for you on the project.
* **New Critical Vulnerability** - You will receive notifications each time a team member discovers a new critical vulnerability, where this option is enabled for you on the project.
* **New High Vulnerability** - You will receive notifications each time a team member discovers a new high vulnerability, where this option is enabled for you on the project.
* **New Medium Vulnerability** - You will receive notifications each time a team member discovers a new medium vulnerability, where this option is enabled for you on the project.
* **New Low Vulnerability** - You will receive notifications each time a team member discovers a new low vulnerability, where this option is enabled for you on the project.
* **New Informational Vulnerability** - You will receive notifications each time a team member discovers a new informational vulnerability, where this option is enabled for you on the project.
* **Vulnerability Ready for Retesting** - You will receive notifications each time a team member marks a vulnerability as ready for retesting, where this option is enabled for you on the project.
* **Vulnerability Re-Opened** - You will receive notifications each time a team member marks a vulnerability as re-opened, where this option is enabled for you on the project.
* **Vulnerability Closed** - You will receive notifications each time a team member marks a vulnerability as closed, where this option is enabled for you on the project.
* **Project Role Updated** - You will receive notifications each time your role on a project has been updated, where this option is enabled for you on the project.
* **Project On-Hold/Off-Hold** - You will receive notifications each time the project is placed on-hold or off-hold, where this option is enabled for you on the project.
* **Retest Completed** - You will receive notifications each time a round of retesting has been completed, where this option is enabled for you on the project.

### Daily Project Update

The Daily Project Update Email is intended to provide an overview of activities happening on your projects over the past 24 hours.

You must be a member on the project team in order for the project to be included in this update.

The Daily Project Update Email includes the following:

* Total number of **Vulnerabilities Discovered** in past 24 hours, including Critical, High, Medium, Low & Info
* Total number of **Vulnerabilities Closed** in past 24 hours
* Total number of **Vulnerabilities Ready for Retest** in past 24 hours
* Total number of **Projects In-Progress**, including project name, test window, total number of vulnerabilities & total number of completed test cases
* Total number of **Projects Completed** in past 24 hours, or completed ahead of schedule over next week, including project name, test window & total number of vulnerabilities
* Total number of **Projects Overrun** (i.e. testing has not yet completed & project has exceeded scheduled end date), including project name, test window & total number of completed test cases
* Total number of **Projects Waiting to Start** in next 7 days, including project name, test window & total number of test cases allocated

Daily Project Update supports following options:

* Enable/Disable - depending on whether you would like to use the feature or not. Default is Disabled.
* Time each day the email will be sent - this is based on the geographical region assigned to your tenant. The emails will be sent at any given point during the selected hour.

![](/files/-Maakz4l7yRvlwCJHJ4s)

![](/files/-Maal0A29zNengKurkPC)

### Weekly Project Update

The Weekly Project Update Email is intended to provide an overview of activities happening on your projects over the past 7 days.

You must be a member on the project team in order for the project to be included in this update.

The Weekly Project Update Email includes the following:

* Total number of **Vulnerabilities Discovered** in past 7 days, including Critical, High, Medium, Low & Info
* Total number of **Vulnerabilities Closed** in past 7 days
* Total number of **Vulnerabilities Ready for Retest** in past 7 days
* Total number of **Projects In-Progress**, including project name, test window, total number of vulnerabilities & total number of completed test cases
* Total number of **Projects Completed** in past 7 days, or completed ahead of schedule over next week, including project name, test window & total number of vulnerabilities
* Total number of **Projects Overrun** (i.e. testing has not yet completed & project has exceeded scheduled end date), including project name, test window & total number of completed test cases
* Total number of **Projects Waiting to Start** in next 7 days, including project name, test window & total number of test cases allocated

Weekly Project Update supports following options:

* Enable/Disable - depending on whether you would like to use the feature or not. Default is Disabled.
* Time each day the email will be sent - this is based on the geographical region assigned to your tenant. The emails will be sent at any given point during the selected hour.
* Day of the week the emails will be sent.

![](/files/-MaalQdMtTsnh5c3Pn0Q)

![](/files/-MaalSU_TYTOm7a9TxOg)

## Preferences

You can modify your landing page when you log into the application, as well as set your theme.

<figure><img src="/files/7TVkMy56STVNxstIANlh" alt=""><figcaption></figcaption></figure>

You can also choose to share your secret with other users if you would need to.

<figure><img src="/files/uo3FtDM9PDMzHqMbGKLd" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/prL1aUV53h7lHOStHzYL" alt=""><figcaption></figcaption></figure>

## Custom Tables

In AttackForge, anywhere you see a cog on the top-right of a table, means you can configure the table settings to your preferences. Settings will apply only to you.

When you click on the cog, you can set the following configurations for the table:

* Results per page
* Default sort column
* Default sort order
* Enable/disable columns
* Rearrange columns

<figure><img src="/files/jbYDMn34x1Q8Qg4QukND" alt=""><figcaption></figcaption></figure>


# Login Redirects

AttackForge supports login redirects to help your users & customers access the data they need, quickly and efficiently.

You can append any of the following redirects to the login URL, which can be shared with customers:

* **?redirectTo=sso**
  * This will automatically redirect the user to sign in with Single-Sign-On (if available)
* **?redirectTo=register**
  * This will automatically redirect the user to registration page (if available)
* **?redirectTo=resetPassword**
  * This will automatically redirect the user to password reset page (if available)
* **?redirectTo=/app/…**
  * This will automatically redirect the user to a page within AFE, after the user has logged in.

Examples are provided below for reference:

* https\://\<AFE\_TENANT>/#!/login?redirectTo=sso
  * Redirect to SSO login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=register
  * Redirect to registration page
* https\://\<AFE\_TENANT>/#!/login?redirectTo=resetPassword
  * Redirect to password reset
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/app/projects
  * Redirect to list of all projects after login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/app/projects/5bdd20d8128aa82e0040a75d/dashboard
  * Redirect to Project Dashboard for a specified project, after login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/app/projects/5bdd20d8128aa82e0040a75d/overview
  * Redirect to Project Overview for a specified project, after login
* https\://\<AFE\_TENANT>/#!/login?redirectTo=/app/schedule
  * Redirect to Schedule / Calendar after login


# Modules


# Dashboard

## Overview

The global dashboard provides a single pane of glass view into your security testing projects and vulnerabilities.

> Every user has a global dashboard which is presented upon login. The data is made up of projects and vulnerabilities the user has access to.

<figure><img src="/files/Bp8cQPfwTxsLs4diIAZX" alt=""><figcaption></figcaption></figure>

&#x20;Clicking on any dashboard box will drill down to see data.

<figure><img src="/files/uImZW98HcbWS2gB559bq" alt=""><figcaption></figcaption></figure>


# Analytics

## Overview

AttackForge incorporates Analytics to help you answer the tough questions.&#x20;

We help management & executives track performance of their organisation, business unit, suppliers, teams, platforms and technologies, and compliance - to make more informed decisions backed by real hard data.

> Analytics can be viewed globally across all projects, assets and vulnerabilities the user has access to.

You can also compare different time periods and/or groups to filter down the results to specific reporting periods or requirements.

All analytics can be clicked on to see the underlying data, for further analysis.

<figure><img src="/files/0P7KHNNZaRNpa7RLKtIP" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/VIN4CO4g8IdMUvCDpqHV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/u8Y2Xp4jONhM6NKv99jP" alt=""><figcaption></figcaption></figure>


# Vulnerabilities

## Overview

Vulnerabilities module is where you can see all your vulnerabilities. Here you can filter and track your vulnerabilities to help make remediation and follow ups faster and easier.

AttackForge supports extensive filters to help you get the right information you need. You can combine filters to drill-down the results. You can sort and group data. You can also toggle different views of certain fields.

> **Tip:** you can customize your table settings to see the data how you want it. Check [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#custom-tables) for more details.

See [Creating Vulnerabilities](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-vulnerabilities) and [Updating Vulnerabilities](https://support.attackforge.com/attackforge-enterprise/getting-started/updating-vulnerabilities) for more information on vulnerabilities.

<figure><img src="/files/vpWjAmt7Extb7COrwm6b" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/pyPxD5cjoJjytNskpZ1N" alt=""><figcaption></figcaption></figure>


# Projects

## My Projects

AttackForge is built upon Projects. Each project has scope (assets) and findings (vulnerabilities). Vulnerabilities are linked to assets and to projects.

Projects can be any of the following, however is not limited to the following:

* **Red Team and Purple Team Assessments**
* **Web Application Penetration Test**
* **Web Services / API Penetration Test**
* **Mobile Application Penetration Test**
* **Network and Infrastructure Penetration Test**
* **Wireless Network Assessment**
* **Source Code Review**
* **Configuration Audit**
* **PCI-DSS Assessment**
* **SCADA Assessment**
* **OSINT Assessment**
* **Physical Security Audit**

Only Administrators, Project Coordinators, and delegated users can create a new project.

Projects module is where you can see all of your projects, including ones that you have created; or ones that you have been invited to by other users.

<figure><img src="/files/pW7xcmIynxzdKRAN9Bos" alt=""><figcaption></figcaption></figure>

To create a new project, click on `New -> Project` from the page menu.

Otherwise for all other users, you can request a new project by clicking on `New -> Project Request`.

See [Getting Started](https://support.attackforge.com/attackforge-enterprise/getting-started) for more details on how to [Create a New Project](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects) or how to [Request a New Project](https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project).

You can click on any of the projects to drill down into the project view. From here, you can view testing progress, view vulnerabilities, download reports and more.

<figure><img src="/files/0TWl7iHAUlQiYageExKg" alt=""><figcaption></figcaption></figure>

### Retesting

You can toggle the projects table into a retesting view in order to see remediation related details for each project, such as how many vulnerabilities have been remediated or how many retests have been performed.

<figure><img src="/files/7Y4W37i7XpJ8aUGwPbvg" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/OYHwTYAp2Nvg6X77r1CE" alt=""><figcaption></figcaption></figure>

See [Retesting & Remediation](https://support.attackforge.com/attackforge-enterprise/getting-started/retesting) for more details on how to manage retesting.

## User Projects

Authorized users can view projects on behalf of another user. This can be helpful for resource management and team planning.

Start by selecting `User Projects` from the menu, then select a user. You can filter the results further by searching projects where user is assigned a particular role. You can also toggle between a list view and a calendar view.

<figure><img src="/files/XpSbcgofLf3HC94EXSlm" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/ZJbHrsPTjr2Nr0XCvZUa" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/wFmtirda8nWsUcDNigTT" alt=""><figcaption></figcaption></figure>

## Archived Projects

Archived projects provides a list of all projects which have been archived.

Archived projects are hidden and only accessible to Admins. The results from the archived projects (such as vulnerabilities) are not included in Analytics or other sections with AttackForge.

Archived projects can be restored at any time by selecting `Restore Project` from the actions menu.

You can also delete projects entirely. This can be performed using the `Destroy Project Data` option.

> **!WARNING** - once a project has been destroyed, there is no way to recover it or its data.

<figure><img src="/files/TC26PEKxqGs8NzC4uP2F" alt=""><figcaption></figcaption></figure>

## Pending Requests

Pending requests section provides a list of all project requests which are awaiting review & action.

The authorized users will be notified when a new project request is submitted and can commence the review process. The submitter will also be notified by email that their request is pending review.

<figure><img src="/files/5yvzm8VpR3PkW9E00jLo" alt=""><figcaption></figcaption></figure>

Users can make modifications to their pending requests.

Authorized users can `Approve` or `Reject` requests.&#x20;

If a request is Approved, the project will be automatically created and the customer will be invited to the project. The customer will be notified by email that their request was approved.

See [Requesting a Project](https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project) for more information on project requests.

## Actioned Requests

All users can see the history of their project requests in the `Actioned Requests` section.

You can click on the request to drill down on the information which had been submitted & reviewed.

<figure><img src="/files/u1Av99gabB6HbkZUOsR7" alt=""><figcaption></figcaption></figure>


# Scheduling

## Overview

Schedule keeps you on top of all your projects - historical, present & future.

> All projects are color-coded to quickly identify their status.

You can filter the schedule by Month, Week or Day. You can look back in time to see historical projects; or to the future to plan for upcoming projects.

<figure><img src="/files/PC9TW49Sw6PZoPXI9TxA" alt=""><figcaption></figcaption></figure>

Clicking on a project request will display key information on that project.

<figure><img src="/files/thEVV0VTA1Jb0SslguTy" alt=""><figcaption></figcaption></figure>

From here, you can visit the `Project Tracking` or `Project Overview`

<figure><img src="/files/P1qZodIfBUBVXhuKkV9s" alt=""><figcaption></figcaption></figure>

## Resource Allocation

As a `Project Coordinator` or `Administrator` - you can view the schedules on behalf of other users to help you with resource allocation.

<figure><img src="/files/2ZVON732L1g1LaPwo76t" alt=""><figcaption></figcaption></figure>

You can look up schedules per user, or filter by roles.

<figure><img src="/files/zWCjdwF7OKhTzfZtWgsw" alt=""><figcaption></figcaption></figure>

You can also filter by certain roles that the user is assigned on projects. For example, show me everything where this person was the *Pentest Lead*.

<figure><img src="/files/X5Ql3rJBDH1PFzJypHV0" alt=""><figcaption></figcaption></figure>

You can also switch into a list view, to see more information about those projects. And you can also export this information into a CSV for working with the data offline.

<figure><img src="/files/V3yL8ZvyhmlocWnq6eA4" alt=""><figcaption></figcaption></figure>


# Portfolios

## Overview

Portfolios help you to create dedicated programs to track and manage your security testing activities. Want to know how your internal systems compare to your external systems? Or wanting to track security posture for your applications? Portfolios makes this easy!

<figure><img src="/files/PGGHQ0aY8dX2OP1Oafw2" alt=""><figcaption></figcaption></figure>

Every `Portfolio` comes with `Streams`.&#x20;

Streams help you to consolidate all of your related testing activities for a portfolio, for example:

![](/files/mIfLpACaIVAFLwhpXTgq)

#### **Portfolio:&#x20;*****Externally Facing Applications***

**Stream 1:&#x20;*****US External Apps***

* Project 1: USA Commerce Portal
* Project 2: USA Mobile App
* Project 3: Main Integration Gateway

**Stream 2:&#x20;*****European External Apps***

* Project 1: Main Integration Gateway
* Project 2: EU Mobile App

![](/files/Qh4KNnOIrDlZEwrhtcaC)

Portfolios and Streams can help you track Business-as-Usual (BAU) pentesting and help you to better understand where to focus your time and resources more effectively.

Projects can be assigned to many streams and portfolios. This can help to ensure you are tracking the right vulnerabilities, across your enterprise.

Using the example above, vulnerabilities in project `Main Integration Gateway` might be relevant to both `USA External Apps` & `European External Apps` - therefore could be assigned to both streams.

Every Portfolio and Stream has a unique dashboard which includes details on vulnerabilities, projects & assets - helping you make more informed business decisions when it comes to tracking and remediation.

Portfolios are created and managed by administrators.

View access can be given to individual portfolios, or their respective streams, to non-admin users. Those users will only see data relevant to projects they have access to on the portfolio and/or stream.

<figure><img src="/files/Oo1VD9CId5OclfYbR07c" alt=""><figcaption></figcaption></figure>

## Creating a Portfolio

Start by clicking on Portfolios module in your main menu. You must be an Administrator. Click on `New`.

<figure><img src="/files/FdhFQvk5zaF1cYYMN5L0" alt=""><figcaption></figcaption></figure>

Complete the details for your portfolio. You can add a stream by clicking on `Streams`.

<figure><img src="/files/BwHb0GeW80A3NGL9iy7C" alt=""><figcaption></figcaption></figure>

Enter a name for your stream, and optionally link any existing projects to the stream.&#x20;

> You can create as many streams as you need.

View access can be given to to non-admin users for individual portfolios or their respective streams. Those users will only see data relevent to projects they have access to on the portfolio and/or stream.

## Linking Projects to Streams

You can add new projects to Portfolios & Streams directly..

To edit a Portfolio, click on the `cog` from the portfolio page.

<figure><img src="/files/VHFoR0ih2fNae6kPCNBj" alt=""><figcaption></figcaption></figure>

Select Access and Streams. You can create new streams and link associated projects, or you can update existing projects on existing streams.

<figure><img src="/files/DMUb8My3EqOgalXuZa8e" alt=""><figcaption></figcaption></figure>

You can also associate a project with one or more Portfolios & Streams at time of project creation or approval; or when editing a project.

<figure><img src="/files/XE3oNB2ql7TJJCNB4LqP" alt=""><figcaption></figcaption></figure>

## Managing Access to Streams

View access can be given to to non-admin users for individual portfolios or their respective streams. Those users will only see data relevent to projects they have access to on the portfolio and/or stream.

Access can be granted to portfolios and/or streams based on `Groups` or `Users`.

Access can be granted to the entire Portfolio and its related Streams using the option at the top of the `Access and Streams` settings page for the Portfolio.

<figure><img src="/files/NPBqsLWRkhweibgnWrEJ" alt=""><figcaption></figcaption></figure>

Alternatively, access to individual streams can be granted by clicking on any of the streams and configuring the access on that stream.

<figure><img src="/files/4zGtpqFmHSXxhcOCd5Dh" alt=""><figcaption></figcaption></figure>


# Groups

## Overview

Groups can help you to more easily manage the following:&#x20;

* **Users and SSO Groups**
* **Customers and Teams**
* **Access to:**
  * **Portfolios**
  * **Projects, including their Vulnerabilities**
  * **Project Requests**
  * **Assets**
  * **Writeups**
  * **Reports**
  * **Custom Fields**
  * **Self-Service APIs**
  * **Events**
  * **Flows**
  * **Actions**
  * **AI MCP**
  * and more..

> Groups includes Enterprise-level user administration - placing you in control of your data.&#x20;

When applying [various access controls in AttackForge](https://support.attackforge.com/attackforge-enterprise/data-concepts-and-access), you can usually apply the access to:

* Roles
* Groups
* Users

Groups can help you to relate users together, for example a customer, vendor or internal team.

You can then use the Group to make access control easier by applying access at the group level - allowing the group members to inherit access through their group.

<figure><img src="/files/JHUeRBJqIc7piG11NOmQ" alt=""><figcaption></figcaption></figure>

Group membership provides easy to manage access controls for your teams.&#x20;

<figure><img src="/files/nzUT6lXZz4ateL0MmCqS" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/HAT6b8Mi7RJ7q1DM6EJt" alt=""><figcaption></figcaption></figure>

## Creating A Group

You can create a group by clicking on `New` .

<figure><img src="/files/fRBA0JyC0eegKcDb2rvz" alt=""><figcaption></figcaption></figure>

### Auto Add Project Request

You can automatically assign `Groups` to `Project Requests` when they get created by group members.

This means any custom access controls you have created on the group will take effect immediately.

This makes it easy to have dedicated teams of people who can work together to view, edit and approve project requests - without the involvement of Administrators or Project Coordinators.

<figure><img src="/files/IAL3hxY96FO8PvAnitgU" alt=""><figcaption></figcaption></figure>

### Project Team Notifications

This option will allow the group to be in-scope of receiving project email notifications.&#x20;

Whether a user receives the emails is still dependant on how the emails are configured on the project, and the users own personal email notification preferences.

### Manage Group Membership via SSO Groups

You can link `Identity Provider (IDP)` or `Active Directory (AD) Groups` to AttackForge Groups.

This feature is available for Single-Sign-On (SSO) enabled tenants to help automate provisioning and removal of users to AttackForge Groups and their related projects, based on the users' IDP/AD groups.

This feature can help to ensure that users accessing AttackForge receive sufficient access to projects based on the Enterprises' own access control groups, and remove access to projects which they should not have access - upon each login.

This option is Disabled by default. It is only enabled, on an AttackForge Group-by-Group basis, when an IDP/AD group is linked to the AttackForge Group.

> How it works:&#x20;

1. When creating a new group in AttackForge, or when editing an existing group - you can link one or more IDP/AD groups. This is an optional field.
2. Once a group is linked, **all group membership** will be controlled via SSO.&#x20;

If the user signing into AttackForge via SSO has IDP/AD groups returned in their SSO profile, the following checks will apply:

* For each AttackForge Group with linked IDP/AD groups - check to see if any linked groups match any of the users' IDP/AD groups:
  * **If match exists**
    * If the user is not already a member of the group, add the user as a member to the group.
    * Assign the access level according to the mapping.
  * **If no match exists**
    * If the user is already a member of the group - remove their access to the group.

<figure><img src="/files/4rqT4DsuAMiXpF1XttS8" alt=""><figcaption></figcaption></figure>

After you create the group, you can then invite members to the group; assign `Group Membership Administrators` and start linking the group to all related access control systems in AttackForge.

## Group Overview

Group overview provides a single pane of glass view into your projects and vulnerabilities for the group.

You can also view the groups' assets and attack chains, as well as view and manage group membership.

<figure><img src="/files/oXkrjBzwCCdllSxOjl1e" alt=""><figcaption></figcaption></figure>

## Group Membership Admins

You can delegate management of your groups to other users and groups.

This can help you to:

* Empower customers and engineering teams to manage stakeholder access to relevant projects and project requests.
* Delegate security teams to manage groups on a needs basis.
* Reduce the burden on application administrators.

When assigning a user or a group to manage access to the group, you can configure the following:

* **Project Access Level Limit -** Set the highest level of access the user or group is allowed to assign for access to the groups' projects.
* **Project Request Access Level Limit -** Set the highest level of access the user or group is allowed to assign for access to the groups' project requests.
* **Add User Method -** Configure how group members can be added - either by selecting an existing user from a drop-down list, or by entering in an email address.
* **Allow User Invite -** Allow user to invite new persons to your AttackForge tenant.

> Group Membership Admins cannot see any projects, vulnerabilities, assets or attack chains associated with the group. They can only see the Group details, such as owner and primary contact, and also any current members on the group.

You can also allow the group membership admin to invite other users to your AttackForge.

This makes it convenient to get access to the right people, quickly and without the need to involve application administrators.

This can help you to:

* Empower customers to invite their engineering teams directly to the relevant projects or groups.
* Delegate account managers to better manage their customer accounts.
* Share vulnerability information faster, to help remediate vulnerabilities sooner.

<figure><img src="/files/RrlcFCOh5AiOJXgXGvRc" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4pL2bVoxBQdDYbATcSM8" alt=""><figcaption></figcaption></figure>

## Group Membership

Group membership helps to ensure the right people have the right access in a easy and convenient way to manage.&#x20;

You can access group membership page by clicking on `Members`.

<figure><img src="/files/Yx5iLo8kOA3YMgalIweJ" alt=""><figcaption></figcaption></figure>

Here you can manage existing group members and invite a new member to the group.

When adding a new group member, you can do the following:

* Pick the user from a drop-down list, or by entering an email address (depending on your access)
* Invite a new user to AttackForge and make them a group member (depending on your access)
* Assign a `Project Access Level` - None, View, Upload or Edit. This will apply to all projects linked to the group.
* Assign a `Project Request Access Level` - None, View, Edit or Action. This will apply to all project requests linked to the group.

For more information on Project Access Levels, check [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/invite-user-to-project#access-roles).

For more information on Project Request Access Levels, check [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project#project-request-direct-access-controls).

<figure><img src="/files/H2dttUgENu8ZlJLFwe9P" alt=""><figcaption></figcaption></figure>

## Linking Groups

### Projects

You can link groups to Projects.&#x20;

Access to the project will be extended to the group members, based on each members project access level.

<figure><img src="/files/vNogJWTnzFtQWgqtlWkQ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/sjT6HcVQtqTZ2FaHmR8f" alt=""><figcaption></figcaption></figure>

Linking a group to a project will have the following effects:

* Any Group Members will automatically receive access to the project, based on their access level defined in the Group settings.
* Group Members will be able to filter Analytics based on the Group.
* Group Members will see project-related data in their dashboards, including vulnerabilities.

### Project Requests

You can link groups to Project Requests.

Access to the project requests will be extended to the group members, based on each members project access level.

<figure><img src="/files/fcsWSBaWTXjsP656dXOc" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/OukmATeINkjjp2AOU2zA" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/oOLm9fLIJLHoJRfqpWPh" alt=""><figcaption></figcaption></figure>

Linking a group to a project request will have the following effects:

* Any Group Members will automatically receive access to the project request, based on their access level defined in the Group settings.

### Assets

You can link groups to `Asset Libraries`.

This can help to manage users access to Assets, for example:

* Allowing customers to see or modify their own assets
* Allowing technology teams to see assets which belong to them
* Allowing security teams to manage assets under their responsibility

Groups can be assigned to asset libraries within Administration module.

<figure><img src="/files/H86UJ4gjwDt3x7pM7PHj" alt=""><figcaption></figcaption></figure>

### Writeups

You can link groups to `Writeups Libraries`.

This can help to manage users access to Writeups, for example:

* Allowing technical writers to get access to writeups they manage
* Allowing pentesters to view or make changes to writeups they are allowed to see or change
* Allowing security teams to manage which writeups libraries can be accessed by internal and external teams

Groups can be assigned to writeups libraries within Administration module.

<figure><img src="/files/breCaKpAJDJ0NuwykXmt" alt=""><figcaption></figcaption></figure>

### Portfolios

You can link groups to `Portfolios` and `Streams`.

This can help to manage users access to Portfolios and Streams, for example:

* Allowing customers to see dashboards for their entire portfolio
* Allowing customers to see dashboards for specific areas within their portfolios
* Allowing teams to track and manage compliance better

Group members will only see data relevant to projects they have access to on the portfolio and/or stream.

Groups can be assigned to portfolios and streams within the Portfolio settings.

<figure><img src="/files/Q48YFACWjkQVVoSTrRL4" alt=""><figcaption></figcaption></figure>

### Custom Fields

You can link groups to `Custom Fields`.

This can help to manage users access to data points within AttackForge, for example:

* Allowing customers to see particular project and vulnerability data that only relates to them
* Allowing Red Teams and Blue Teams to access but not change each-others data when collaborating on Purple Team assessments
* Allowing security teams to configure and use data on a need-to-know-basis

Group members will only see, or have ability to modify, custom fields relevant to their access settings.

Groups can be assigned to custom fields within the custom field settings.

<figure><img src="/files/CuAhTlP2yPeadJjYUXiU" alt=""><figcaption></figcaption></figure>

### Reports

You can link groups to `Reports`.

This can help to manage users access to reports they can download within AttackForge, for example:

* Allowing customers to see particular reports that only relates to them
* Creating reports for auditors, which only auditors can access
* Allowing security teams to create reports which are only need-to-know for the security team

Group members will only see reports relevant to their access settings.

Groups can be assigned to reports within the `Report Templates` module.

<figure><img src="/files/KjZfW9KW5ccAL0uDl19q" alt=""><figcaption></figcaption></figure>

## Additional Group Access

Groups can be used to additionally provide Group Members with access to the following:

* [Events via Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#internal-events)
* [Events via the Self-Service API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/getting-started)
* [Self-Service RESTful APIs](/app/modules/self-service-restful-api)
* [Flows](/app/modules/flows)
* [Actions](/app/actions)
* [AI MCP](/app/modules/ai-mcp-and-skills)

Administrators can assign access per-group on a needs basis.

Access to the items above can be managed by clicking on `Other Access` from the Group page.

<figure><img src="/files/18Q3WmSETIoEVL8YnpIE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4vIlC5fy1gXZm3COZoya" alt=""><figcaption></figcaption></figure>

### Events (Flows / Self-Service API)

See this link for more details on [access to Events via Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#internal-events).

See this link for more details on [access to Events via the Self-Service API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api/getting-started).

<figure><img src="/files/bB8Lh8l8LT5BNQwByDFR" alt=""><figcaption></figcaption></figure>

### Self Service RESTful API

See this link for more details on [access to Service RESTful APIs](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started).

<figure><img src="/files/tcG688PHmUSrJvYmYn5h" alt=""><figcaption></figcaption></figure>

### Flows and Actions

See this link for more details on [access to Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#getting-access-to-flows).

See this link for more details on [access to Actions](https://support.attackforge.com/attackforge-enterprise/actions#sharing-actions).

<figure><img src="/files/oj29x9z7EMMm1Slt0Ffj" alt=""><figcaption></figcaption></figure>

### AI MCP

See this link for more details on access to [AI MCP](https://support.attackforge.com/attackforge-enterprise/modules/ai-mcp-and-skills#user-access-to-mcp).

<figure><img src="/files/By9jy7sLhH3DmIn2KxAu" alt=""><figcaption></figcaption></figure>


# Attack Chains

## Overview

Attack Chains help demonstrate exactly what an attacker is doing at every step of the way - in a simple and easy to understand visual story.&#x20;

> Attack Chains help clients understand attack paths and focus remediation where it's needed.

Using Attack Chains can provide extra information to help customers and developers prioritize where to remediate first, without relying on traditional risk ratings or scores.&#x20;

Attack Chains can help to identify ways to block attacks from being chained together, and prioritize core issues with least effort & resources.

<figure><img src="/files/Lvnp6CyK3tGhMqSUktkJ" alt=""><figcaption></figcaption></figure>

The Attack Chains module provides access to all attack chains across all projects user has access to.

See [Attack Chains](https://support.attackforge.com/attackforge-enterprise/getting-started/attack-chains) for more information on attack chains.


# Assets

## Overview

Assets module can help you to centralize and maintain all of your assets in one place. Using this module, you can define a new asset, store its properties and metadata, and assign it to projects and groups.

> You can utilize the UI import option or the Self-Service APIs to import assets into this module, for example via CMDB.

This module is optional and can be enabled & disabled via Administration module.

<figure><img src="/files/ClrB1uVO7GtBXNvY4Nwh" alt=""><figcaption></figcaption></figure>

When the Assets module is enabled, the scope sections on projects will reflect the available assets in this module.

<figure><img src="/files/7KQCGH5EcG93IY6sYyUs" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TEbrNijRsZUoRdvlddNh" alt=""><figcaption></figcaption></figure>

## Configuring Asset Fields

You can create custom asset fields from the `Administration` module.

You can take advantage of custom field access controls to have different asset forms and fields for different types of `Roles`, `Groups` or `Users`. This means customers, business units, teams or individuals can have tailored asset fields based on their needs.

<figure><img src="/files/0j7qjyexAb7ajbM3W8Ym" alt=""><figcaption></figcaption></figure>

## Creating Assets

You can create a new asset by clicking on `New Asset` button.

<figure><img src="/files/vQcQg2U8TvrHH7TTmYGL" alt=""><figcaption></figcaption></figure>

## Viewing & Updating Assets

You can view the details for an asset by clicking on its name. Here you can update the asset or view its vulnerabilities.

> **!IMPORTANT:** If you update the name of the asset, it will automatically update the name of the asset on all projects and vulnerabilities which are also linked to/referencing this asset.

<figure><img src="/files/vFDxVjmWIXoGhVsPso1W" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/aLBgtEuqCwjhWP3NLwaQ" alt=""><figcaption></figcaption></figure>

## Importing Assets

You can import assets from the Assets module using the `UI import option` or the `Service Service RESTful API`.

### UI Import

<figure><img src="/files/wLOUDlrHkunVI4tzHgZE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/TZ0hLMWopX0ucJBuodgE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/kn8Nmac3PdCrbzCw1WCP" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/U72ltzrwjpDLnQxT20C7" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Bq819YhM06AizvaOSauR" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/8KDkN85JfVTAOvR78HoF" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/hsoNw8VWjNK0RtRqpGor" alt=""><figcaption></figcaption></figure>

### Self-Service API

<figure><img src="/files/7H0wu1yFUmuRWv7EziDz" alt=""><figcaption></figcaption></figure>

## Grouping and Managing Access to Assets

You can use Asset Libraries to assign your assets into libraries. This makes it easier to manage and group your assets, and to also configure assets for different customers, business units, networks, platforms or teams.

> Asset libraries can be configured from the Administration module.

Assets can belong to one or more libraries, allowing to share access or ownership of assets and to reducing asset duplication.

Asset libraries can have access controls to manage who can `View` or `Edit` the assets.&#x20;

Asset library access controls can be applied to `Roles`, `Groups` or `Users`, and assigned with either View or Edit privileges.

Users with View access to an asset library will be able to see the asset and any vulnerabilities for which that user has access to via their projects.

Users with Edit access to an asset library will be able to make changes to an asset, including ability to archive the asset or link additional asset libraries.

> Uncatalogued assets (assets which do not belong to any libraries) can be restricted by access controls from the Administration module

> !IMPORTANT: Users without Edit access to at least one asset library or uncatalogued assets will not be able to import vulnerabilities on a project

<figure><img src="/files/D5NWpE4hEwTazodpVlER" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/8S0QPK7xzCqypnejCc9i" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/4JTOE0qKZMRaGkbrj5WT" alt=""><figcaption></figcaption></figure>

Bulk actions can be used to bulk assign assets to libraries.

<figure><img src="/files/pVQaA47LEkb5akv6KJFv" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/sQt0oTOoxMbMFxNRjDd4" alt=""><figcaption></figcaption></figure>

## Archiving Assets

You can archive assets. If you accidentally archive an asset, you can always restore it from the `Archived Assets` section.

> **!IMPORTANT:** If you archive an asset from the library - it will NOT delete the asset from a project, or any vulnerabilities linked to/referencing that asset.

## Importing Vulnerabilities on a Project

When importing vulnerabilities on a project - the application will check to see if the affected asset for the vulnerability already exists in the library. If it does not exist - it will automatically create the new asset in the library for you.


# Writeups

## Overview

AttackForge utilizes centralized vulnerability writeup libraries which makes it fast & easy to create new vulnerabilities on your projects.

> Writeups act as a knowledge-base to make creation to vulnerabilities quick and easy.&#x20;

Writeups are linked to vulnerabilities. They are not copied to vulnerabilities. This is important as it means every vulnerability has a direct relationship to its writeup. This is useful to:

* Ensure changes to a writeup (improved recommendations, spelling mistakes, etc.) gets propagated to all linked vulnerabilities.
* Speed up the quality review process during a pentest, in conjunction with access controls.
* Be able to properly determine unique vulnerabilities.

<figure><img src="/files/g17KIJ6lm0hT8G0efDgV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/43nYBEZhcIDdj3j71X6N" alt=""><figcaption></figcaption></figure>

Every writeup acts as a template, which can then be re-used on projects. This means you only need to write the issue once - saving you hours of reporting time for every single project.

The `Writeups Libraries` are separated into four (4) areas:

## **Main Library**

* This is the primary source of your vulnerability writeups.
* It contains 1500+ pre-loaded writeups from [MITRE CWE](https://cwe.mitre.org/) and [MITRE CAPEC](https://capec.mitre.org/) that come with AttackForge. You can also add your own.
* This library is shared by default, which means any user on a project with permissions to create vulnerabilities for the project, will be able to use any of the writeups in this library.
* Access to this library can be restricted to certain roles, users or groups via Administration module.

## **Imported Library**

* This is where you can find all of the writeups you have imported from various tools & scanners.&#x20;
* This library is shared by default, which means any user on a project with permissions to create vulnerabilities for the project, will be able to use any of the writeups in this library.
* Access to this library can be restricted to certain roles, users or groups via Administration module.

## **Project Library**

* This is where you can access writeups that are designated to specific projects.
* This is useful if you have project-specific or sensitive data which you would like to seperate from the Main and Imported libraries which are shared libraries.
* Only users with access to the project will be able to view and use writeups belonging to the project.
* Access to this library can be restricted to certain roles, users or groups via Administration module.

## **Custom Libraries**

* You can create custom libraries to suite your own needs. For example, you can create libraries for specific types of tests, or libraries for individual teams or testers, or libraries for particular technologies.&#x20;
* Access to this library can be restricted to certain roles, users or groups via Administration module.

For details on access controls to the Writeups libraries - see [Access Control Matrix](https://support.attackforge.com/attackforge-enterprise/access-control-matrix)

> !IMPORTANT: Users without Edit access to any Writeups libraries will not be able to import vulnerabilities on a project.

<figure><img src="/files/22kMcvXK0VbAfXLjOWrG" alt=""><figcaption></figcaption></figure>

## Creating a Writeup&#x20;

To create a new writeup, click on `New` button.&#x20;

Selecting which library you wish to assign the writeup to - `Main`*,* `Imported`*,* `Project` *or* `Custom`.

<figure><img src="/files/JHzYkUIFKx37izVW2NFL" alt=""><figcaption></figcaption></figure>

If you select `Imported` - you will be required to enter additional information regarding to the source/tool where the writeup is being imported from.

If you select `Project` - you will be required to link the vulnerability to at least one (1) project. Only users with access to the linked project(s) will be able to use this writeup when creating a new vulnerability on the linked projects.

After you have selected the library, you can proceed to enter in the information relating to the writeup. You can also search for an existing writeup, which will pre-fill the fields for you to make it faster.

<figure><img src="/files/h6ZhLUh2pVUPPST7b1bE" alt=""><figcaption></figcaption></figure>

You can create a placeholder `CVSS` score which will be loaded when a user selects this writeup when creating a vulnerability on their project. This makes it easier to set a default score for vulnerabilities.

<figure><img src="/files/CJ990wc8xcm4jUxBOP03" alt=""><figcaption></figcaption></figure>

You can add tags and custom tags. This is useful if you are using sections within your custom reports, or exporting custom tags for vulnerabilities into your tools. You can enter any name and value for the custom tags.

<figure><img src="/files/nSrdUuFRukHy6UnEhEIe" alt=""><figcaption></figcaption></figure>

Custom fields may be also be configured for your writeups. See [this link](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms#vulnerability-custom-fields) for more details.

<figure><img src="/files/dEvOA6PQyLJAHuS635Wt" alt=""><figcaption></figcaption></figure>

## Integrations

### SecDim Sandboxes

We're excited to be the first Pentest Management Platform to release a Secure Code Learning collaboration with [SecDim - Dev-Native Attack & Defence Wargames](https://secdim.com/).

With the power 💪 of SecDim and AttackForge, you can:

* Explore a real vulnerability in a cloud native app. Debug and verify if your security patch can remediate the vulnerability
* Train developers on how to fix vulnerabilities identified in their applications, during a pentest
* Collaborate between engineers and security teams on how to best remediate vulnerabilities
* Improve retesting pass rates for discovered vulnerabilities
* Test your knowledge on how to fix common vulnerabilities and measure your effectiveness

Every Sandbox comes with a security test suite to simulate the exploitation of the vulnerability.

Sandboxes are integrated with `git` so you can save your progress and pick it up again where you left off.

SecDim's catalogue is extensive, covering everything from AI, GraphQL, React, Kubernetes, to Web3. You can test your skills against modern security vulnerabilities inspired by real-world issues.

> Each sandbox is deployed in a secure isolated Cloud Development Environment directly in your browser. You can debug, patch and test your code as if you were building an app.

{% embed url="<https://youtu.be/FycgBdGKA5w>" %}

Start by learning more about this integration, and when ready - switch the integration on.

<figure><img src="/files/MF8knCNKswA1osXgVDMA" alt=""><figcaption></figcaption></figure>

Search SecDim's catalogue of vulnerable sandboxes which you can link to your Writeups.

{% embed url="<https://youtu.be/E8X9017GXI8>" %}

When you create a vulnerability on a project, users will be able to see the linked SecDim sandboxes and launch a sandbox to get started.

{% embed url="<https://youtu.be/mnsjLeV_gRc>" %}

## Updating Writeups

You can update or modify the writeups at any time by clicking on `Edit` button for the writeup you wish to update.

> **!IMPORTANT:** updates to write-ups will affect all linked vulnerabilities on all linked projects.

## Custom Libraries

You can create custom libraries to suite your own needs. For example, you can create libraries for specific types of tests, or libraries for individual teams or testers, or libraries for particular technologies.&#x20;

Custom Libraries can be configured from the Administration module.

<figure><img src="/files/TOMjjFFJG3G1HCK3IlY6" alt=""><figcaption></figcaption></figure>

## My Writeups

These are all of the Writeups which you have created.

<figure><img src="/files/BpraEXDCjLmwsLDXyrXh" alt=""><figcaption></figcaption></figure>

## All Writeups

This is where you can see all writeups you have access to.

<figure><img src="/files/muK8rzNNWzTJAUKSdWYS" alt=""><figcaption></figcaption></figure>

## Archived Writeups

This is where you can see all archived writeups in AttackForge. It is restricted to Admin users only.

You can archive a writeup by using the actions menu and selecting `Archive`. Any writeups you archive from the library will be relocated to the Archived Writeups section and will no longer be selectable on projects for new vulnerabilities or changes to existing vulnerabilities.&#x20;

However, archived writeups will still be referenced on existing projects - so that the integrity of vulnerabilities on a project remains in-tact.

Admins can restore writeups if required to do so.

<figure><img src="/files/1WeGQJ5W3m3BbXPVM3fT" alt=""><figcaption></figcaption></figure>

## Moving Writeups Between Libraries

You can move writeups between libraries by first selecting the writeups, then clicking on `Reassign` from the actions menu.

<figure><img src="/files/5A4K4tguzL5AXPgKMBnH" alt=""><figcaption></figcaption></figure>

Select the option for the library you would like to assign the vulnerabilities to.

<figure><img src="/files/Xo9De1dsQM9qXSQck1Ke" alt=""><figcaption></figcaption></figure>

## Importing Writeups

You can import additional vulnerability knowledge-bases that AttackForge team has prepared and made available on our GitHub: <https://github.com/AttackForge/Writeups>

### MITRE

* [MITRE CWE](https://github.com/AttackForge/Writeups/tree/main/MITRE/CWE/v4.13)
* [MITRE CAPEC](https://github.com/AttackForge/Writeups/tree/main/MITRE/CAPEC/v3.9)

<figure><img src="/files/G8e8XaotjKbTZsZunBHN" alt=""><figcaption></figcaption></figure>

Start by clicking on `New -> Import Writeups`.

<figure><img src="/files/YsbWm4ESoihHGotRu0Bm" alt=""><figcaption></figcaption></figure>

Select a import source:

* **AttackForge Community** - you can export your writeups in AttackForge Community, and import them into your AttackForge Core/Enterprise.
* **JSON** - generic JSON import option. Includes a template file to help with preparing your data file for import.
* **CSV** - generic CSV import option. Includes a template file to help with preparing your data file for import.

<figure><img src="/files/1YrV0LKIEzwsoijdqRCM" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/T4mqj75he8vAvBNnxHIH" alt=""><figcaption></figcaption></figure>

Select the writeups you would like to import.

<figure><img src="/files/BiRvULRh19SUJYrOLLIW" alt=""><figcaption></figcaption></figure>

You can make changes to the writeups prior to import.

<figure><img src="/files/SMfZciYhtZDWXcUjwbVi" alt=""><figcaption></figcaption></figure>

Select which library you would like to import the writeups into.

<figure><img src="/files/OurL5C0vHECfTdHT9tpP" alt=""><figcaption></figcaption></figure>

Receive updates on import progress.

<figure><img src="/files/0tssg0DZ9TbPoSS2SUJK" alt=""><figcaption></figcaption></figure>


# Test Suites

## Overview

Test Suites module is where you can create custom methodologies, checklists and service catalogues for your customers to pick from when requesting a project; or for you to assign to any new projects you create.

When a `Test Suite` is assigned to a project, the linked `Test Cases` will also be assigned to the project - so that the pentester or auditor has a checklist to work from.

<figure><img src="/files/gYNCgw5r0faJBwvoOozk" alt=""><figcaption></figcaption></figure>

A test suite helps:

* Clients understand exactly what was tested on the project
* Developers/Engineers link test cases to vulnerabilities
* Pentesters structure their testing in a methodical, consistent & standardized way
* Organizations create repeatable, standardized & comparable assessments - independent of who was actually performing the assessment

> Test cases can provide valuable insight into a penetration test or audit.&#x20;

Test cases demonstrate:

* What was tested
* How was it tested
* When was it tested
* Who tested it
* What was the outcome
* What is the supporting evidence

See [Test Cases](https://support.attackforge.com/attackforge-enterprise/getting-started/test-cases) for more information on how test cases are used on projects.

## My Test Suites

AttackForge comes pre-loaded with dozens of industry methodologies that you can select from, for any given project. The methodologies are gathered from [OWASP](https://owasp.org/www-project-application-security-verification-standard/), [MITRE ATT\&CK](https://attack.mitre.org/), [OSSTMM](https://www.isecom.org/research.html), [NIST](https://www.nist.gov/) and others.

Any new test suites that you create will show in `My Test Suites`. You can use the actions menu to Edit, Duplicate, Reorder or Delete any of these entries.

You can view the test cases linked to the test suites by clicking on the test suite name.

All test suites are shared and common. This means any entries you create can be used by your peers - pooling together your knowledge to save time & effort.&#x20;

<figure><img src="/files/aPhx0liEi1lba07OtaKI" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/IrPyexXkYqPZXKuXZru7" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/0UuIt0kO0Mpq69EDWIwF" alt=""><figcaption></figcaption></figure>

## Creating A New Test Suite

To create a new test suite in the library, click on `New` button.&#x20;

Once the form is submitted, the test suite will be immediately available to start assigning test cases.

<figure><img src="/files/xr5VHae89TA5hoWvSOHS" alt=""><figcaption></figcaption></figure>

To add a new test case, click on the test suite, then click on the `Test Cases` tab, then click on `Add Test Case`.

<figure><img src="/files/m8x2IIYUg1pCVnbCev0b" alt=""><figcaption></figcaption></figure>

You can also search the library for an existing test case, this will pre-fill the fields for you to save you time & effort when you only want to make small changes to an existing test case.

The `Code` field is used to help with sorting & ordering test cases when displayed in projects & reports.

Code will appear before the details of the test case. For example: `WEB-APP-001 Test for X, Y & Z`; `WEB-APP-002 Test for A, B & C`; etc.

You can update or modify the test cases at any time by using the actions menu.

You can also update or modify the test suite at any time by using the page menu.

> **!IMPORTANT:** updates to test cases in your library will apply globally to all projects which are referencing that test case.

> **!IMPORTANT:** any test cases you add or delete on a test suite **will not** apply retrospectively to existing projects. This is to preserve integrity of what was actually assigned & tested on projects, and avoid situation where a project may be Completed and is now Not Completed as new test cases are assigned.

You can create `Custom Fields` on your Test Cases from the Administration module.

<figure><img src="/files/qlwPNwRIBMyg8495Mdlz" alt=""><figcaption></figcaption></figure>

## Importing Test Cases

You can import additional methodologies that AttackForge team has prepared and made available on our GitHub: <https://github.com/AttackForge/TestSuites>

### Red Teaming

* [MITRE ATT\&CK Enterprise Version 16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ENTERPRISE/mitre_attack_enterprise_16_1.json) - see [recommended mapping](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder#import-mappings)
* [MITRE ATT\&CK Mobile Version 16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/MOBILE/mitre_attack_mobile_16_1.json) - see [recommended mapping](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder#import-mappings)
* [MITRE ATT\&CK ICS Version 16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ICS/mitre_attack_ics_16_1.json) - see [recommended mapping](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder#import-mappings)
* [OSSTMM Version 3 - Human Security Testing](https://github.com/AttackForge/TestSuites/blob/main/OSSTMM/v3/Chapter%207/osstmm_human_security_testing.json)
* [OSSTMM Version 3 - Physical Security Testing](https://github.com/AttackForge/TestSuites/blob/main/OSSTMM/v3/Chapter%208/osstmm_physical_security_testing.json)

### Artifical Intelligence (AI)

* [MITRE ATLAS Version 4.8.0](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATLAS/mitre_atlas_4.8.0_testcases.json)

### Desktop Applications

* [OWASP Desktop App Security Top 10 2021](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-Desktop-App-Security-Top-10-2021.json)

### Operational Technology (OT)

* [OWASP Operational Technology (OT) Top 10 2025](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-Operational-Technology-\(OT\)-Top-10-2025.json)

### Web Application & API

* [OWASP Web Security Testing Guide Version 4.2](https://github.com/AttackForge/TestSuites/blob/main/OWASP/WSTG/v4.2/owasp_wstg.json)
* [OWASP Application Security Verification Standard (ASVS) Version 4 - Level 1](https://github.com/AttackForge/TestSuites/blob/main/OWASP/ASVS/v4/Level%201/owasp_asvs_level_1.json)
* [OWASP Application Security Verification Standard (ASVS) Version 4 - Level 2](https://github.com/AttackForge/TestSuites/blob/main/OWASP/ASVS/v4/Level%202/owasp_asvs_level_2.json)
* [OWASP Application Security Verification Standard (ASVS) Version 4 - Level 3](https://github.com/AttackForge/TestSuites/blob/main/OWASP/ASVS/v4/Level%203/owasp_asvs_level_3.json)
* [OWASP Web Application Security Top 10 2021](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-Web-Application-Top-10-2021.json)
* [OWASP API Security Top 10 2023](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-API-Security-Top-10-2023.json)

### Mobile Application

* [OWASP Mobile Application Security Testing Guide (MASTG) Version 2 2025](https://github.com/AttackForge/TestSuites/blob/main/OWASP/MASTG/v2/OWASP-Mobile-Application-Security-Testing-Guide-\(MASTG\)-Version-2-2025)
* [OWASP Mobile Top 10 2024](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-Mobile-Top-10-2024.json)
* [MITRE ATT\&CK Mobile Version 16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/MOBILE/mitre_attack_mobile_16_1.json) - see [recommended mapping](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder#import-mappings)

### Network Infrastructure, Hardware and IOT

* [OSSTMM Version 3 - Telecommunications Security Testing](https://github.com/AttackForge/TestSuites/blob/main/OSSTMM/v3/Chapter%2010/osstmm_telecommunications_security_testing.json)
* [OSSTMM Version 3 - Data Networks Security Testing](https://github.com/AttackForge/TestSuites/blob/main/OSSTMM/v3/Chapter%2011/osstmm_data_networks_security_testing.json)
* [OSSTMM Version 3 - Wireless Security Testing](https://github.com/AttackForge/TestSuites/blob/main/OSSTMM/v3/Chapter%209/osstmm_wireless_security_testing.json)
* [MITRE ATT\&CK ICS Version 16.1](https://github.com/AttackForge/TestSuites/blob/main/MITRE/ATT%26CK/ICS/mitre_attack_ics_16_1.json) - see [recommended mapping](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder#import-mappings)

### Cloud Configuration

* [CIS Amazon Web Services Foundation v1.2.0](https://github.com/AttackForge/TestSuites/blob/main/AWS/CIS-Amazon-Web-Services-Foundation-v1.2.0.json)
* [CIS Microsoft Azure Foundation v1.2.0](https://github.com/AttackForge/TestSuites/blob/main/AZURE/CIS-Microsoft-Azure-Foundation-v1.2.0.json)
* [CIS Google Cloud Platform Foundation v1.1.0](https://github.com/AttackForge/TestSuites/blob/main/GCP/CIS-Google-Cloud-Platform-Foundation-v1.1.0.json)
* [Oracle Cloud Infrastructure](https://github.com/AttackForge/TestSuites/blob/main/OCI/Oracle-Cloud-Infrastructure.json)
* [Kubernetes Infrastructure](https://github.com/AttackForge/TestSuites/blob/main/KUBERNETES/Kubernetes-Infrastructure.json)

### CI/CD

* [OWASP Top 10 CI/CD Security Risks 2023](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-Top-10-CI-CD-Security-Risks-2023.json)

### Low Code/No Code

* [OWASP Low Code/No Code Top 10 2024](https://github.com/AttackForge/TestSuites/blob/main/OWASP/Top%2010/OWASP-Low-Code-No-Code-Top-10-2024.json)

<figure><img src="/files/QfkXf2U3MNiNwaVfUEU2" alt=""><figcaption></figcaption></figure>

Start by clicking on `New -> Import Test Cases`.

<figure><img src="/files/q3cJL7tZcJDQQkLsc4zt" alt=""><figcaption></figcaption></figure>

Select a import source:

* **AttackForge Community** - you can export your test cases in AttackForge Community, and import them into your AttackForge Core/Enterprise.
* **JSON** - generic JSON import option. Includes a template file to help with preparing your data file for import.
* **CSV** - generic CSV import option. Includes a template file to help with preparing your data file for import.

<figure><img src="/files/1L5MyFob2jDiS9QSrM4f" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6uzqDy4kK1DSkjizpzRn" alt=""><figcaption></figcaption></figure>

Select the test cases you would like to import.

<figure><img src="/files/6bnv5vsTZQUJgjkY2aPY" alt=""><figcaption></figcaption></figure>

You can make changes to the test cases prior to import.

<figure><img src="/files/qZqYeoYliU8wtcu1ujAm" alt=""><figcaption></figcaption></figure>

Receive updates on import progress.

<figure><img src="/files/2sm7bdoNM6Hrg8k0iWMY" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/0M2i90mHa6FivefOR1Fd" alt=""><figcaption></figcaption></figure>

## Import Mappings

We recommend setting the following [Test Case Custom Fields](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms) when importing test cases from the [AttackForge built testing methodologies](https://github.com/AttackForge/TestSuites).

### MITRE ATT\&CK Enterprise, Mobile and ICS

* Key - **x\_mitre\_attack\_spec\_version**
  * Field Type - ***Input***
* Key - **x\_mitre\_is\_subtechnique**
  * Field Type - ***Input***
* Key - **x\_mitre\_permissions\_required**
  * Field Type - ***List***
* Key - **kill\_chain\_phases**
  * Field Type - ***Table***
  * Columns:
    * Key - **kill\_chain\_name**
      * Field Type - ***Input***
    * Key - **phase\_name**
      * Field Type - ***Input***
* Key - **x\_mitre\_platforms**
  * Field Type - ***List***
* Key - **x\_mitre\_data\_sources**
  * Field Type - ***List***
* Key - **external\_references**
  * Field Type - ***Table***
  * Columns:
    * Key - **source\_name**
      * Field Type - ***Input***
    * Key - **description**
      * Field Type - ***Input***
    * Key - **url**
      * Field Type - ***Input***
    * Key - **external\_id**
      * Field Type - ***Input***
* Key - **mitre\_domain**
  * Field Type - ***Input***
* Key - **mitre\_tactic**
  * Field Type - ***List***
* Key - **mitigations**
  * Field Type - ***Table***
  * Columns:
    * Key - **mitigation**
      * Field Type - ***Input***
    * Key - **description**
      * Field Type - ***Input***
* Key - **detections**
  * Field Type - ***Table***
  * Columns:
    * Key - **data\_source**
      * Field Type - ***Input***
    * Key - **data\_component**
      * Field Type - ***Input***
    * Key - **detects**
      * Field Type - ***Input***
* Key - **x\_mitre\_defense\_bypassed**
  * Field Type - ***List***

## Execution Flows

Execution flows can be assigned to each test case.

Execution flows can have many uses such as:

* Documenting steps and procedures guiding a person in how to perform the test case
* Documenting which tools should be used to perform the test case
* Documenting internal processes and procedures required by the test case
* Links to external resources

You can add execution flows to any test case when creating or updating the test case.

<figure><img src="/files/2sfZfxHqIS8DyUHp4HHR" alt=""><figcaption></figcaption></figure>

## Abuse Cases

Abuse cases are project-specific test cases. They are unique test cases which apply to the project. For example, consider a web application pentest for a reverse auction website. Typically the pentest may cover the standard OWASP ASVS test cases, however the customer also requires that business logic tests are performed against the bidding functionality to determine whether it can be cheated or not. Abuse cases can be created to specifically test this functionality and provide higher level of assurance beyond standard test cases.

To create abuse cases on the project, you must be either an Administrator or Project Coordinator.

From the project test cases section, click on `Add -> Abuse Case`.&#x20;

<figure><img src="/files/XtUfEaWLowbMLi1XS8o0" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/10QtvvYyGEQDae3985hM" alt=""><figcaption></figcaption></figure>

Abuse Cases are stored & tracked per project in the Test Suites module under the `Project Abuse Cases` section.

<figure><img src="/files/tbbGuUYfTAymcvvwiLtb" alt=""><figcaption></figcaption></figure>

You can delete Abuse Cases directly from the project.

## Archived Test Suites

You can access any archived test suites by clicking on the `Archived Test Suites` button. Here you can view and restore any test suites if desired.

Any test suites you archive from the library will no longer be available for projects or project requests. However, any historical project using the test suite will not be affected so that integrity of test cases on a project remains in-tact.

<figure><img src="/files/fvVWJiSCa3KPBqZM5m2K" alt=""><figcaption></figcaption></figure>


# Report Templates


# Overview

## Overview

AttackForge has a custom built reporting engine we call `ReportGen` which helps you create custom DOCX reports, on-demand and in a variety of templates.

For example - you can create:

* **Pentest reports**
* **Retesting reports**
* **Executive reports**
* **Testing summary reports**
* **Application and Infrastructure reports**
* **Mobile testing reports**
* **Red Team and Purple Team reports**
* **Configuration Audit reports**
* **Compliance reports, to name a few.**

Custom reports can be generated by any user, on demand, from the project dashboard or reporting section on their projects.

Each user can only see reporting options which have been made available to them.

> You can create and upload as many templates as you desire. There is no limitations on how many reports you can make available on your projects.

<figure><img src="/files/Tm0XO7nFbXEH9SoT64Rb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Yx26b26OFTgXsgPrRe0N" alt=""><figcaption></figcaption></figure>

You can upload a new template by clicking on `Add Template`.

Every report template can be configured with access controls to restrict visibility and usage of templates for `Roles, Groups and Users`.

<figure><img src="/files/3qPGlpvSWkONG1NAyLIs" alt=""><figcaption></figcaption></figure>

You can create a report for a project(s) directly from here by clicking on `Create Report` button for your desired template.

<figure><img src="/files/MevUnJKjn0lNwGzdbPjT" alt=""><figcaption></figcaption></figure>

## Getting Started

> UPDATED: Please head over to our new GitHub Support Site for getting started, help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

### Offline Browser Tool

The offline browser tool is useful to help you build and debug your templates. Once your template is ready, you can upload it back into AttackForge and start using it right away.

<figure><img src="/files/MoXpfjlnsRfj7RkBufGV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/hmSeqtDz7TssQiT9oqPf" alt=""><figcaption></figcaption></figure>

### CLI Tool

The AttackForge ReportGen CLI provides users with a command-line tool suitable for creating reports in a programmatic way.

<figure><img src="/files/5OScdtfQuPe2Azd6KsOX" alt=""><figcaption></figcaption></figure>

### Library Package

The AttackForge ReportGen Library Package provides users with a ReportGen library which can be imported into your own code and scripts for creating reports in a programmatic way.

<figure><img src="/files/9waC8MnSrJJBhs7p4O1v" alt=""><figcaption></figcaption></figure>


# Tutorial

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

{% embed url="<https://youtu.be/W3nKWOQBJGU>" %}


# Tips & Tricks

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>


# Troubleshooting

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>


# Template - Report Templates

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

## Pentest Report&#x20;

### [Template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-report-v3-4-template.docx)

### [Example Report](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-report-v3-4-example.docx)

### [Test Data](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-report-test-data.json)

## Red Team Report

### [Template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-red-team-report-v3-0-template.docx)

### [Example Report](https://attackforge.com/attackforge-core-enterprise/report-templates/af-red-team-report-v3-0-example.docx)

### [Test Data](https://attackforge.com/attackforge-core-enterprise/report-templates/af-red-team-report-test-data.json)

## Exec Summary PowerPoint-Style Slide Deck

### [Template](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-exec-summary-slide-deck-template.docx)

### [Example Report](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-exec-summary-slide-deck-example.pdf)

### [Test Data](https://attackforge.com/attackforge-core-enterprise/report-templates/af-pentest-exec-summary-slide-deck-test-data.json)


# Template - Tags

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

> **!IMPORTANT: The tags listed below are NOT an exhaustive list of all available tags. For up to date list, we recommend using the $help function - see** [**https://github.com/AttackForge/ReportGen/issues/18**](https://github.com/AttackForge/ReportGen/issues/18)**.**

## **Accessing Parents**

To access parent objects - please visit [Tips & Tricks](https://support.attackforge.com/attackforge-enterprise/modules/reporting/tips-and-tricks#accessing-parents)

## **Tags for Individual Reports**

* {**@pageBreak**} - adds a page break.
* {**data**} - provides access to the root data file.
* {**#projectCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
* {**#projectCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
* {**projectName**} - name of the project
* {**projectCode**} - project code
* {**projectGroups**} - details for each linked Group
  * {**name**} - name of the group
* {**timestamp**} - timestamp for when JSON report was downloaded
* {**#statusUpdates**} - details for each project status update e.g. when project goes on-hold or off-hold
  * {**status**} - e.g. 'On-Hold' or 'Off-Hold'
  * {**note**} - reason why project was on-hold or off-hold
  * {**created**} - timestamp when project went on-hold or off-hold
* {**totalUniqueVulnerabilities**} - total unique vulnerabilities on the project
* {**totalCriticalVulns**} - total unique critical vulnerabilities on the project
* {**totalHighVulns**} - total unique high vulnerabilities on the project
* {**totalMediumVulns**} - total unique medium vulnerabilities on the project
* {**totalLowVulns**} - total unique low vulnerabilities on the project
* {**totalInfoVulns**} - total unique informational vulnerabilities on the project
* {**totalZeroDayVulns**} - total unique zero-day vulnerabilities on the project
* {**totalEasilyExploitableVulns**} - total unique easily exploitable vulnerabilities on the project
* {**totalTestcases**} - total test cases assigned to the project
* {**totalCompleted**} - total completed test cases on the project
* {**totalInProgress**} - total in-progress test cases on the project
* {**totalNotTested**} - total not-tested test cases on the project
* {**totalNotApplicable**} - total not applicable test cases on the project
* {**#execSummaryNotesHeading**} - set a custom heading for the exec summary, auto disable if no exec summary on project
* {**#execSummaryNotes**} - executive summary notes on the project
  * {**execSummaryNotes**} - exec summary notes
  * {**%inlineScreenshot**} - display exec summary screenshots
  * {**caption**} - text caption for the image
* {**@execSummaryNotesStyled**} - executive summary notes on the project styled based on in-app WYSIWYG editor with inline images
* {**startDate**} - test window start date for the project
* {**progress**} - percentage of test cases actioned on the project
* {**endDate**} - test window start date for the project
* {**projectDuration**} - project duration in days i.e. difference between start & end dates
* {**totalVulns**} - total vulnerabilities across all assets on the project
* {**totalCriticalVulnsAllAssets**} - total critical vulnerabilities across all assets on the project
* {**totalHighVulnsAllAssets**} - total high vulnerabilities across all assets on the project
* {**totalMediumVulnsAllAssets**} - total medium vulnerabilities across all assets on the project
* {**totalLowVulnsAllAssets**} - total low vulnerabilities across all assets on the project
* {**totalInfoVulnsAllAssets**} - total informational vulnerabilities across all assets on the project
* {**totalFixedVulns**} - total fixed/closed vulnerabilities across all assets on the project
* {**totalRetestingVulns**} - total vulnerabilities flagged as retesting across all assets on the project
* {**totalNotFixedVulns**} - total not fixed/open vulnerabilities across all assets on the project
* {**#assets**} - list of all assets on the project
  * {**.**} - name of each asset
* {**#assetsDetails**} - details for all assets (if using Assets module)
  * {**id**} - id for the asset on the project
  * {**name**} - name of the asset on the project
  * {**library\_id**} - id for the asset in the library
  * {**library\_created**} - created timestamp for asset in the library
  * {**library\_updated**} - updated timestamp for asset in the library
  * {**library\_name**} - name of the asset in the library
  * {**type**} - type of asset
  * {**external\_id**} - external id for the asset
  * {**details**} - details for the asset
  * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
* {**#projectTeam**} - list of all project team members
  * {**.**} - name of each project team member
* {**#retestingHistory**} - list of all rounds of retesting requested & completed on the project
  * {**retesting\_round**} - e.g. 1, 2, 3, etc.
  * {**retesting\_round\_status**} - whether the retest round was Requested or Completed
  * {**retesting\_custom\_round\_name**} - custom round name (optional)
  * {**retesting\_custom\_status\_name**} - custom status name (optional)
  * {**retesting\_round\_actioned\_by**} - name of person who requested or completed the round of retesting
  * {**created**} - date when round of retest was requested or completed
  * {**#vulnerabilities**} - list of all vulnerabilities requested / completed on the round of retesting
    * {**vulnerability**} - name of the vulnerability
    * {**vulnerability\_alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
    * {**#vulnerability\_details**}
      * {**#vulnerabilityCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within ReportGen Reports**](https://support.attackforge.com/attackforge.com/modules/reportgen#creating-custom-fields-within-reportgen-reports)
      * {**#vulnerabilityCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**title**} - title of the vulnerability
      * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
      * {**remediation\_status**} - either Open or Closed. Only Closed if all affected assets are also Closed.
      * {**description**} - description of the vulnerability
      * {**@description\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
      * {**attack\_scenario**} - attack scenario for the vulnerability
      * {**@attack\_scenario\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
      * {**remediation\_recommendation**} - remediation recommendation for the vulnerability
      * {**@remediation\_recommendation\_styled**} - remediation recommendation for the vulnerability styled based on in-app WYSIWYG editor
      * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
      * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
      * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
      * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
      * {**testcases**} - list of all the linked test cases to the vulnerability
      * {**#tags**} - list of all tags
        * {**.**} - tag
      * {**#affected\_asset**} - details for the affected asset - see {#assetVulnerabilityMapping}  - {asset}
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
        * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {**.**} - asset name
        * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
          * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
          * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
          * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
          * {**asset**} - asset name
          * {**#assets**} - details for grouped assets on the vulnerability
            * {**name**} - asset name
            * {**actioned**} - whether asset has been actioned or not
            * {**#components**} - affected components on the asset
              * {**name**} - component name
              * {**#notes**} - notes on the component
                * {**.**} - note
              * {**#tags**} - tags on the component
                * {**.**} - tag
            * {**#notes**} - notes on the asset
              * {**.**} - note
            * {**#tags**} - tags on the asset
              * {**.**} - tag
          * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
          * {**#remediation\_notes**} - list of all remediation notes for this affected asset
            * {**created**} - date stamp when remediation note was created
            * {**note**} - remediation note details
          * {**#notes**} - list of all notes for this affected asset
            * {**note**} - note details
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
            * {**text**} - proof of concept / steps to reproduce
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
          * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
          * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
          * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
          * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
          * {**#tags**} - list of all tags
            * {**.**} - tag
      * {**#affected\_assets**} - list of all affected assets for this vulnerability
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * **{#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
        * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {**.**} - asset name
        * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
          * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
          * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
          * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
          * {**asset**} - asset name
          * {**#assets**} - details for grouped assets on the vulnerability
            * {**name**} - asset name
            * {**actioned**} - whether asset has been actioned or not
            * {**#components**} - affected components on the asset
              * {**name**} - component name
              * {**#notes**} - notes on the component
                * {**.**} - note
              * {**#tags**} - tags on the component
                * {**.**} - tag
            * {**#notes**} - notes on the asset
              * {**.**} - note
            * {**#tags**} - tags on the asset
              * {**.**} - tag
          * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
          * {**#remediation\_notes**} - list of all remediation notes for this affected asset
            * {**created**} - date stamp when remediation note was created
            * {**note**} - remediation note details
          * {**#notes**} - list of all notes for this affected asset
            * {**note**} - note details
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
            * {**text**} - proof of concept / steps to reproduce
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
          * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
          * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
          * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
          * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
          * {**#tags**} - list of all tags
            * {**.**} - tag
      * {**#evidence**} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
        * {**%fileBase64**} - display image (if evidence type is of image format)
        * {**fileName**} - name of the file uploaded
        * {**caption**} - caption for the file (optional)
  * {**#vulnerabilitiesNotTested**} - list of all vulnerabilities not retested on the round of retesting
    * {**vulnerability**} - name of the vulnerability
    * {**vulnerability\_alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
    * **{#vulnerability\_details}**
      * {**#vulnerabilityCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within ReportGen Reports**](https://support.attackforge.com/attackforge.com/modules/reportgen#creating-custom-fields-within-reportgen-reports)
      * {**#vulnerabilityCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**title**} - title of the vulnerability
      * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
      * {**remediation\_status**} - either Open or Closed. Only Closed if all affected assets are also Closed.
      * {**description**} - description of the vulnerability&#x20;
      * {**@description\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
      * {**attack\_scenario**} - attack scenario for the vulnerability
      * {**@attack\_scenario\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
      * {**remediation\_recommendation**} - remediation recommendation for the vulnerability
      * {**@remediation\_recommendation\_styled**} - remediation recommendation for the vulnerability styled based on in-app WYSIWYG editor
      * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
      * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
      * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
      * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
      * {**testcases**} - list of all the linked test cases to the vulnerability
      * {**#tags**} - list of all tags
        * {**.**} - tag
      * {**#affected\_asset**} - details for the affected asset - see {#assetVulnerabilityMapping}  - {asset}
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
        * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {**.**} - asset name
        * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
          * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
          * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
          * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
          * {**asset**} - asset name
          * {**#assets**} - details for grouped assets on the vulnerability
            * {**name**} - asset name
            * {**actioned**} - whether asset has been actioned or not
            * {**#components**} - affected components on the asset
              * {**name**} - component name
              * {**#notes**} - notes on the component
                * {**.**} - note
              * {**#tags**} - tags on the component
                * {**.**} - tag
            * {**#notes**} - notes on the asset
              * {**.**} - note
            * {**#tags**} - tags on the asset
              * {**.**} - tag
          * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
          * {**#remediation\_notes**} - list of all remediation notes for this affected asset
            * {**created**} - date stamp when remediation note was created
            * {**note**} - remediation note details
          * {**#notes**} - list of all notes for this affected asset
            * {**note**} - note details
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
            * {**text**} - proof of concept / steps to reproduce
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
          * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
          * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
          * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
          * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
          * {**#tags**} - list of all tags
            * {**.**} - tag
      * {**#affected\_assets**} - list of all affected assets for this vulnerability
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
        * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {**.**} - asset name
        * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
          * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
          * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
          * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
          * {**asset**} - asset name
          * {**#assets**} - details for grouped assets on the vulnerability
            * {**name**} - asset name
            * {**actioned**} - whether asset has been actioned or not
            * {**#components**} - affected components on the asset
              * {**name**} - component name
              * {**#notes**} - notes on the component
                * {**.**} - note
              * {**#tags**} - tags on the component
                * {**.**} - tag
            * {**#notes**} - notes on the asset
              * {**.**} - note
            * {**#tags**} - tags on the asset
              * {**.**} - tag
          * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
          * {**#remediation\_notes**} - list of all remediation notes for this affected asset
            * {**created**} - date stamp when remediation note was created
            * {**note**} - remediation note details
          * {**#notes**} - list of all notes for this affected asset
            * {**note**} - note details
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
            * {**text**} - proof of concept / steps to reproduce
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
          * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
          * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
          * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
          * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
          * {**#tags**} - list of all tags
            * {**.**} - tag
      * {**#evidence**} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
        * {**%fileBase64**} - display image (if evidence type is of image format)
        * {**fileName**} - name of the file uploaded
        * {**caption**} - caption for the file (optional)
* {**#projectNotes**} - list of all exportable project notes
  * {**modified**} - contains date when note was last created or last updated
  * {**note**} - contains note
  * {**notes\_raw**} - details for note in RAW HTML format (verbatim)
  * {**@notes\_styled**} - note styled based on in-app WYSIWYG editor with inline images
* {**#criticalVulns**} - list of all critical vulnerabilities & statistics for affected assets. You can also use {**#highVulns**}; {**#mediumVulns**}; {**#lowVulns**}; and {**#infoVulns**} to access details for vulnerabilities in each of the priority categories.
  * {**retest\_status**} - contains status whether vulnerability is Fixed or Not Fixed. A vulnerability is only considered Fixed if ALL affected assets are also fixed/closed.
  * {**title**} - title of the vulnerability
  * {**total\_affected\_assets**} - total number of affected assets
  * {**total\_affected\_assets\_fixed**} - total number of affected assets which are fixed / closed
  * {**total\_affected\_assets\_retesting**} - total number of affected assets which are flagged for retesting
  * {**total\_affected\_assets\_not\_fixed**} - total number of affected assets which not fixed / open
* {**#attackchains**} - list of all attack chains on the project
  * {**title**} - attack objective
  * {**#links**} - contains details for all links in the chain
    * {**%icon**} - icon displayed for the link in the chain
    * {**type**} - type of link e.g. Action, Vulnerability, Flag etc.
    * {**description**} - details for the link in the chain
    * {**discovered**} - details for when the vulnerability was discovered and by whom
* {**#vulnerabilities**} - list of all the vulnerabilities on the project. You can also use {**#criticalVulnerabilities**}; {**#highVulnerabilities**}; {**#mediumVulnerabilities**}; {**#lowVulnerabilities**}; and {**#infoVulnerabilities**} to access details for vulnerabilities in each of the priority categories.
  * {**#vulnerabilityCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
  * {**#vulnerabilityCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
  * {**title**} - title of the vulnerability
  * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
  * {**remediation\_status**} - either Open or Closed. Only Closed if all affected assets are also Closed.
  * {**description**} - description of the vulnerability&#x20;
  * {**@description\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
  * {**attack\_scenario**} - attack scenario for the vulnerability
  * {**@attack\_scenario\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
  * {**remediation\_recommendation**} - remediation recommendation for the vulnerability
  * {**@remediation\_recommendation\_styled**} - remediation recommendation for the vulnerability styled based on in-app WYSIWYG editor
  * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
  * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
  * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
  * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
  * {**testcases**} - list of all the test cases linked to the vulnerability
  * {**#tags**} - list of all tags
    * {**.**} - tag
  * {**#affected\_assets**} - list of all affected assets for this vulnerability
    * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
    * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
    * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
    * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
    * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
    * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
    * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
    * {**asset**} - asset name
    * {**#assets**} - details for grouped assets on the vulnerability
      * {**name**} - asset name
      * {**actioned**} - whether asset has been actioned or not
      * {**#components**} - affected components on the asset
        * {**name**} - component name
        * {**#notes**} - notes on the component
          * {**.**} - note
        * {**#tags**} - tags on the component
          * {**.**} - tag
      * {**#notes**} - notes on the asset
        * {**.**} - note
      * {**#tags**} - tags on the asset
        * {**.**} - tag
    * {**asset\_library\_created**} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
    * {**asset\_library\_id**} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
    * {**asset\_external\_id**} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
    * {**asset\_type**} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
    * {**asset\_details**} - asset details. NOTE: requires tenant configuration with Assets module enabled.
    * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
    * {**#remediation\_notes**} - list of all remediation notes for this affected asset
      * {**created**} - date stamp when remediation note was created
      * {**note**} - remediation note details
    * {**#notes**} - list of all notes for this affected asset
      * {**note**} - note details
      * {**%inlineScreenshot**} - display inline images where they are included in the note
      * {**caption**} - text caption for the image
    * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
      * {**text**} - proof of concept / steps to reproduce
      * {**%inlineScreenshot**} - display inline images where they are included in the note
      * {**caption**} - text caption for the image
    * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
    * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
    * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
    * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
    * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
    * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
    * {**#tags**} - list of all tags
      * {**.**} - tag
    * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
    * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
      * {**.**} - asset name
    * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
      * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
      * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
      * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
      * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
      * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
      * {**asset**} - asset name
      * {**#assets**} - details for grouped assets on the vulnerability
        * {**name**} - asset name
        * {**actioned**} - whether asset has been actioned or not
        * {**#components**} - affected components on the asset
          * {**name**} - component name
          * {**#notes**} - notes on the component
            * {**.**} - note
          * {**#tags**} - tags on the component
            * {**.**} - tag
        * {**#notes**} - notes on the asset
          * {**.**} - note
        * {**#tags**} - tags on the asset
          * {**.**} - tag
      * {**asset\_library\_created**} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_library\_id**} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_external\_id**} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_type**} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_details**} - asset details. NOTE: requires tenant configuration with Assets module enabled.
      * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
      * {**#remediation\_notes**} - list of all remediation notes for this affected asset
        * {**created**} - date stamp when remediation note was created
        * {**note**} - remediation note details
      * {**#notes**} - list of all notes for this affected asset
        * {**note**} - note details
        * {**%inlineScreenshot**} - display inline images where they are included in the note
        * {**caption**} - text caption for the image
      * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
        * {**text**} - proof of concept / steps to reproduce
        * {**%inlineScreenshot**} - display inline images where they are included in the note
        * {**caption**} - text caption for the image
      * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
      * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
      * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
      * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
      * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
      * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
      * {**#tags**} - list of all tags
        * {**.**} - tag
  * {**#evidence**} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
    * {**%fileBase64**} - display image (if evidence type is of image format)
    * {**fileName**} - name of the file uploaded
    * {**caption**} - caption for the file (optional)
* {**#completedTestcases**} - list of all completed test cases on the project. You can also access {**#inProgressTestcases**}; {**#notTestedTestcases**}; {**#notApplicableTestcases**}; {**#passedTestcases**}; {**#failedTestcases**}; {**#remediatedTestcases**} and {**#abuseCases**} to get details on test cases and their linked vulnerabilities.
  * {**is\_failed**} - default is *No*. If at least one vulnerability is linked to the test case, value will be *Yes*.
  * {**is\_remediated**} - default is *Not Applicable*. If at least one vulnerability is linked to the test case and is Open, value will be *No*. If all vulnerabilities linked to the test case are Closed, value will be *Yes*.
  * {**remediation\_status**} - default is *Passed*. If at least one vulnerability is linked to the test case and is Open, value will be *Failed*. If all vulnerabilities linked to the test case are Closed, value will be *Remediated*.
  * {**tags**} - list of all tags presented as a string
  * {**title**} - test case details
  * {**modified**} - date stamp when test case was created or last modified
  * {**modifiedBy**} - user that created or last last modified the test case
  * {**testcase\_code**} - code assigned to the test case.
  * {**testsuite\_name**} - name of the associated test suite.
  * {**testsuite\_code**} - code of the associated test suite.
  * {**#notes**} - list of all notes assigned to the test case
    * {**modified**} - date stamp when notes was created or last modified
    * {**modifiedBy**} - user that created or last modified the note
    * {**note**} - note details
  * {**#evidence**} - list of all evidence uploaded to the test case
    * {**fileName**} - name of the file for the evidence uploaded
    * {**%fileBase64**} - display image (if evidence type is of image format)
    * {**caption**} - text caption for the image
  * {**#linked\_vulnerabilities**}
    * {**#vulnerabilityCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
    * {**#vulnerabilityCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
    * {**title**} - title of the vulnerability
    * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
    * {**remediation\_status**} - either Open or Closed. Only Closed if all affected assets are also Closed.
    * {**description**} - description of the vulnerability&#x20;
    * {**@description\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
    * {**attack\_scenario**} - attack scenario for the vulnerability
    * {**@attack\_scenario\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
    * {**remediation\_recommendation**} - remediation recommendation for the vulnerability
    * {**@remediation\_recommendation\_styled**} - remediation recommendation for the vulnerability styled based on in-app WYSIWYG editor
    * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
    * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
    * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
    * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
    * {**testcases**} - list of all the linked test cases to the vulnerability
    * {**#tags**} - list of all tags
      * {**.**} - tag
    * {**#affected\_assets**} - list of all affected assets for this vulnerability
      * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
      * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
      * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
      * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
      * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
      * {**asset**} - asset name
      * {**#assets**} - details for grouped assets on the vulnerability
        * {**name**} - asset name
        * {**actioned**} - whether asset has been actioned or not
        * {**#components**} - affected components on the asset
          * {**name**} - component name
          * {**#notes**} - notes on the component
            * {**.**} - note
          * {**#tags**} - tags on the component
            * {**.**} - tag
        * {**#notes**} - notes on the asset
          * {**.**} - note
        * {**#tags**} - tags on the asset
          * {**.**} - tag
      * {**asset\_library\_created**} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_library\_id**} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_external\_id**} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_type**} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
      * {**asset\_details**} - asset details. NOTE: requires tenant configuration with Assets module enabled.
      * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
      * {**#remediation\_notes**} - list of all remediation notes for this affected asset
        * {**created**} - date stamp when remediation note was created
        * {**note**} - remediation note details
      * {**#notes**} - list of all notes for this affected asset
        * {**note**} - note details
        * {**%inlineScreenshot**} - display inline images where they are included in the note
        * {**caption**} - text caption for the image
      * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
        * {**text**} - proof of concept / steps to reproduce
        * {**%inlineScreenshot**} - display inline images where they are included in the note
        * {**caption**} - text caption for the image
      * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
      * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
      * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
      * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
      * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
      * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
      * {**#tags**} - list of all tags
        * {**.**} - tag
      * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
      * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
        * {**.**} - asset name
      * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**asset\_library\_created**} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
        * {**asset\_library\_id**} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
        * {**asset\_external\_id**} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
        * {**asset\_type**} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
        * {**asset\_details**} - asset details. NOTE: requires tenant configuration with Assets module enabled.
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
    * {**#evidence**} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
      * {**%fileBase64**} - display image (if evidence type is of image format)
      * {**fileName**} - name of the file uploaded
      * {**caption**} - caption for the file (optional)
* {**#vulnerabilityAssetMapping**} - list of all vulnerabilities mapped to their affected assets
  * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
  * {**vulnerability**} - vulnerability title
  * {**#assets**} - list of all affected assets
    * {**status**} - remediation status e.g. Fixed / Not Fixed
    * {**asset**} - asset name
* {**#assetVulnerabilityMapping**} - list of all assets on the project mapped to their vulnerabilities
  * {**asset**} - asset name
  * {#**vulnerabilities**} - list of all vulnerabilities the asset is affected by
    * {**vulnerability**} - vulnerability title
    * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
    * {**status**} - remediation status e.g. Fixed / Not Fixed
    * **{#vulnerabilityDetails}**
      * {**#vulnerabilityCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within ReportGen Reports**](https://support.attackforge.com/attackforge.com/modules/reportgen#creating-custom-fields-within-reportgen-reports)
      * {**#vulnerabilityCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
      * {**title**} - title of the vulnerability
      * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
      * {**remediation\_status**} - either Open or Closed. Only Closed if all affected assets are also Closed.
      * {**description**} - description of the vulnerability
      * {**@description\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor&#x20;
      * {**attack\_scenario**} - attack scenario for the vulnerability
      * {**@attack\_scenario\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
      * {**remediation\_recommendation**} - remediation recommendation for the vulnerability
      * {**@remediation\_recommendation\_styled**} - remediation recommendation for the vulnerability styled based on in-app WYSIWYG editor
      * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
      * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
      * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
      * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
      * {**testcases**} - list of all the linked test cases to the vulnerability
      * {**#tags**} - list of all tags
        * {**.**} - tag
      * {**#affected\_asset**} - details for the affected asset - see {#assetVulnerabilityMapping}  - {asset}
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
        * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {**.**} - asset name
        * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
          * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
          * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
          * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
          * {**asset**} - asset name
          * {**#assets**} - details for grouped assets on the vulnerability
            * {**name**} - asset name
            * {**actioned**} - whether asset has been actioned or not
            * {**#components**} - affected components on the asset
              * {**name**} - component name
              * {**#notes**} - notes on the component
                * {**.**} - note
              * {**#tags**} - tags on the component
                * {**.**} - tag
            * {**#notes**} - notes on the asset
              * {**.**} - note
            * {**#tags**} - tags on the asset
              * {**.**} - tag
          * {**asset\_library\_created**} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_library\_id**} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_external\_id**} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_type**} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_details**} - asset details. NOTE: requires tenant configuration with Assets module enabled.
          * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
          * {**#remediation\_notes**} - list of all remediation notes for this affected asset
            * {**created**} - date stamp when remediation note was created
            * {**note**} - remediation note details
          * {**#notes**} - list of all notes for this affected asset
            * {**note**} - note details
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
            * {**text**} - proof of concept / steps to reproduce
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
          * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
          * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
          * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
          * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
          * {**#tags**} - list of all tags
            * {**.**} - tag
      * {**#affected\_assets**} - list of all affected assets for this vulnerability
        * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
        * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
        * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
        * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
        * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project setting
        * {**asset**} - asset name
        * {**#assets**} - details for grouped assets on the vulnerability
          * {**name**} - asset name
          * {**actioned**} - whether asset has been actioned or not
          * {**#components**} - affected components on the asset
            * {**name**} - component name
            * {**#notes**} - notes on the component
              * {**.**} - note
            * {**#tags**} - tags on the component
              * {**.**} - tag
          * {**#notes**} - notes on the asset
            * {**.**} - note
          * {**#tags**} - tags on the asset
            * {**.**} - tag
        * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
        * {**#remediation\_notes**} - list of all remediation notes for this affected asset
          * {**created**} - date stamp when remediation note was created
          * {**note**} - remediation note details
        * {**#notes**} - list of all notes for this affected asset
          * {**note**} - note details
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
          * {**text**} - proof of concept / steps to reproduce
          * {**%inlineScreenshot**} - display inline images where they are included in the note
          * {**caption**} - text caption for the image
        * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
        * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
        * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
        * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
        * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
        * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
        * {**#tags**} - list of all tags
          * {**.**} - tag
        * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
        * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
          * {**.**} - asset name
        * {**#assets\_equally\_affected\_full\_details**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to access the full asset details of all the assets which have the same POC & Notes as the vulnerability above.
          * {**#assetCustomTags**} - you can define & use custom tags in ReportGen. For more details check out [**Creating Custom Tags within Individual Reports**](https://support.attackforge.com/attackforge-enterprise/modules/reporting#creating-custom-fields-within-individual-reports)
          * {**#assetCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**#assetLibraryCustomFields**} - you can define & use custom fields in AttackForge. For more details check out [**Creating Custom Fields & Forms**](https://support.attackforge.com/attackforge-enterprise/getting-started/custom-fields-and-forms)
          * {**sla**} - SLA for the vulnerability, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**release\_date**} - Release date for when the vulnerability was published/available to project team, in date format. Requires SLAs to be enabled in tenant configuration options.
          * {**target\_remediation\_date**} - Remediation plan for the vulnerability, in date format. Requires Remediation Plan to be enabled in tenant configuration options.
          * {**alternate\_id**} - user-friendly id associated with the vulnerability, set via project settings
          * {**asset**} - asset name
          * {**#assets**} - details for grouped assets on the vulnerability
            * {**name**} - asset name
            * {**actioned**} - whether asset has been actioned or not
            * {**#components**} - affected components on the asset
              * {**name**} - component name
              * {**#notes**} - notes on the component
                * {**.**} - note
              * {**#tags**} - tags on the component
                * {**.**} - tag
            * {**#notes**} - notes on the asset
              * {**.**} - note
            * {**#tags**} - tags on the asset
              * {**.**} - tag
          * {**asset\_library\_created**} - timestamp when asset was added to Assets module library. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_library\_id**} - Assets module library id. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_external\_id**} - user-defined external id for the asset. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_type**} - asset type e.g. Web App, API, Network, etc. NOTE: requires tenant configuration with Assets module enabled.
          * {**asset\_details**} - asset details. NOTE: requires tenant configuration with Assets module enabled.
          * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open / Ready for Retest on \<DATE> / Closed on \<DATE>*
          * {**#remediation\_notes**} - list of all remediation notes for this affected asset
            * {**created**} - date stamp when remediation note was created
            * {**note**} - remediation note details
          * {**#notes**} - list of all notes for this affected asset
            * {**note**} - note details
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
            * {**text**} - proof of concept / steps to reproduce
            * {**%inlineScreenshot**} - display inline images where they are included in the note
            * {**caption**} - text caption for the image
          * {**@proof\_of\_concept\_styled**} - proof of concept styled based on in-app WYSIWYG editor with inline images
          * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
          * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
          * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0
          * {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
          * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
          * {**#tags**} - list of all tags
            * {**.**} - tag
      * {**#evidence**} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
        * {**%fileBase64**} - display image (if evidence type is of image format)
        * {**fileName**} - name of the file uploaded
        * {**caption**} - caption for the file (optional)

## **Tags for Combined Reports**

**!IMPORTANT:** To test your combined reports, simply select multiple JSON files on the *'Select Your JSON File'* step when using the ReportGen browser tool.

* {**@pageBreak**} - adds a page break.
* {**#projectName**} - list of all projects combined in the report
  * {**.**} - name of the project
* {**#projectCode**} - list of all project codes for all projects combined in the report
  * {**.**} - project code
* {**timestamp**} - timestamp for when this report was created
* {**totalUniqueVulnerabilities**} - total unique vulnerabilities across all projects
* {**totalCriticalVulns**} - total unique critical vulnerabilities across all projects
* {**totalHighVulns**} - total unique high vulnerabilities across all projects
* {**totalMediumVulns**} - total unique medium vulnerabilities across all projects
* {**totalLowVulns**} - total unique low vulnerabilities across all projects
* {**totalInfoVulns**} - total unique informational vulnerabilities across all projects
* {**totalZeroDayVulns**} - total unique zero-day vulnerabilities across all projects
* {**totalEasilyExploitableVulns**} - total unique easily exploitable vulnerabilities across all projects
* {**#execSummaryNotes**} - list of all executive summary's across all projects
  * {**project**} - name of the project
  * {**notes**} - executive summary notes on the project
* {**#testWindow**} - list of all test windows and progress across all projects
  * {**project**} - name of the project
  * {**startDate**} - test window start date for the project
  * {**progress**} - percentage of test cases actioned on the project
  * {**endDate**} - test window start date for the project
* {**totalVulns**} - total vulnerabilities across all assets across all projects
* {**totalCriticalVulnsAllAssets**} - total critical vulnerabilities across all assets across all projects
* {**totalHighVulnsAllAssets**} - total high vulnerabilities across all assets across all projects
* {**totalMediumVulnsAllAssets**} - total medium vulnerabilities across all assets across all projects
* {**totalLowVulnsAllAssets**} - total low vulnerabilities across all assets across all projects
* {**totalInfoVulnsAllAssets**} - total informational vulnerabilities across all assets across all projects
* {**#assets**} - list of all assets on the project
  * {**name**} - name of each asset
  * {**project**} - name of the project
* {**#projectTeam**} - list of all project team members
  * {**name**} - name of each project team member
  * {**project**} - name of the project
* {**#retestingHistory**} - list of all rounds of retesting requested & completed on the project
  * {**retesting\_round\_status**} - whether the retest round was Requested or Completed
  * {**retesting\_round\_actioned\_by**} - name of person who requested or completed the round of retesting
  * {**created**} - date when round of retest was requested or completed
  * {**project**} - name of the project
  * {**#vulnerabilities**} - list of all vulnerabilities requested / completed on the round of retesting
    * {**vulnerability**} - contains name of the vulnerability
* {**#projectNotes**} - list of all exportable project notes
  * {**project**} - name of the project
  * {**modified**} - contains date when note was last created or last updated
  * {**note**} - contains note
* {**#criticalVulns**} - list of all critical vulnerabilities & statistics for affected assets across all projects
  * {**title**} - title of the vulnerability
  * {**total\_affected\_assets**} - total number of affected assets
* {**#highVulns**} - list of all high vulnerabilities & statistics for affected assets across all projects
  * {**title**} - title of the vulnerability
  * {**total\_affected\_assets**} - total number of affected assets
* {**#mediumVulns**} - list of all medium vulnerabilities & statistics for affected assets across all projects
  * {**title**} - title of the vulnerability
  * {**total\_affected\_assets**} - total number of affected assets
* {**#lowVulns**} - list of all low vulnerabilities & statistics for affected assets across all projects
  * {**title**} - title of the vulnerability
  * {**total\_affected\_assets**} - total number of affected assets
* {**#infoVulns**} - list of all critical vulnerabilities & statistics for affected assets across all projects
  * {**title**} - title of the vulnerability
  * {**total\_affected\_assets**} - total number of affected assets
* {**#attackchains**} - list of all attack chains across all projects
  * {**title**} - attack objective
  * {**#links**} - contains details for all links in the chain
    * {**%icon**} - icon displayed for the link in the chain
    * {**type**} - type of link e.g. Action, Vulnerability, Flag etc.
    * {**description**} - details for the link in the chain
    * {**discovered**} - details for when the vulnerability was discovered and by whom
* {**#vulnerabilities**} - list of all the vulnerabilities across all projects
  * {**title**} - title of the vulnerability
  * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
  * {**remediation\_status**} - either Open or Closed. Only Closed if all affected assets are also Closed.
  * {**description**} - description of the vulnerability
  * {**@description\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor&#x20;
  * {**attack\_scenario**} - attack scenario for the vulnerability
  * {**@attack\_scenario\_styled**} - description of the vulnerability styled based on in-app WYSIWYG editor
  * {**remediation\_recommendation**} - remediation recommendation for the vulnerability
  * {**@remediation\_recommendation\_styled**} - remediation recommendation for the vulnerability styled based on in-app WYSIWYG editor
  * {**cvssv3\_vector**} - includes the CVSS v3.1 vector string e.g. /AV/...
  * {**cvssv3\_base\_score**} - includes the CVSS v3.1 base score e.g. 10.0\
    {**cvssv3\_temporal\_score**} - includes the CVSS v3.1 temporal score e.g. 10.0
  * {**cvssv3\_environmental\_score**} - includes the CVSS v3.1 environmental score e.g. 10.0
  * {**#tags**} - list of all tags
    * {**.**} - tag
  * {**#affected\_assets**} - list of all affected assets for this vulnerability
    * {**asset**} - asset name
    * {**remediation\_status**} - includes the remediation status of the vulnerability for the affected asset e.g. *Open* or *Closed on \<DATE>*
    * {**#remediation\_notes**} - list of all remediation notes for this affected asset
      * {**created**} - date stamp when remediation note was created
      * {**note**} - remediation note details
    * {**#notes**} - list of all notes for this affected asset
      * {**note**} - note details
      * {**%inlineScreenshot**} - display inline images where they are included in the note
    * {**#proof\_of\_concept**} - details for proof of concept / steps to reproduce
      * {**text**} - proof of concept / steps to reproduce
      * {**%inlineScreenshot**} - display inline images where they are included in the note
    * {**#proof\_of\_concept\_raw**} - details for proof of concept / steps to reproduce in RAW HTML format (verbatim).
    * {**#assets\_equally\_affected\_title**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the heading for this section e.g. LIST OF ASSETS EQUALLY AFFECTED
    * {**#assets\_equally\_affected**} - in order to cut-down report size, de-duplication is performed for each asset where #notes and #proof\_of\_concept are the same. This tag is used to display the names of all the assets which have the same POC & Notes as the vulnerability above.
      * {**.**} - asset name
  * {**#evidence**} - list of all evidence files uploaded to the vulnerabilities for each affected asset. De-duplication is performed to remove images which have already been displayed in the in-line screenshots
    * {**%fileBase64**} - display image (if evidence type is of image format)
    * {**fileName**} - name of the file uploaded
* {**#vulnerabilityAssetMapping**} - list of all vulnerabilities mapped to their affected assets
  * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
  * {**vulnerability**} - vulnerability title
  * {**#assets**} - list of all affected assets
    * {**status**} - remediation status e.g. Fixed / Not Fixed
    * {**asset**} - asset name
* {**#assetVulnerabilityMapping**} - list of all assets across all projects mapped to their vulnerabilities
  * {**asset**} - asset name
  * {#**vulnerabilities**} - list of all vulnerabilities the asset is affected by
    * {**vulnerability**} - vulnerability title
    * {**priority**} - priority of the vulnerability e.g. Critical, High, Medium, Low, Info
    * {**status**} - remediation status e.g. Fixed / Not Fixed


# Template - Options

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

## RemoveDuplicateEvidence

This option can be set at the beginning of your template in order to remove duplicate Evidence for vulnerabilities which have already used/displayed the evidence within the Proof-of-Concept or Notes for any of the affected assets, for example the images have already appeared in-line within the Proof-of-Concept or Notes.

```
{#$optionRemoveDuplicateEvidence}{/}
```

This option is useful to reduce duplicate evidence displaying, significantly reducing report size and making content more useful to the reader.

#### How it works:

When looping over `{#affected_assets}` - if the Proof of Concept or Notes includes an inline image - this image will be removed from the {#evidence} section to avoid duplication of displaying the same evidence file.

Example:

**!IMPORTANT: You must include {#$optionRemoveDuplicateEvidence}{/} tag at the beginning of your template file.**

```
{#$optionRemoveDuplicateEvidence}{/}
...
{#vulnerabilities}
VULNERABILITY: {title}
{#affected_assets}
ASSET: {asset}
NOTES:
{#notes_html}
{@note_styled(“image_description”:“caption”) | resize:[“400”]}
{/notes_html}
PROOF OF CONCEPT:
{@proof_of_concept_styled(“image_description”:“caption”) | resize:[“400”]}
{/affected_assets}
{#evidence}
{%fileBase64 | resize:[“400”]}
{fileName}
{/evidence}
{/vulnerabilities}
```


# Template - Functions

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

![Cheat Sheet](/files/o69POhn6ItWQVM8Qb7uN)

## $declare

Use this function to declare a variable, which can then be used in other procedures and operators below.

```
{$declare[variable][value]}
```

#### Parameters

* **variable** - the name of the variable.
  * variables - other variables. See [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$declare\["$(SomeVariable)"]\["Warrior"]} will translate to {$declare\[Ninja]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
* **value** - the value to assign to this variable. Supports:
  * booleans
  * integers
  * "strings"
  * \[] - empty array
  * \<Dictionary> - a flat list of key:value pairs
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
  * this - the current scope
  * this\[number] - an array element of the current scope

**Example**

Declare a new variable 'myVariable' and set a value of 0.

```
{$declare[myVariable][0]}
```

Declare a new variable 'myVariable' and set a value of false.

```
{$declare[myVariable][false]}
```

&#x20;Declare a new variable 'myVariable' set a value of empty list/array.

```
{$declare[myVariable][[]]}
```

Declare a new key 'key1' on an existing Dictionary variable 'myVariable' and set a value of empty list/array.

```
{$declare[myVariable][<Dictionary>]}
{$declare[myVariable["key1"]][[]]}
```

Declare a new key based on value of another variable 'otherVariable' on an existing Dictionary variable 'myVariable' and set a value of empty list/array.

```
{$declare[myVariable][<Dictionary>]}
{$declare[myVariable["$(otherVariable)"]][[]]}
```

## $push

Use this function to push a value to a variable which has been initialized as an array.

```
{$push[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
  * variables - other variables. See [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$push\["$(SomeVariable)"]\["test"]} will translate to {$push\[Ninja]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
* **value** - the value to add to the end of the array. Supports:
  * "strings"
  * integers
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
  * this - the current scope
  * this\[number] - an array element of the current scope

#### Example 1

Example below will create a new variable that contains the details for each affected asset for *SSL Weak Cipher* vulnerabilities based on custom tags. Combine with $value to retrieve the affected assets.&#x20;

```
{$declare[WeakCipherVulns][[]]}
{#vulnerabilities | filterBy:“AffectedAssetCustomTags”:[“ssl_weak_ciphers:true”]}
{$push[WeakCipherVulns][“%(./)”]}
{/}
{#$value[WeakCipherVulns]}
Weak Cipher Vulnerability Title: {title}
{/}
```

#### **Example 2 - pushing to a Dictionary**

Example below will push data to a Dictionary and use $keys to display the data.

```
{$declare[TestingPhaseVulns][<Dictionary>]}
{#projectCustomFields}
{#testing_phases}
{$declare[CurrentPhase][“%(./)”]}
{$declare[TestingPhaseVulns[“$(CurrentPhase)”]][[]]}
{#vulnerabilities | filterBy:“AffectedAssetCustomFields”:[“testing_phase:$(CurrentPhase)”]}
{$push[TestingPhaseVulns[“$(CurrentPhase)”]][“%(./)”]}
{/}{/}{/}
{#$keys[TestingPhaseVulns]}
Vulnerabilities for Testing Phase: {this[0]}
{#this[1]}
Vulnerability Title: {title}
{/}{/}
```

## $increment

Use this function to increment a variable which has a numeric value.

```
{$increment[variable][number]}
```

#### Parameters

* **variable** - the name of the variable. Supports:
  * variables - see [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$increment\["$(SomeVariable)"]\[1]} will translate to {$increment\[Ninja]\[1]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
* **number** - the number to increment the existing value for this variable. Supports:
  * integers
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that increments the total number of vulnerabilities by 1, every time it loops through a new vulnerability. Combine with $value to retrieve the count.

```
{$declare[CountTotalVulnerabilities][0]}
{#vulnerabilities}
{$increment[CountTotalVulnerabilities][1]}
{/}
```

## $decrement

Use this function to decrease a variable which has a numeric value.

```
{$decrement[variable][number]}
```

#### Parameters

* **variable** - the name of the variable. Supports:
  * variables - see [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$decrement\["$(SomeVariable)"]\[1]} will translate to {$decrement\[Ninja]\[1]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
* **number** - the number to decrease the existing value for this variable. Supports:&#x20;
  * integers
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that counts all vulnerabilities except for *info* vulnerabilities. It decreases the total number of vulnerabilities by 1, every time it loops through a new vulnerability and where the vulnerability priority is Info. Combine with $value to retrieve the count.

```
{$declare[CountAllVulnsExceptInfo][0]}
{#vulnerabilities}
{$increment[CountAllVulnsExceptInfo][1]}
{#priority === "Info"}
{$decrement[CountAllVulnsExceptInfo][1]}
{/}{/}
```

## $multiply

Use this function to multiply a variable which has a numeric value.

```
{$multiply[variable][number]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **number** - the number to multiply the existing value for this variable. Supports:&#x20;
  * integers
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable 'AmountToCharge' with a default amount of $500. It then checks for a project custom field 'rateToMultiplyCharge' and multiplies 'AmountToCharge' by this amount. Combine with $value to show the amount to charge.

```
{$declare[AmountToCharge][500]}
{#projectCustomFields}
{#rateToMultiplyCharge}
{$multiply[AmountToCharge][”%(rateToMultiplyCharge)”]}
{/}{/}
{$value[AmountToCharge]}
```

## $percentage

Use this function to get the percentage of two numeric values.

```
{$percentage[variable][number]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **number** - the number to compare for percentage. Supports:&#x20;
  * integers
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example 1 - Get Percentage of Value Divided by TotalValue

```
{$declare[TotalValue][100]}
{$declare[Value][25]}
{$percentage[Value]["$(TotalValue)"]}%
```

#### Example 2 - Get Percentage of Value Divided by TotalValue, Rounded Up

```
{$declare[TotalValue][47]}
{$declare[Value][25]}
{$percentage[Value]["$(TotalValue)"] | roundUp}%
```

#### Example 3 - Get Percentage of Value Divided by TotalValue, to 2 Decimal Points

```
{$declare[TotalValue][47]}
{$declare[Value][25]}
{$percentage[Value]["$(TotalValue)"] | toFloat:'2'}%
```

## $append

Use this function to append data to an existing variable.

```
{$append[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **value** - the value to assign to this variable. Supports:
  * booleans
  * integers
  * "strings"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable, then append the vulnerability title to it.

```
{$declare[VulnTitle]["Title: "]}
{#vulnerabilities[0]}
{$append[VulnTitle]["%(./)"]}
{/}
{$value[VulnTitle]}
```

## $assign

Use this function to assign a new value for a variable.

```
{$assign[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. Supports:
  * variables - see [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$assign\["$(SomeVariable)"]\[1]} will translate to {$assign\[Ninja]\[1]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
* **value** - the value to assign to this variable. Supports:
  * booleans
  * integers
  * "strings"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
  * this - the current scope
  * this\[number] - an array element of the current scope

#### Example 1

Example below will create a new variable that counts all affected assets for every vulnerability, then prints the count along with the vulnerability name. It uses $assign to reset the counter for every new vulnerability.

```
{$declare[CountAllAffectedAssets][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[CountAllAffectedAssets][1]}
{/}
Total affected assets for vulnerability {title}:
{$value[CountAllAffectedAssets]}
{$assign[CountAllAffectedAssets][0]}
{/}
```

#### Example 2 - assigning to a Dictionary

Example below will create a new Dictionary variable that counts all affected assets for every vulnerability, then prints the count along with the vulnerability name.

```
{$declare[VulnsCountAffectedAssets][<Dictionary>]}
{#vulnerabilities}
{$declare[VulnsCountAffectedAssets[“%(title)”]][0]}
{$assign[VulnsCountAffectedAssets[“%(title)”]][“%(affected_assets.length)”]}
{/}
{#$keys[VulnsCountAffectedAssets]}
Vulnerability Title: {this[0]} – Total Affected Assets: {this[1]}
{/}
```

## $value

Use this function to retrieve the value for a variable.

```
{$value[variable]}
```

#### Parameters

* **variable** - the name of the variable. Supports:
  * variables - see [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$value\["$(SomeVariable)"]} will translate to {$value\[Ninja]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that counts all affected assets for every vulnerability, then prints the count along with the vulnerability name. It resets the counter for each new vulnerability.

```
{$declare[CountAllAffectedAssets][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[CountAllAffectedAssets][1]}
{/}
Total affected assets for vulnerability {title}:
{$value[CountAllAffectedAssets]}
{$assign[CountAllAffectedAssets][0]}
{/}
```

## $keys

Use this function to retrieve the value for a Dictionary. $keys will return the data from a Dictionary in the following format:

```
[
    [
        "key",
        "value"
    ]
]
```

Therefore you can access the key using this\[0] and value using this\[1]. See example below.

#### Example

The example below uses a Dictionary to store the name of every unique vulnerability along with its total number of affected assets, then prints the data using this function.

```
{$declare[VulnsCountAffectedAssets][<Dictionary>]}
{#vulnerabilities}
{$declare[VulnsCountAffectedAssets[“%(title)”]][0]}
{$assign[VulnsCountAffectedAssets[“%(title)”]][“%(affected_assets.length)”]}
{/}
{#$keys[VulnsCountAffectedAssets]}
Vulnerability Title: {this[0]} – Total Affected Assets: {this[1]}
{/}
```

You can also use {$keys\[this]} on any object, which will return each key/value pair in the object as an array - {this\[0]} for the key, and {this\[1]} for the value.

```
{#vulnerabilities}
{#$keys[this]}
{$help[scope]}
Key: {this[0]}
Value: {this[1]}
{/}{/}
```

## $sort

Use this function to sort the data within a variable.

```
{#$sort[variable]["key:<asc/desc>"]}{/}
```

* **key** - JSON object key. Omit this value if sorting a string array (just keep colon)
* **asc** - sort data in ascending order
* **desc** - sort data in descending order

To observe the variables available for sorting - use the [$help](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdhelp) procedure.

#### Example 1: Sort A List

```
{#$sort[UniqueAffectedEndpoints][":asc"]}{/}
```

#### Example 1: Single-Key Sort

```
{#$sort[UniqueAffectedEndpoints]["af_sys_affected_endpoint:desc"]}{/}
```

#### Example 2: Multi-Key Sort

```
{#$sort[UniqueOWASPTop10Vulns]["priority:asc","title:desc"]}{/}
```

## $includes

Use this function to check if a value exists or does not exist (excludes) within a variable.

To check if data exists:

```
{#$includes[variable][value]}{/}
```

To check if data does not exist (excludes):

```
{^$includes[variable][value]}{/}
```

* **value** - the value to assign to this variable. Supports:
  * booleans
  * integers
  * "strings"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$includes\["$(SomeVariable)"]\["Something"]} will translate to {$includes\[Ninja]\["Something"]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.

The following example creates a unique list of affected asset names, then prints the list.

```
{$declare[UniqueAssets][[]]}
{#vulnerabilities}
{#affected_assets}
{^$includes[UniqueAssets]["%(asset)"]}
{$push[UniqueAssets]["%(asset)"]}
{/}{/}{/}
{#$value[UniqueAssets]}
{.}
{/}
```

The following example checks if a Dictionary "Evidence" has a key "POC" which includes a given POC.

```
{$declare[UniquePOCs][[]]}
{#affected_assets}
{^$includes[UniquePOCs][“%(proof_of_concept_raw)”]}
{$push[UniquePOCs][“%(proof_of_concept_raw)”]}
{/}{/}{/}
{#$value[UniquePOCs]
{$declare[Evidence][<Dictionary>]}
{$declare[Evidence["POC"]]["%(./)"]}
{#affected_assets}
{^$includes[Evidence["POC"]][“%(proof_of_concept_raw)”]}
{$push[Evidence["POC"]][“%(proof_of_concept_raw)”]}
{/}{/}{/}
```

## $comment

Use this function to include a comment in your template which will not show in your report.

You can include a comment in two separate ways:

```
{$comment[THIS IS A COMMENT]}
{!!THIS IS A COMMENT}
```

## $help

Use this function to print diagnostic information to your ReportGen browser console.

Insert this function in the relevant section in your template to se information about variables and scope.

You can add your own labels to help with debugging when you have multiple help functions used in your template.

```
{$help[label][function]}
```

#### Parameters

* {$help\[some label]\[var]} - prints diagnostic information for [variables](#variables)
* {$help\[some label]\[scope]} - prints diagnostic information for [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example 1 - Printing Help for Scope

```
{$help[All Report Data][scope]}
```

<figure><img src="/files/FwjmocmvDTX1Q23yePei" alt=""><figcaption></figcaption></figure>

```
{#vulnerabilities}
{$help[Vulnerability][scope]}
{/}
```

<figure><img src="/files/tZwVrDrrKZ07qAsL17vR" alt=""><figcaption></figcaption></figure>

#### Example 2 - Printing Help for Variables

```
{$declare[TotalUniqueVulnerabilities][0]}
{#vulnerabilities}
{$increment[TotalUniqueVulnerabilities][1]}
{/}
{$help[My Variables][var]}
```

<figure><img src="/files/m4hqog0gLjsdw4LBdIpV" alt=""><figcaption></figcaption></figure>

## **@$hyperlink**

Use this function to create a hyperlink.

```
{@$hyperlink[text][link]}
```

#### Parameters

* **text** - the text for the hyperlink. Supports:
  * "strings"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
* **link** - the URL to assign to the hyperlink. Supports:
  * "strings"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example 1 - Scope

Example below will create a new hyperlink based on scope.

```
{@$hyperlink[“%(projectName)”][“%(data.project.url)”]}
```

#### Example 2 - Manual

Example below will create a new hyperlink based on manually entered in values.

```
{@$hyperlink[“AttackForge.com”][“https://attackforge.com”]}
```

#### Example 3 - Variables

Example below will create a new hyperlink based on the values from other variables.

```
{$declare[Text][“Link to ReportGen”]}
{$declare[Link][“https://www.attackforge.com/reportgen.html”]}
{@$hyperlink[“$(Text)”][“$(Link)”]}
```

## **$range**

Use this function to create a range of data which you can iterate over.

```
{#$range[start][stop][step]}
{this}
{/}
```

#### Parameters

* **start** - a positive or negative integer. Supports:
  * positive integers
  * negative integers
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
* **stop** - a positive or negative integer. Supports:
  * positive integers
  * negative integers
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
* **step** - a positive or negative integer. Supports:
  * positive integers
  * negative integers
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example 1&#x20;

```
{!! Range 0 -> 10 - step 1 }
{#$range[0][10][1]}
{this}
{/}
{!! Will loop 10 times, where 'this' equals 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
```

#### Example 2

```
{!! Range 10 -> 0 - step -1 }
{#$range[10][0][-1]}
{this}
{/}
{!! Will loop 10 times, where 'this' equals 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }
```

#### Example 3

```
{$declare[Start][0]}
{$declare[Stop][10]}
{$declare[Step][1]}
{!! Range 10 -> 0 - step -1 }
{#$range["$(Start)"]["$(Stop)"]["$(Step)"]}
{this}
{/}
{!! Will loop 10 times, where 'this' equals 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
```

## **$dateRange**

Use this function to create a range of dates which you can iterate over.

```
{#$dateRange[start][stop][step]}
{this}
{/}
```

#### Parameters

* **start** - an ISO 8601 date string. Supports:
  * "string"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
* **stop** - an ISO 8601 date string. Supports:
  * "string"
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
* **step** - a duration. Supports:
  * X milliseconds / -X milliseconds
  * X seconds / -X seconds
  * X minutes / -X minutes
  * X hours / -X hours
  * X days / -X days
  * X weeks / -X weeks
  * X months / -X months
  * X years / -X years
  * variables - other variables. See [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example 1&#x20;

```
{!! Range 1st Jan 2026 -> midnight 2nd Jan 2026 - step 1 day }
{#$dateRange[2026-01-01T00:00:00.000Z][2026-01-03T00:00:00.000Z][1 day]}
{this}
{/}
{!! Will loop 2 times, where 'this' equals 2026-01-01T00:00:00.000Z, 2026-01-02T00:00:00.000Z }
```

#### Example 2

```
{!! Range 1st Jan 2026 -> 3rd Jan 2026 - step 1 day }
{#$dateRange[2026-01-01T00:00:00.000Z][2026-01-03T00:00:00.001Z][1 day]}
{this}
{/}
{!! Will loop 3 times, where 'this' equals 2026-01-01T00:00:00.000Z, 2026-01-02T00:00:00.000Z, 2026-01-03T00:00:00.000Z }
```

#### Example 3

```
{$declare[Start]["2026-01-01T00:00:00.000Z"]}
{$declare[Stop]["2026-01-03T00:00:00.000Z"]}
{$declare[Step]["1 day"]}
{!! Range 1st Jan 2026 -> midnight 2nd Jan 2026 - step 1 day }
{#$dateRange["$(Start)"]["$(Stop)"]["$(Step)"]}
{this}
{/}
{!! Will loop 2 times, where 'this' equals 2026-01-01T00:00:00.000Z, 2026-01-02T00:00:00.000Z }
```

#### Example 4

Show the date on each day between a start date and end date:

```
{$declare[StartDate]["%(data.testing_summary.start_date_isodatetime)"]}
{$declare[EndDate]["%(data.testing_summary.end_date_isodatetime)"]}
Start Date: {$value[StartDate]}
End Date: {$value[EndDate]}
{$declare[DayCounter][1]}
{#$dateRange["$(StartDate)"]["$(EndDate)"][1 day]}
Day {$value[DayCounter]}: {this}{$increment[DayCounter][1]}
{/}
```

## **$index**

Use this function to print the current index of the loop you are iterating over:

```
{#vulnerabilities}
{$index} - {title}
{/}
```

## **$isFirst**

Use this function to check if you are in the first iteration of a loop.

For example, if you want to add a section heading BEFORE printing the vulnerability titles:

```
{#vulnerabilities}
{#$isFirst}VULNERABILITIES
{/}{title}
{/}
```

Another example is if you want to check if it IS NOT the first iteration of a loop:

```
{#vulnerabilities}
{^$isFirst}NOT FIRST{/}{title}
{/}
```

## **$isLast**

Use this function to check if you are in the last iteration of a loop.

For example, if you want to add an extra line break after every vulnerability title except for the last:

```
{#vulnerabilities}
{title}{^$isLast}
{/}{/}
```

Another example is if you want to check if it IS the last iteration of a loop:

```
{#vulnerabilities}
{#$isLast}IS LAST{/}{title}
{/}
```

## $equals

Use this function to perform an equality comparison for a variable against a value.

```
{$equals[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. Supports:
  * variables - see [variables](#variables)
  * dynamic variables - used when the variable name is not known ahead of time (in the template). For example:
    * {$declare\[SomeVariable]\["Ninja"]}
    * {$equals\["$(SomeVariable)"]\[true]} will translate to {$equals\[Ninja]\[true]} where "Ninja" is the value of the variable "SomeVariable" and only known at runtime.
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * booleans
  * integers
  * "strings"
  * variables - see [variables](#variables)
  * scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that will be used to print a section heading *AFFECTED ASSETS:* before listing all of the affected assets for every vulnerability. After printing the heading, it assigns the variable to *false* to prevent it displaying on the next affected asset. After looping through and listing every affected asset, it re-assigns the variable to *true* so it is printed for the next vulnerability.

```
{$declare[AffectedAssetsHeading][true]}
{#vulnerabilities}
{#affected_assets}
{#$equals[AffectedAssetsHeading][true]}
AFFECTED ASSETS:
{$assign[AffectedAssetsHeading][false]}
{/}
{asset}
{/}
{$assign[AffectedAssetsHeading][true]}
{/}
```

## $equalsRegExp

Use this function to perform an equality comparison for a variable against a value using a Regular Expression test. Performs a global, case insensitive test.

```
{$equalsRegExp[variable][/someRegExp/]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * Regular Expression - /.../

#### Example 1

```
{$declare[MyName][“Bobby Brown”]}
{#$equalsRegExp[MyName][/brown/]}
My Name has Brown
{/}{^$equalsRegExp[MyName][/brown/]}
My Name does not have Brown
{/}
```

#### Example 2

```
{$declare[TestURL][“https://google.com”]}
{#$equalsRegExp[TestURL][/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]|www\.[a-zA-Z0-9]+\.[^\s])/]}
Is a URL
{/}{^$equalsRegExp[TestURL][/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]|www\.[a-zA-Z0-9]+\.[^\s])/]}
Is NOT a URL
{/}
```

#### Example 3

```
{$declare[TestIP][“192.168.0.1”]}
{#$equalsRegExp[TestIP][/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/]}
Is an IP address
{/}{^$equalsRegExp[TestIP][/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/]}
Is NOT an IP address
{/}
```

## $lessThan

Use this function to perform a 'less than' comparison for a variable against a number or date.

```
{$lessThan[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * integers
  * UTC datetime string
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that will be used to count all affected assets for every vulnerability, and if there are less than 5 affected assets on a vulnerability - it will print *There is less than 5 affected assets for this vulnerability.* It resets the counter to 0 after looping on each vulnerability so it is ready for the next vulnerability.

```
{$declare[TotalAffectedAssets][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[TotalAffectedAssets][1]}
{/}
{#$lessThan[TotalAffectedAssets][5]}
There is less than 5 affected assets for this vulnerability.
{/}
{$assign[TotalAffectedAssets][0]}
{/}
```

## $lessThanOrEqual

Use this function to perform a 'less than or equal' comparison for a variable against a number.

```
{$lessThanOrEqual[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * integers
  * UTC datetime string
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that will be used to count all affected assets for every vulnerability, and if there are less than 5 affected assets on a vulnerability - it will print *There is 5 or less affected assets for this vulnerability.* It resets the counter to 0 after looping on each vulnerability so it is ready for the next vulnerability.

```
{$declare[TotalAffectedAssets][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[TotalAffectedAssets][1]}
{/}
{#$lessThanOrEqual[TotalAffectedAssets][5]}
There is 5 or less affected assets for this vulnerability.
{/}
{$assign[TotalAffectedAssets][0]}
{/}
```

## $greaterThan

Use this function to perform a 'greater than' comparison for a variable against a number.

```
{$greaterThan[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * integers
  * UTC datetime string
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that will be used to count all affected assets for every vulnerability, and if there are more than 5 affected assets on a vulnerability - it will print *There are more than 5 affected assets for this vulnerability.* It resets the counter to 0 after looping on each vulnerability so it is ready for the next vulnerability.

```
{$declare[TotalAffectedAssets][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[TotalAffectedAssets][1]}
{/}
{#$greaterThan[TotalAffectedAssets][5]}
There are more than 5 affected assets for this vulnerability.
{/}
{$assign[TotalAffectedAssets][0]}
{/}
```

## $greaterThanOrEqual

Use this function to perform a 'greater than or equal' comparison for a variable against a number.

```
{$greaterThanOrEqual[variable][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * integers
  * UTC datetime string
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

Example below will create a new variable that will be used to count all affected assets for every vulnerability, and if there are 5 or more affected assets on a vulnerability - it will print *There are 5 or more affected assets for this vulnerability.* It resets the counter to 0 after looping on each vulnerability so it is ready for the next vulnerability.

```
{$declare[TotalAffectedAssets][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[TotalAffectedAssets][1]}
{/}
{#$greaterThanOrEqual[TotalAffectedAssets][5]}
There are 5 or more affected assets for this vulnerability.
{/}
{$assign[TotalAffectedAssets][0]}
{/}
```

## $dateDiff

Use this function to perform a diff between two dates.

```
{$dateDiff[variable/scope][value]}
```

#### Parameters

* **variable** - the name of the variable. See $declare.
* **scope** - scope - data contained within the JSON file. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)
* **value** - the value which is used to perform the comparison against this variable. Supports:
  * UTC datetime string
  * variables - other variables which are integers. See [variables](#variables)
  * scope - data contained within the JSON file that is number format. See [scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope)

#### Example

```
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"] | declare:"diffInMilliseconds"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["milliseconds"] | declare:"diffInMilliseconds"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["seconds"] | declare:"diffInSeconds"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["minutes"] | declare:"diffInMinutes"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["hours"] | declare:"diffInHours"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["days"] | declare:"diffInDays"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["weeks"] | declare:"diffInWeeks"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["months"] | declare:"diffInMonths"}
{$dateDiff[%(timestamp)]["2026-02-27T04:28:47.515Z"]["years"] | declare:"diffInYears"}
```

## Variables

Variables can be declared using the [$declare](#usddeclare) function.

Once a variable has been declared, it can be used in other functions as follows:

**"$(variable)"**

For example, you can declare a new variable with the value of a different variable as follows:

```
{$declare[Variable1][25]}
{$declare[Variable2]["$(Variable1)"]}
Value of Variable2 is 25:
{$value[Variable2]}
```

Or you can compare variables again each other:

```
{$declare[Variable1][25]}
{$declare[Variable2][30]}
Check if Variable1 is greater than Variable2
{#$greaterThan[Variable1]["$(Variable2)"]}
True
{/}
```

## Scope

Functions support passing *scope* as a value.&#x20;

Scope is a path/reference to a key within the JSON data structure used by ReportGen.

Scope uses a '*relative path*' format, meaning you can traverse up or down the JSON data structure to get to the data you need.

Scope can be accessed as follows:

**"%(pathToScopeItem)"**

For reference on what to use as *pathToScopeItem* - please see examples listed below.

> **!IMPORTANT**: Before using scope, we recommend you to first get comfortable with JSON data structures. This will make it easier for you to understand how to access the data you need.

To access the scope you can use the following [$help](#usdhelp) procedure within your template:

```
{$help[scope]}
```

Place this function within the area of your template where you would like to see more details on the available scope.&#x20;

After that, save your template and then open the ReportGen tool and open the browser console.&#x20;

Run your report and notice that additional diagnostic information relating to your scope is printed in the console.

Example:

```
{#vulnerabilities}
{$help[scope]}
{/}
```

![](/files/i8OxeTefmzlzxs22pcr0)

### **Example 1 - Accessing Current Scope**

```
{#vulnerabilities}
{#affected_assets}
{$value["%(asset)"]}
{/}{/}
```

In the example above, we are printing the value of the asset name using the $value function.

Because we are calling this function within the *affected\_assets* loop - the immediate scope available is any JSON object keys/values that exist within affected\_assets object, for example 'asset' which is the asset name.&#x20;

Therefore we can access the asset name using "%(asset)".

If you wanted to access the JSON object (affected\_asset) instead of the name of the asset, you can use the following:

```
{#vulnerabilities}
{#affected_assets}
{$value["%(./)"]}
{/}{/}
```

### Example 2 - Accessing Parent Scope

```
{#vulnerabilities}
{#affected_assets}
{$value["%(../priority)"]}
{/}{/}
```

In the example above, we are printing the value of the vulnerability priority using the $value function.

Because we are calling this function within the *affected\_assets* loop and the data we need - the priority of the vulnerability - is one-level above in the vulnerability JSON object, we will need to traverse up the scope path to the vulnerability object. We can do this by using ../ syntax which will go up a level within the JSON data structure.&#x20;

Once we are at the vulnerability level, we can access the 'priority' JSON object key. The result is using "%(../priority)" to get the priority.

If you wanted to access the JSON object for the parent (vulnerability) instead of the priority, you can use the following:

```
{#vulnerabilities}
{#affected_assets}
{$value["%(../)"]}
{/}{/}
```

### Example 3 - Accessing Multi-Level Parent Scope

```
{#vulnerabilities}
{#affected_assets}
{$value["%(../../project.name)"]}
{/}{/}
```

In the example above, we are printing the value of the project name using the $value function.

Because we are calling this function within the *affected\_assets* loop and the data we need - the name of the project - is two-levels above in the project JSON object, we will need to traverse up the scope path to the project object. We can do this by using ../../ syntax which will go up two levels within the JSON data structure.&#x20;

Once we are at the project level, we can access the 'name' key within the project JSON object. The result is using "%(../../project.name)" to get the project name.

Notice that the project JSON object has a number of different keys such as "name", "code" and "created". You can use dot (.) syntax to traverse down the JSON data structure, in the same way you would in object-oriented programming - for example project.name or project.code.

### Example 4 - Accessing Arrays

```
{#vulnerabilities}
{#affected_assets}
{$value["%(../../projectCustomFields[1].out_of_scope)"]}
{/}{/}
```

In the example above, we are printing the value of the project custom field 'out\_of\_scope' using the $value function.

Because we are calling this function within the *affected\_assets* loop and the data we need - the project custom field 'out\_of\_scope' - is two-levels above in the projectCustomFields JSON object array, we will need to traverse up the scope path to the projectCustomFields object array. We can do this by using ../../ syntax which will go up two levels within the JSON data structure.&#x20;

Once we are at the projectCustomFields level, note that this key is an *array of objects*. Therefore, we cannot use syntax such as *projectCustomFields.out\_of\_scope* as out\_of\_scope is not a key in the projectCustomFields object array.&#x20;

We will need to instead identify which object in the array has the key we need, then use the index of that object.

```
"projectCustomFields":
    [
        {
            "source_ips": "192.168.0.1"
        },
        {
            "out_of_scope": "URL1, URL2, URL3"
        }
    ],
```

From the example above, the out\_of\_scope object is the second (2nd) index in the array. Because this is a JSON structure, indexes start at 0 and count upwards. Therefore the index we need to use is 1.

The result is using "%(../../projectCustomFields\[1].out\_of\_scope)" to traverse up two (2) levels, select the projectCustomFields object array, select object \[1] within the array, then select out\_of\_scope key.

### **Example 5 - Accessing Array Length**

```
{$value["%(testing_summary.assets.length())"]}
```

In the example above, we are printing the total number of assets in the project scope using the $value function.

Because we are calling this function at the root (top) level - we can immediately access the testing\_summary object which has the data we need - 'assets' key.

Because assets is a *string array*, it has a length. Therefore we can refer to that length using a .length() operation.&#x20;

The result is using "%(testing\_summary.assets.length())" to select the 'testing\_summary' object, select 'assets' string array, and perform a length operation.

## Combining Filters In Functions

You can combine filters in your functions in two ways:

### Example 1: Using a filter inside the function

```
{#vulnerabilities}
{#affected_assets}
{#assetCustomFields}
{$value[“%(af_sys_affected_endpoint) | replace:[“http”,”https”]”]}
{$declare[Custom][“%(af_sys_affected_endpoint) | replace:[“http”,”https”]”]}
{/}{/}{/}

{$value[“$(Custom) | replace:[“com”,”moc”]”]}
```

### Example 2: Chaining a filter to the output of a function

```
{$declare[EasilyExploitableAffectedAssets][[]]}
{#vulnerabilities}
{#affected_assets}
{$push[EasilyExploitableAffectedAssets][“%(./)”]}
{/}{/}
{#$value[EasilyExploitableAffectedAssets] | filter:’easily_exploitable’}
{asset}
{/}
```


# Template - Filters

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

## Sort

You can use this filter to sort the data within a tag by a key within scope (or multiple keys).

```
{tag | sort:["key:<asc/desc>"]}
```

* **key** - JSON object key available in scope.
* **asc** - sort data by key in ascending order
* **desc** - sort data by key in descending order

To observe the keys available within scope for sorting - use the [$help](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#usdhelp) procedure.

### Example 1: Single-Key Sort

You can sort by a single key as follows:

```
{#vulnerabilities | sort:["cvssv3_base_score:asc"]}
{title} - {cvssv3_base_score}
{/}
```

The example above will sort all vulnerabilities by their CVSSv3 Baseline score, in ascending order.

You can also sort affected\_assets by their CVSSv3 Baseline scores:

```
{#affected_assets | sort:["cvssv3_base_score:asc"]}
{name} - {cvssv3_base_score}
{/}
```

### Example 2: Multi-Key Sort

You can sort by multiple keys as follows:

```
{#vulnerabilities | sort:["cvssv3_base_score:asc","title:desc"]}
{title} - {cvssv3_base_score}
{/}
```

The example above will sort all vulnerabilities first by their CVSSv3 Baseline score, in ascending order, then by the vulnerability title, in descending order.

You can also sort affected\_assets by their CVSSv3 Baseline scores and asset name:

```
{#affected_assets | sort:["cvssv3_base_score:asc","name:asc"]}
{name} - {cvssv3_base_score}
{/}
```

## DateFormat

You can use this filter to adjust the date for a tag to a desired format.

```
{tag | dateFormat:["namedFormat/mask"]}
```

For example "fullDate" will return *Saturday, June 9, 2007*

```
{timestamp | dateFormat:["fullDate"]}
```

You can also extend it with a [canonical timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones):

```
{tag | dateFormat:"namedFormat/mask":"timezone"}
```

For example "America/Chicago" will localize the date and time to Chicago.

```
{timestamp | dateFormat:"isoDateTime":"America/Chicago"}
```

The full list of supported options are included below for reference.&#x20;

You can use either a *named format* (for example fullDate, shortTime) or a *mask* (for example h:MM:ss TT Z).

### Named formats

| Name              | Mask                         | Example                  |
| ----------------- | ---------------------------- | ------------------------ |
| `default`         | ddd mmm dd yyyy HH:MM:ss     | Sat Jun 09 2007 17:46:21 |
| `shortDate`       | m/d/yy                       | 6/9/07                   |
| `paddedShortDate` | mm/dd/yyyy                   | 06/09/2007               |
| `mediumDate`      | mmm d, yyyy                  | Jun 9, 2007              |
| `longDate`        | mmmm d, yyyy                 | June 9, 2007             |
| `fullDate`        | dddd, mmmm d, yyyy           | Saturday, June 9, 2007   |
| `shortTime`       | h:MM TT                      | 5:46 PM                  |
| `mediumTime`      | h:MM:ss TT                   | 5:46:21 PM               |
| `longTime`        | h:MM:ss TT Z                 | 5:46:21 PM EST           |
| `isoDate`         | yyyy-mm-dd                   | 2007-06-09               |
| `isoTime`         | HH:MM:ss                     | 17:46:21                 |
| `isoDateTime`     | yyyy-mm-dd'T'HH:MM:sso       | 2007-06-09T17:46:21+0700 |
| `isoUtcDateTime`  | UTC:yyyy-mm-dd'T'HH:MM:ss'Z' | 2007-06-09T22:46:21Z     |

### Mask options

| Mask             | Description                                                                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `d`              | Day of the month as digits; no leading zero for single-digit days.                                                                                            |
| `dd`             | Day of the month as digits; leading zero for single-digit days.                                                                                               |
| `ddd`            | Day of the week as a three-letter abbreviation.                                                                                                               |
| `DDD`            | "Ysd", "Tdy" or "Tmw" if date lies within these three days. Else fall back to ddd.                                                                            |
| `dddd`           | Day of the week as its full name.                                                                                                                             |
| `DDDD`           | "Yesterday", "Today" or "Tomorrow" if date lies within these three days. Else fall back to dddd.                                                              |
| `m`              | Month as digits; no leading zero for single-digit months.                                                                                                     |
| `mm`             | Month as digits; leading zero for single-digit months.                                                                                                        |
| `mmm`            | Month as a three-letter abbreviation.                                                                                                                         |
| `mmmm`           | Month as its full name.                                                                                                                                       |
| `yy`             | Year as last two digits; leading zero for years less than 10.                                                                                                 |
| `yyyy`           | Year represented by four digits.                                                                                                                              |
| `h`              | Hours; no leading zero for single-digit hours (12-hour clock).                                                                                                |
| `hh`             | Hours; leading zero for single-digit hours (12-hour clock).                                                                                                   |
| `H`              | Hours; no leading zero for single-digit hours (24-hour clock).                                                                                                |
| `HH`             | Hours; leading zero for single-digit hours (24-hour clock).                                                                                                   |
| `M`              | Minutes; no leading zero for single-digit minutes.                                                                                                            |
| `MM`             | Minutes; leading zero for single-digit minutes.                                                                                                               |
| `N`              | ISO 8601 numeric representation of the day of the week.                                                                                                       |
| `o`              | GMT/UTC timezone offset, e.g. -0500 or +0230.                                                                                                                 |
| `p`              | GMT/UTC timezone offset, e.g. -05:00 or +02:30.                                                                                                               |
| `s`              | Seconds; no leading zero for single-digit seconds.                                                                                                            |
| `ss`             | Seconds; leading zero for single-digit seconds.                                                                                                               |
| `S`              | The date's ordinal suffix (st, nd, rd, or th). Works well with `d`.                                                                                           |
| `l`              | Milliseconds; gives 3 digits.                                                                                                                                 |
| `L`              | Milliseconds; gives 2 digits.                                                                                                                                 |
| `t`              | Lowercase, single-character time marker string: a or p.                                                                                                       |
| `tt`             | Lowercase, two-character time marker string: am or pm.                                                                                                        |
| `T`              | Uppercase, single-character time marker string: A or P.                                                                                                       |
| `TT`             | Uppercase, two-character time marker string: AM or PM.                                                                                                        |
| `W`              | ISO 8601 week number of the year, e.g. 4, 42                                                                                                                  |
| `WW`             | ISO 8601 week number of the year, leading zero for single-digit, e.g. 04, 42                                                                                  |
| `Z`              | US timezone abbreviation, e.g. EST or MDT. For non-US timezones, the GMT/UTC offset is returned, e.g. GMT-0500                                                |
| `'...'`, `"..."` | Literal character sequence. Surrounding quotes are removed.                                                                                                   |
| `UTC`            | Must be the first four characters of the mask. Converts the date from local time to UTC/GMT/Zulu time before applying the mask. The "UTC:" prefix is removed. |

## HasAny

You can use this filter to check whether at least one value exists in a tag. This can be used to create logic gates or sections within your reports.

This filter accepts the following parameters:

* \[REQUIRED] Values to search - List of strings i.e. \['SQL','xss']
* \[OPTIONAL] Case Insensitive search - true/false. False by default. E.g. hasAny:\['SQL','xss']:true

This filter returns true or false.

The example below combines this filter with the 'Includes' filter to print a list of all CVEs found on the affected asset, along with a section heading 'Associated CVEs'. If no CVEs are found, it will print 'No CVEs Identified'.&#x20;

```
{#vulnerabilities}
Vulnerability: {title}
{#affected_assets}
Affected Asset: {asset}
{#tags | hasAny:['CVE','cve']}Associated CVEs
{#tags}{#this | includes:['CVE','cve']}{.}
{/}{/}{/}
{^tags | hasAny:['CVE','cve']}No CVEs Identified{/}
{/}{/}
```

## Filter

You can use this filter to select objects within a list that match a particular condition.

For example, if you wanted to filter your vulnerabilities by **critical AND easily exploitable** you could use the following:

```
{#vulnerabilities | filter:’easily_exploitable === true AND priority === “Critical”’}
{title}
{/}
```

Another example is filtering affected assets based on **remediation status AND priority.** Note this example applies the filter to the {#affected\_assets} and utilises "parent" to access the priority from the vulnerability.

```
{#vulnerabilities}
{#affected_assets | filter:’remediation_status === "Open" AND parent.priority === “Critical”’}
{title}
{/}{/}
```

Another example is applying the filter against **custom tags**. This example assumes you have defined a custom tag with name/key "owasp\_top\_10" and value of "yes".

```
{#vulnerabilities}
{#affected_assets} 
{#assetCustomTags | filter:’owasp_top_10 === "yes"’}
{title}
{/}{/}{/}
```

Another example is applying the filter against **custom fields**. This example assumes you have defined a custom field with name/key "owasp\_top\_10" and value of "yes".

```
{#vulnerabilities}
{#affected_assets} 
{#assetCustomFields | filter:’owasp_top_10 === "yes"’}
{title}
{/}{/}{/}
```

Another example is applying the filter combined with **functions**. This example will create a new list of all OWASP Top 10 vulnerabilities that can be referenced anywhere further in the report. This example assumes you have defined a custom tag with name/key "owasp\_top\_10" and value of "yes".

Example of pushing into new list using *parent* object&#x73;*:*

```
{$declare[OWASP_Top_10_Vulns][[]]}
{#vulnerabilities}
{#affected_assets} 
{#assetCustomTags | filter:’owasp_top_10 === "yes"’}
{$push[OWASP_Top_10_Vulns][“%(parent.parent)”]}
{/}{/}{/}
...
{#$value[OWASP_Top_10_Vulns]}
{title}
{/}
```

Example of pushing into new list using *scope:*

```
{$declare[OWASP_Top_10_Vulns][[]]}
{#vulnerabilities}
{#affected_assets} 
{#assetCustomTags | filter:’owasp_top_10 === "yes"’}
{$push[OWASP_Top_10_Vulns][“%(../../)”]}
{/}{/}{/}
...
{#$value[OWASP_Top_10_Vulns]}
{title}
{/}
```

The following operators are supported when using this filter:

* **NOT** or **!** - used to negate an expression. For example *!(priority == "Critical")*
* **AND** or **&&** - used to *and* multiple expressions. For example *priority == "Critical" AND* zero\_day *== true*
* **OR** or **||** - used to *or* multiple expressions. For example *priority == "Critical" OR priority == "High"*
* **==** - used to check for equivalency. For example *priority == "Critical"*
* **===** - used to check for equality. For example *priority === "Critical"*
* **!==** - used to check for not equivalency. For example *priority !== "Critical"*
* **>** - used to check for greater-than comparison. For example *likelihood\_of\_exploitation > 5*
* **<** - used to check for less-than comparison. For example *likelihood\_of\_exploitation < 5*
* **>**= - used to check for greater-than-or-equals comparison. For example *likelihood\_of\_exploitation >= 5*
* **<=** - used to check for less-than-or-equals comparison. For example *likelihood\_of\_exploitation <= 5*
* **( )** - used to group statements together. For example (priority == "Critical") AND (zero\_day *== true*) OR ((priority == "Critical") AND (*likelihood\_of\_exploitation >= 8*))

## HasAll

You can use this filter to check whether all supplied values exist in a tag. This can be used to create logic gates or sections within your reports.

This filter accepts the following parameters:

* \[REQUIRED] Values to search - List of strings i.e. \['SQL','xss']
* \[OPTIONAL] Case Insensitive search - true/false. False by default. E.g. hasAny:\['SQL','xss']:true

This filter returns true or false.

The example prints 'OWASP Top 10 & CWE Top 25' for all affected assets which have both tags 'OWASP Top 10' & 'CWE Top 25'. If both matching tags are not found, it will print 'Not OWASP Top 10 & CWE Top 25'.

```
{#vulnerabilities}
Vulnerability: {title}
{#affected_assets}
Affected Asset: {asset}
{#tags | hasAll:['OWASP Top 10','CWE Top 25']}OWASP Top 10 & CWE Top 25{/}
{^tags | hasAll:['OWASP Top 10','CWE Top 25']}Not OWASP Top 10 & CWE Top 25{/}
{/}{/}
```

## **FilterBy**

You can use this filter in order to extract filtered data for vulnerabilities using various conditions.&#x20;

Currently the following conditions are supported:&#x20;

* **filterBy:'AffectedAssetProperties'**
* **filterBy:'AffectedAssetCustomTags'**
* **filterBy:'AffectedAssetCustomFields'**
* **filterBy:'AffectedAssetCustomTags-CountVulns'**
* **filterBy:'AffectedAssetCustomFields-CountVulns'**

#### **!IMPORTANT:** CustomFields are used in the exact same way as CustomTags within this filter.

**filterBy:'AffectedAssetProperties'**

This filter can be used to retrieve a set of vulnerabilities where the affected assets meet certain conditions.

For example, you can return a list of vulnerabilities and their affected assets which are closed.

```
{#vulnerabilities | filterBy:'AffectedAssetProperties':['status:Closed']}
{priority} - {title}
{#affected_assets}
{asset}
{/}{/}
```

Or you can extend the filter to match multiple AND or OR conditions. For example, you can return a list of vulnerabilities and their affected assets which are either open or ready for retest.

```
{#vulnerabilities | filterBy:’AffectedAssetProperties’:[‘status:Open’,’status:Retest’]:’OR’}
{priority} - {title}
{#affected_assets}
{asset}
{/}{/}
```

This filter works with any key:value pair on affected\_assets. If you are unsure which properties you can use this filter on, try using the Helper function to see which fields are available to you.

```
{#vulnerabilities}
{#affected_assets}
{$help["%()"]}
{/}{/}
```

<figure><img src="/files/05kIclIVoGA7HgjxCiPa" alt=""><figcaption></figcaption></figure>

#### **filterBy:'AffectedAssetCustomTags'**

This filter can be used to retrieve a list of vulnerabilities which have affected assets that meet conditions in their custom tags.

The following example will return a list of vulnerabilities which have affected assets that have at least one custom tag that is set to *Source = External*. This is useful for reporting on External Vulnerabilities in your report.

```
{#vulnerabilities | filterBy:'AffectedAssetCustomTags':['Source:External']}
{priority} - {title}
{#affected_assets}
{asset}
{/}{/}
```

* **{#vulnerabilities | filterBy:'AffectedAssetCustomTags':\['Source:External']}**&#x20;
  * Loop through vulnerabilities.
  * Apply filterBy filter with following parameters:
    * AffectedAssetCustomTags - this instructs the filter to use this condition
    * \['Source:External'] - this instructs the filter to only return vulnerabilities and their affected assets which specifically have a custom tag which equals *Source = External*.
* **{priority} - {title}**
  * Print priority and title of vulnerability which meets the filter.
* **{#affected\_assets}**
  * Loop through affected assets on the vulnerability.
* **{asset}**
  * Print name of the affected asset.

This filter supports an array of custom tags when inputting conditions, as well as AND and OR operators.&#x20;

For example, using an AND operator with multiple custom tag conditions:

```
{#criticalVulnerabilities | filterBy:'AffectedAssetCustomTags':['Source:External','OWASPTop10:True']:'AND'}
{priority} - {title}
{#affected_assets}
{asset}
{/}{/}
```

This will return a list of critical vulnerabilities which have affected assets that have **both** custom tags **Source = External** and **OWASPTop10 = True**.

You can also omit the AND operator, as this filter uses AND condition by default.

For example, using an OR operator with multiple custom tag conditions:

```
{#criticalVulnerabilities | filterBy:'AffectedAssetCustomTags':['Source:External','OWASPTop10:True']:'OR'}
{priority} - {title}
{#affected_assets}
{asset}
{/}{/}
```

This will return a list of critical vulnerabilities which have affected assets that have **either** custom tags **Source = External** or **OWASPTop10 = True**.

#### **filterBy:'**&#x41;ffectedAssetCustomTags-CountVuln&#x73;**'**

This filter can be used to retrieve a count of vulnerabilities which have affected assets that meet conditions in their custom tags.

The following example will return a count of vulnerabilities which have affected assets that have at least one custom tag that is set to *Source = External*. This is useful for reporting on total number of External Vulnerabilities in your report.

```
{vulnerabilities | filterBy:'AffectedAssetCustomTags-CountVulns':['Source:External']}

```

* **{vulnerabilities | filterBy:'AffectedAssetCustomTags-CountVulns':\['Source:External']}**&#x20;
  * Apply filterBy filter with following parameters:
    * AffectedAssetCustomTags-CountVulns - this instructs the filter to use this condition
    * \['Source:External'] - this instructs the filter to only count vulnerabilities where their affected assets specifically have a custom tag which equals *Source = External*.

This filter supports an array of custom tags when inputting conditions, as well as AND and OR operators.&#x20;

For example, using an AND operator with multiple custom tag conditions:

```
{criticalVulnerabilities | filterBy:'AffectedAssetCustomTags':['Source:External','OWASPTop10:True']:'AND'}
```

This will return a count of critical vulnerabilities which have affected assets that have **both** custom tags **Source = External** and **OWASPTop10 = True**.

You can also omit the AND operator, as this filter uses AND condition by default.

For example, using an OR operator with multiple custom tag conditions:

```
{criticalVulnerabilities | filterBy:'AffectedAssetCustomTags':['Source:External','OWASPTop10:True']:'OR'}
```

This will return a count of critical vulnerabilities which have affected assets that have **either** custom tags **Source = External** or **OWASPTop10 = True**.

#### **Example - using Scope or Variables in your filter**

FilterBy supports the use of Scope or Variables in your filter criteria. For example, you can search a custom field without knowing the exact criteria you are search for.

The example below will loop over every 'Testing Phase' in the project custom fields, then filter the vulnerabilities by each testing phase, then print the 'Testing Phase' and it's vulnerabilities at the end. This is ideal if you have multi-phase projects and you want to print a section which has vulnerabilities for each phase.

```
{$declare[TestingPhaseVulns][<Dictionary>]}
{#projectCustomFields}
{#testing_phases}
{$declare[CurrentPhase][“%(./)”]}
{$declare[TestingPhaseVulns[“$(CurrentPhase)”]][[]]}
{#vulnerabilities | filterBy:“AffectedAssetCustomFields”:[“testing_phase:$(CurrentPhase)”]}
{$push[TestingPhaseVulns[“$(CurrentPhase)”]][“%(./)”]}
{/}{/}{/}
{#$keys[TestingPhaseVulns]}
Vulnerabilities for Testing Phase: {this[0]}
{#this[1]}
Vulnerability Title: {title}
{/}{/}
```

## Array\_chunk

This filter can be used to create an array of elements split into groups the length of `size`. If the array can't be split evenly, the final chunk will be the remaining elements. Each array is then accessible inside the parent loop via `chunk[index]`.

```
array_chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
 
array_chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]

{#tag | array_chunk:<size>}{chunk[0]}{chunk[1]}{chunk[...]}{/}
```

For example, say you wanted to distribute your project scope assets across three (3) columns to help save whitespace in the report - you could do the following:

<figure><img src="/files/KAH5TGNhPfZE3XDrpJ75" alt=""><figcaption></figcaption></figure>

The result will be the distribution of each asset across three columns:

<figure><img src="/files/KDvRk4frChQGgmFMRlyC" alt=""><figcaption></figcaption></figure>

For another example, say you wanted to distribute your vulnerability titles across two (2) columns - you could do the following:

<figure><img src="/files/KkDHSwkMteEx2VbdLLQg" alt=""><figcaption></figcaption></figure>

The result will be the distribution of each vulnerability title across two columns:

<figure><img src="/files/NLoF3Fgi0asAsy7Ih0Vd" alt=""><figcaption></figcaption></figure>

## **Includes**

You can check to see if a tag contains a specified value, or array of values, and continue if true/exists.

```
{#vulnerabilities}
{#title | includes:['SQL Injection','Cross Site Scripting']}
{priority} - {title}
{/}{/}
```

* **{#vulnerabilities}**&#x20;
  * Loop through vulnerabilities.
* **{#title | includes:\['SQL Injection','Cross Site Scripting']}**&#x20;
  * Check to see if the title of the vulnerability contains the values "SQL Injection" or "Cross Site Scripting", and if so proceed. Otherwise stop. This filter also includes partial matches e.g. "Blind SQL Injection" would also return true.
* **{priority} - {title}**
  * Print priority and title of vulnerability assuming that it includes or partially includes&#x20;

    "SQL Injection" or "Cross Site Scripting" in the title.

You can also use this filter on string arrays (lists) - for example if you want to only show CVE tags for a vulnerability you can use the following:

```
{#vulnerabilities}
{title}
{#tags}{#this | includes:['CVE','cve']}CVE: {.}
{/}{/}
{/}
```

The template above will print the title of each vulnerability, and loop through each vulnerability checking if the tag contains 'CVE' or 'cve' anywhere within the tag, and if so, it will print it.

Includes also supports [Scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope) and [Variables](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#variables).

**Scope Example:**

```
{#projectCustomFields}
{#customer_name}
{#projectGroups}
{#name | includes:[“%(../customer_name)”]}
Customer {customer_name} has access to this project.
{/}{/}{/}{/}
```

**Variables Example:**

```
{$declare[CustomerName][“”]}
{#projectCustomFields}
{#customer_name}
{$assign[CustomerName][“%(customer_name)”]}
{/}{/}

{#projectGroups}
{#name | includes:[“$(CustomerName)”]}
Customer {$value[CustomerName]} has access to this project.
{/}{/}
```

## **Excludes**

You can check to see if a tag does not contain a specified value, or array of values, and continue if true/doesn't exist.

```
{#vulnerabilities}
{#title | excludes:['SQL Injection','Cross Site Scripting']}
{priority} - {title}
{/}{/}
```

* **{#vulnerabilities}**&#x20;
  * Loop through vulnerabilities.
* **{#title | excludes:\['SQL Injection','Cross Site Scripting']}**&#x20;
  * Check to see if the title of the vulnerability does not contain the values "SQL Injection" or "Cross Site Scripting", and if so proceed. Otherwise stop. This filter also includes partial matches e.g. "Blind SQL Injection" would also return true.
* **{priority} - {title}**
  * Print priority and title of vulnerability assuming that it does not include or partially include&#x20;

    "SQL Injection" or "Cross Site Scripting" in the title.

Excludes also supports [Scope](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#scope) and [Variables](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions#variables).

## **Replace**

You can use the replace filter to replace data.&#x20;

For example, say you had some assets that looked like this:

***<https://application.com>***

And you wanted to remove the https\:// part so it appears as follows:

***application.com***

You could do the following:

```
{#vulnerabilities}
{#affected_assets}
{asset | replace:[“https://”,“”]}
{/}{/}
```

## **ReplaceRegExp**

You can use the replace filter with a regular expression to replace data.&#x20;

For example, say you had some assets that looked like this:

***<https://application.com?query=1>***

And you wanted to remove the ?query=1 part so it appears as follows:

***<https://application.com>***

You could do the following:

```
{#vulnerabilities}
{#affected_assets}
{asset | replaceRegExp:[“[?].*”,“”]}
{/}{/}
```

## **Resize**

You can use the resize filter to resize images. It works by setting a fixed width in pixels for which each image will be upscaled or downscaled to match. The height will be automatically adjusted to match the same ratio.

Example resizing images in Steps to Reproduce (Proof of Concept) to 300 pixels wide:

```
{#vulnerabilities}
{#affected_assets}
{@proof_of_concept_styled | resize:[“300”]}
{/}{/}
```

## **Split**

You can use the split function to split data based on a separator - and return a list of items.

For example, assuming you have tags in this format:

\<LHS>:\<RHS>

And you only want to show the right-hand-side (RHS), you could do the following:

```
{#vulnerabilities}
{#tags}
{$declare[tagSplitCounter][0]}
{#this | split:[“:”]}
{#$greaterThan[tagSplitCounter][0]}{.}{/}
{#$equals[tagSplitCounter][0]}{$increment[tagSplitCounter][1]}{/}
{/}{/}{/}
```

This example makes use of the split filter as well as functions. Functions are used as a way to instruct the logic to skip over the first iteration of the loop (LHS) and then print everything after that \<RHS>.&#x20;

## **SplitRegExp**

You can use the splitRegExp function to split data based on a Regular Expression separator - and return a list of items.

For example, if you wanted to only display the 1st paragraph of the vulnerability description, you could do the following:

```
{#vulnerabilities}
{title}
{$declare[SplitDecriptionOnParagraph][1]}
{#description | splitRegExp:[“\n\n“]}
{#$lessThanOrEqual[SplitDecriptionOnParagraph][1]}{.}{/}{$increment[SplitDecriptionOnParagraph][1]}
{/}{/}
```

This example works as follows:

* Loop through every vulnerability
  * Print the title of the vulnerability
  * Define a variable "SplitDecriptionOnParagraph" which determines how many paragraphs to print, e.g. 1 (only first paragraph)
  * Split description on double-newline ‘\n\n’ which indicates new paragraph. This creates a list/array of paragraphs.&#x20;
  * Loop through each paragraph
    * If variable is less than or equal to the amount of paragraphs we want to keep (e.g. 1) – print it.
    * For each iteration of the loop, increment the variable counter

## **Index**

You can use this filter to access an item in an array using its index number.

For example, if you needed to access the first vulnerability in a list - you can do the following:

```
{#vulnerabilities | index:["0"]}
{title}
{/}
```

## **Search**

You can use this filter to search for a value in a string, and return the results (substring) if found.

For example, if you had asset names which looked like the following:

* <https://application.com/route/api/1>
* <http://application.com/route/api/2>

And you wanted to only return the following:

* /route/api/1
* /route/api/2

You can do the following:

```
{#vulnerabilities}
{#affected_assets}
{asset | replace:["https://",""] | replace:["http://",""] | search:["/"]}
{/}{/}
```

The example above works by first removing the https\:// and http\:// part of the asset, then searching for the first forward slash character, then returning the remaining data if found.

## **Declare**

You can declare a new variable, and then refer to that variable using [Functions](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions).

```
{timestamp | dateAdd:"3 months" | declare:"reportValidToDate"}
{$value[reportValidToDate]}
```

## **Assign**

You can assign a new value to a variable, and then refer to that variable using [Functions](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions).

```
{totalVulns | multiply:100 | assign:"totalVulnsMultiplied" | drop}
```

## **Store**

You can store custom data in arbitrarily defined tags using this filter.&#x20;

For example we can create a new custom tag called 'AllVulns' and reference it, along with its data, later in the template as follows:

```
{#vulnerabilities}{#title | store:’AllVulns’:this}{/}{/}
{#$storedAllVulns}
{priority} {title}
{/}
```

* **{#vulnerabilities}{#title | store:’AllVulns’:this}{/}{/}**&#x20;
  * Loop through vulnerabilities.
  * Define a new custom tag called 'AllVulns'
  * Store the value of *this* in the new custom tag. In the context of {#vulnerabilities} - *this* will be equal to the vulnerability &#x20;
* **{#$storedAllVulns}**
  * Loop through the new custom tag we created above called AllVulns
  * You must include *#$stored* prefix in order to use this new custom tag
* **{priority} {title}**&#x20;
  * Print priority & title of vulnerability.

A more complex example includes how to create a custom tag that will hold all of the **Critical Web Application vulnerabilities** found on the project. This includes using ReportGen custom tags for *affected\_assets*:

```
{#vulnerabilities}
{#priority == “Critical”}
{#affected_assets}
{#assetCustomTags}
{#Source == “Web”}
{#title | store:’CriticalWebVulns’:this:’affected_assets’:’assetCustomTags’:[‘Source:Web’]}

{/}{/}{/}{/}{/}{/}
{#$storedCriticalWebVulns}
{priority} {title}
{/}

```

* **{#vulnerabilities}**
  * Loop through vulnerabilities.
* **{#priority == “Critical”}**
  * Check if vulnerability priority is equal to Critical, then proceed
  * This check is used to ascertain the vulnerability is a Critical vulnerability
* **{#affected\_assets}**
  * Loop through affected assets on the vulnerability
* **{#assetCustomTags}**
  * Loop through custom ReportGen tags on the affected asset
* **{#Source == "Web"}**
  * Check if custom ReportGen tag has key/name "Source" and a value "Web"
  * This check is used to ascertain the vulnerability is a Web Application vulnerability
* **{#title | store:’CriticalWebVulns’:this:’affected\_assets’:’assetCustomTags’:\[‘Source:Web’]}**
  * Define a new custom tag called 'CriticalWebVulns'
  * Store the value of *this* which in this case is the vulnerability itself
  * Check if any of the custom affected asset tags contain any of the key/value pairs supplied, in this case ‘Source:Web’ – note this is an array, you can add more values, it is an OR operator &#x20;
* **{/}  {/}  {/}  {/}  {/}  {/}**
  * Close all of the open loops (see above)
* **{#$storedCriticalWebVulns}**
  * Loop through the new custom tag we created above called CriticalWebVulns. This custom tag now contains Critical vulnerabilities which have affected assets which have custom tags indicating they are web application vulnerabilities
  * You must include *#$stored* prefix in order to use this new custom tag
* **{priority} {title}**&#x20;
  * Print priority & title of vulnerability.

## **Find**

You can search a tag which contains an array of objects to return an object which meets a specific condition

```
{#vulnerabilities | find:"title":"Missing X-XSS-Protection Header"}
{title}
{/}
```

* **{#vulnerabilities | find:"title":"Missing X-XSS-Protection Header"}**&#x20;
  * Loop through vulnerabilities and search each one until it finds a vulnerability with a title equal to Missing X-XSS-Protection Header, then return the vulnerability.
* **{title}**&#x20;
  * Print title of vulnerability, in this case it would be "Missing X-XSS-Protection Header" as that is the vulnerability which was returned from the list of vulnerabilities.

## **FindVulns**

You can use this filter to find a vulnerability based on a Title & Priority.

For example, if you wanted to create a report which shows all Assets on the project, and their Vulnerabilities (**Asset to Vulnerability table or mapping**) - you can achieve that using this filter. Note this requires using the *store* filter in combination with *findVulns* filter as follows:

```
{#vulnerabilities}{#title | store:’allVulns’:this}{/}{/}
{#assetVulnerabilityMapping}
{asset}
{#vulnerabilities}
{priority} - {status} - {vulnerability}
{#vulnerabilities | findVuln:’allVulns’:this.vulnerability:this.priority}
{description}
{attack_scenario}
{remediation_recommendation}

{/}{/}{/}
```

* **{#vulnerabilities}{#title | store:’allVulns’:this}{/}{/}**
  * Loop through vulnerabilities.
  * Define a new custom tag called 'allVulns'
  * Store the value of *this* which in this case is the vulnerability itself
* **{#assetVulnerabilityMapping}**
  * Loop through Asset-to-Vulnerability mapping
* **{asset}**
  * Print the name of the asset
* **{#vulnerabilities}**
  * Loop through vulnerabilities linked to the Asset
  * NOTE: This is different to {#vulnerabilities} referenced at line 1 above.
* **{priority} - {status} - {vulnerability}**
  * Print the priority, remediation status & name of the vulnerability
* **{#vulnerabilities | findVuln:’allVulns’:this.vulnerability:this.priority}**
  * Find the vulnerability (from list of all project vulnerabilities) where it matches the name/title & priority of the current vulnerability linked to the asset
* **{description}**
  * Print the description of the vulnerability linked to the asset
* **{attack\_scenario}**
  * Print the attack scenario of the vulnerability linked to the asset
* **{remediation\_recommendation}**
  * Print the remediation recommendation of the vulnerability linked to the asset

## **Unique**

You can use a 'unique' filter to check if a value has already been printed in the report, and if so, it will skip printing it again.

```
{#vulnerabilities}
{#priority == “Critical”}
{#affected_assets}
{#assetCustomTags}
{#Source == “Internal”}
{#title | unique:'InternalVulnsTable'}
{title}
{/}{/}{/}{/}{/}{/}
```

* **{#vulnerabilities}**&#x20;
  * Loop through vulnerabilities.
* **{#priority == “Critical”}**
  * Select vulnerabilities which have a priority of ‘Critical’.
* **{#affected\_assets}**
  * Loop through Affected Assets for each Critical vulnerability.
* **{#assetCustomTags}**&#x20;
  * Loop through Custom Tags for each Affected Asset for each Critical vulnerability.
* **{#Source == “Internal”}**&#x20;
  * Check to see whether a Custom Tag ‘Source’ exists, and if so check to see if it’s value is “Internal”.
* **{#title | unique:'InternalVulnsTable'}**&#x20;
  * This is a condition against the ‘title’ field for a vulnerability
  * This condition will check to see whether the value is unique (i.e. it hasn’t already been used/printed). This is useful to prevent printing duplicate values when looping through affected assets, for example vulnerability title.
  * This function works by taking 2 arguments – Key & Value. In the example above, Key = {title} e.g. Blind SQL Injection; and Value = ‘InternalVulnsTable’
  * This function will check to see if the Key/Value pair has already been printed in the report, and if so, it will skip printing it again – for example to avoid duplicating printing of vulnerability title for each affected asset in a table containing list of Internal Vulnerabilities.
* **{title}**&#x20;
  * Print title of vulnerability.

## **Count**

You can use a 'count' filter to set an arbitrary counter for a condition, then reference that counter later on.

```
{#vulnerabilities}
{#priority == “Critical”}
{#affected_assets}
{#assetCustomTags}
{#Source == “Internal”}
{#title | unique:'InternalVulnsTable' | count:'InternalVulnsTableCritical'}
{title}
{/}{/}{/}{/}{/}{/}
{#$countInternalVulnsTableCritical}
{$countInternalVulnsTableCritical}
{/}
```

* **{#vulnerabilities}**&#x20;
  * Loop through vulnerabilities.
* **{#priority == “Critical”}**
  * Select vulnerabilities which have a priority of ‘Critical’.
* **{#affected\_assets}**
  * Loop through Affected Assets for each Critical vulnerability.
* **{#assetCustomTags}**&#x20;
  * Loop through Custom Tags for each Affected Asset for each Critical vulnerability.
* **{#Source == “Internal”}**&#x20;
  * Check to see whether a Custom Tag ‘Source’ exists, and if so check to see if it’s value is “Internal”.
* **{#title | unique:'InternalVulnsTable' | count:'InternalVulnsTableCritical'}**
  * We are chaining together the 'unique' filter with the 'count' filter against the ‘title’ field for a vulnerability
  * For details on how the 'unique' filter works - see above.
  * Count filter works by taking 2 arguments – Key & Value. In the example above, Key = {title} e.g. Blind SQL Injection; and Value = ‘InternalVulnsTableCritical’
  * This function will count the number of times it is executed and store the result in a tag called $countVALUE where VALUE = ‘InternalVulnsTableCritical’
  * Because we are chaining this filter with another filter - in this case the 'unique' filter - the unique filter condition must be met first before this function executes and counter is incremented.
  * For example, if the dataset had 3 unique Critical vulnerabilities - $countInternalVulnsTableCritical will be equal to 3.
* **{title}**&#x20;
  * Print title of vulnerability.
* **{#$countInternalVulnsTableCritical}**
  * Access the new dynamic tag '$countInternalVulnsTableCritical' created when we ran count:'InternalVulnsTableCritical'
* **{$countInternalVulnsTableCritical}**
  * Print the value of the counter for 'InternalVulnsTableCritical'

## IsArray

You can check if a value is an array:

```
{tags | isArray}
```

## IsBoolean

You can check if a value is a boolean:

```
{easily_exploitable | isBoolean}
```

## IsInteger

You can check if a value is an integer:

```
{totalVulns | isInteger}
```

## IsString

You can check if a value is a string:

```
{projectName | isString}
```

## Trim

You can trim the whitespace before and after a tag using the following filter:

```
{title | trim}
```

## Substring

You can return a substring from a string. This filter takes two parameters:

* start index
* (optional) end index

If an end index is not provided, the substring will return from the start index until the end of the string.

If the substring cannot be determined, the original string is returned.

{% code overflow="wrap" %}

```
{projectName | substring:[3]}
{projectName | substring:[3,12]}
{projectName | substring:[0,12]}
{!! If project name is more than 12 characters long, return the first 12 characters then add ... after the end of the substring }
{#projectName.length > 12}
{projectName | substring:[0,12]}...
{/}
```

{% endcode %}

## Float

You can convert a number to a floating point number. Decimal number e.g. '2' is optional and defaults to 2 if no option is provided. If the conversion doesn’t work it will return 0.0.

```
{cvssv3_base_score | toFloat:'2'}
```

## Integer

You can convert a number to an integer. If the conversion doesn’t work it will return 0.

```
{cvssv3_base_score | integer}
```

## Round

You can round a number to the nearest integer.

```
{cvssv3_base_score | round}
```

## RoundUp

You can round a number up to the nearest integer.

```
{cvssv3_base_score | roundUp}
```

## RoundDown

You can round a number down to the nearest integer.

```
{cvssv3_base_score | roundDown}
```

## Increment

You can increment a number by 1 or a specified integer.

```
{totalVulns | increment}
{totalVulns | increment:25}
```

## Multiply

You can multiply a number by a specified integer.

```
{totalVulns | multiply:5}
```

## DateAdd

You can add units of time to a date.

```
{timestamp | dateAdd:"10 milliseconds"}
{timestamp | dateAdd:"10 seconds"}
{timestamp | dateAdd:"10 minutes"}
{timestamp | dateAdd:"10 hours"}
{timestamp | dateAdd:"10 days"}
{timestamp | dateAdd:"10 weeks"}
{timestamp | dateAdd:"10 months"}
{timestamp | dateAdd:"10 years"}
{timestamp | dateAdd:"$(someDateVariable)"}
```

## DateSubtract

You can subtract units of time to a date.

```
{timestamp | dateSubtract:"10 milliseconds"}
{timestamp | dateSubtract:"10 seconds"}
{timestamp | dateSubtract:"10 minutes"}
{timestamp | dateSubtract:"10 hours"}
{timestamp | dateSubtract:"10 days"}
{timestamp | dateSubtract:"10 weeks"}
{timestamp | dateSubtract:"10 months"}
{timestamp | dateSubtract:"10 years"}
{timestamp | dateSubtract:"$(someDateVariable)"}
```

## DateDiff

You can diff the time between dates. The default response is in milliseconds.

```
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z"}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":milliseconds}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":seconds}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":minutes}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":hours}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":days}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":weeks}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":months}
{timestamp | dateDiff:"2026-02-27T04:28:47.515Z":years}
{timestamp | dateDiff:"%(data.project.created)":days}
{timestamp | dateDiff:"$(someDateVariable)":days}
```

## Capitalize

You can capitalize the tag. The first character will be uppercase, all others lowercase.

```
{title | capitalize}
```

## Titlecase

You can titlecase the tag. Words will start with uppercase letters, all remaining characters are lowercase.

```
{title | toTitleCase}
```

## Uppercase

You can uppercase a tag:

```
{title | toUpperCase}
```

## Lowercase

You can lowercase a tag:

```
{title | toLowerCase}
```

## Drop

You can use drop to prevent a value from showing.

```
{totalVulns | multiply:100 | assign:"totalVulnsMultiplied" | drop}
```


# Template - Styles

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

You can also set default styles in your template which will apply when report is generated.&#x20;

![](/files/53GQyqgJU2qYDI3DkU4I)

* **AF Heading 1**
  * Style for WYSIWYG editor Heading 1
* **AF Heading 2**
  * Style for WYSIWYG editor Heading 2
* **AF Heading 3**
  * Style for WYSIWYG editor Heading 3
* **AF Code Snippet**
  * Style for WYSIWYG editor PRE
* **AF Inline Code**
  * Style for WYSIWYG editor Code
* **AF List**
  * Style for WYSIWYG editor lists (ordered and unordered)
* **AF Images**
  * Style for images and their captions
* **AF Normal**
  * Style for paragraph text
* **AF BlockQuote**
  * Style for blockquotes

## Custom Styles for Rich-Text Fields

You can assign rich-text fields to a custom style that already exists in your template.

{% code overflow="wrap" %}

```
{@proof_of_concept_styled("normal_style":"VulnerabilityNormal", "list_style":"VulnerabilityList")}
```

{% endcode %}

The following style references are supported:

* **normal\_style** - applies to normal body text
* **list\_style** - applies to lists
* **code\_snippet\_style** - applies to code snippets
* **inline\_code\_snippet\_style** - applies to inline code snippets. Must reference a [Character Style](#microsoft-word-character-styles).
* **heading\_1\_style** - applies to heading 1
* **heading\_2\_style** - applies to heading 2
* **heading\_3\_style** - applies to heading 3
* **image\_style** - applies to images and their captions
* **image\_display\_style** - applies to the images only. Must reference a [Character Style](#microsoft-word-character-styles).
* **image\_description\_style** - applies to the image descriptions/captions only. Must reference a [Character Style](#microsoft-word-character-styles).
* **hyperlink\_style** - applies to hyperlinks. Must reference a [Character Style](#microsoft-word-character-styles).
* **blockquote\_style** - applies to blockquotes

Start by creating the custom style in your template - **do not** include any whitespace in the style name.

<figure><img src="/files/JE6lRoV4ODsI9yE0srYe" alt=""><figcaption></figcaption></figure>

Update your tag options to reference the custom style:

<figure><img src="/files/1IVu1zB9cbJIE8tBBRYM" alt=""><figcaption></figcaption></figure>

When the report is generated, the style will be automatically referenced:

<figure><img src="/files/EQPlmIDSfspMOmHCOXCp" alt=""><figcaption></figcaption></figure>

## Toggle Image Description (Filename/Caption)

This option can be used to adjust how the filename or caption is displayed under an image. This option can be set against any {@...\_styled} tags where ... is the name of a tag.&#x20;

```
{@..._styled(“image_description”:“caption”)}
{@..._styled(“image_description”:“prefer-caption”)}
{@..._styled(“image_description”:“filename”)}
{@..._styled(“image_description”:“none”)}
```

* **image\_description: caption** - will display the caption if it exists, otherwise will display nothing.
* **image\_description: prefer-caption** - will display the caption if it exists, otherwise will display filename.
* **image\_description: filename** - will display the filename.
* **image\_description: none** - will display no caption or filename.

## Toggle Image Figure

This option can be used to disable the figure under an image. This option can be set against any {@...\_styled} tags where ... is the name of a tag.&#x20;

```
{@..._styled(“image_figure”:“none”)}
```

* **image\_figure: none** - will not display the figure.

## Table Styling

These option can be used to style rich-text tables. These options can be set against any {@...\_styled} tags where ... is the name of a tag.&#x20;

{% code overflow="wrap" %}

```
{@..._styled(“table_alignment”:”center”,”table_columns_width”:”5000”,”table_rows_split_across_pages”:”true”,”table_borders_thickness”:”30”,”table_borders_color”:”ff5733”,”table_cells_margin_top”:”1”,”table_cells_margin_bottom”:”1”,”table_cells_margin_left”:”1”,”table_cells_margin_right”:”1”,”table_cells_borders_thickness”:”10”,”table_cells_borders_color”:”6c3483”,”table_rows_header_style”:”CustomTableHeader”,”table_rows_header_background_color”:”00B050”,”table_rows_header_height”:”600”,”table_rows_header_vertical_alignment”:”center”,”table_rows_header_horizontal_alignment”:”center”,”table_rows_body_style”:”CustomTableBody”,”table_rows_body_vertical_alignment”:”center”,”table_rows_body_horizontal_alignment”:”center”,”table_rows_body_background_color”:”c7fcc5”,”table_rows_body_height”:”1000”)}
```

{% endcode %}

* **table\_alignment** - left/center/right
* **table\_columns\_width** - pixels e.g. 5000
* **table\_rows\_split\_across\_pages** - true/false
* **table\_borders\_thickness** - pixels e.g. 30
* **table\_borders\_color** - hex color code e.g. 000000
* **table\_cells\_margin\_top** - pixels e.g. 1
* **table\_cells\_margin\_bottom** - pixels e.g. 1
* **table\_cells\_margin\_left** - pixels e.g. 1
* **table\_cells\_margin\_right** - pixels e.g. 1
* **table\_cells\_borders\_thickness** - pixels e.g. 10
* **table\_cells\_borders\_color** - hex color code e.g. 000000
* **table\_rows\_header\_style** - maps to a Word style (don't put whitespace in the style name)
* **table\_rows\_header\_background\_color** - hex color code e.g. 000000
* **table\_rows\_header\_height** - pixels e.g. 600
* **table\_rows\_header\_vertical\_alignment** - top/center/bottom
* **table\_rows\_header\_horizontal\_alignment** - left/center/right
* **table\_rows\_body\_style** - maps to a Word style (don't put whitespace in the style name)
* **table\_rows\_body\_vertical\_alignment** - top/center/bottom
* **table\_rows\_body\_horizontal\_alignment** - left/center/right
* **table\_rows\_body\_background\_color** - hex color code e.g. 000000
* **table\_rows\_body\_height** - pixels e.g. 1000

## Microsoft Word Character Styles

Character styles are used to apply to sections within a paragraph.

The following custom styles only support Character styles:

* **image\_display\_style** - applies to the images only.
* **image\_description\_style** - applies to the image descriptions/captions only.
* **hyperlink\_style** - applies to hyperlinks.

To create a Character style in Microsoft Word, start by creating the custom style in your template - **do not** include any whitespace in the style name.

***IMPORTANT! Make sure you select 'Character' for the Style type. If you select any other option - it will not work.***

<figure><img src="/files/00P4FcpEAtvPesB11MhH" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/qlHqVm8f6RDuP1A1iH2m" alt=""><figcaption></figcaption></figure>

The example above, combined with the code snippet below, will produce the following:

```
{#vulnerabilities}
{#affected_assets}
{@proof_of_concept_styled(“image_display_style”:”BoxedImage”,“image_description”:“caption”) | resize:[“300”]}
{/}{/}
```

<figure><img src="/files/T3mBzsyN5YpIVFeqR87f" alt=""><figcaption></figcaption></figure>


# Template - Tables

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

You can add tables with repeating rows as follows:

![](/files/WgNawwWVwrtvyEeoJEco)

The output in your report will appear as follows:

![](/files/De7Hic0CS9CTYoomt0S4)

## Adding Logic to Tables

You can combine this with Conditions to colour-code the relevant sections:

![](/files/uzt1krbhOsX3yVggsOVV)

The output will appears as follows:

![](/files/PEYziMOkoT8k0nLqKFS7)

## **Dynamic Rows & Conditions**

You can add custom logic to your table rows, for example to render the background of a cell different based on certain conditions.

![](/files/WG5rRfJ8ogTADEYSEnH1)

![](/files/6920mPe7Jf8lQQnV4a1X)


# Template - Charts

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

{% embed url="<https://youtu.be/g1BIGD_Qfi4>" %}

You can add charts to your reports. The following charts are supported in ReportGen v2.5.4 or above.

* Pie chart
* Bar chart

## Pie Chart

<figure><img src="/files/QcWUDOgkfBnOfPszBRcY" alt="" width="563"><figcaption></figcaption></figure>

### Example 1: Pie Chart showing unique vulnerabilities

This example will produce a pie chart using data from existing tags e.g. {*totalCriticalVulns}, {totalHighVulns}*, etc. to populate the values in the chart.

```
{#json}{=< >=}</><%$chart[
   {
      “type”: “pie”,
      “name”: “unique_vulns_by_priority_pie”,
      “options”: {
         “title”: “”, 
         “data”: {
            “ignore_falsy_values”: true,
            “names”: [
	       “Critical”,
	       “High”,
	       “Medium”,
 	       “Low”,
   	       “Info”
	    ],
            “values”: [
               “%(totalCriticalVulns)”,
	       “%(totalHighVulns)”,
	       “%(totalMediumVulns)”,
	       “%(totalLowVulns)”,
	       “%(totalInfoVulns)”
	    ],
            “colors”: [
               “#7030A0”,
	       “#FF0000”,
	       “#FFC000”,
	       “#0070C0”,
	       “#00B050”
            ]
         },
         “width”: 600,
         “height”: 400
      }
   }
]><#json><={ }=>{/}
```

The following chart is produced:

<figure><img src="/files/mmOimaYIyq2ckrQ9HDNK" alt="" width="563"><figcaption></figcaption></figure>

### Example 2: Pie Chart using data from custom variables

This example will produce a pie chart for unique 'Internal Findings'. This example uses filters and functions in order to pre-calculate 'Internal Findings' into variables, then refers to these variables in the chart to populate the values in the chart.

This example uses custom tags to filter 'Internal Findings'. If you are not sure how to use custom tags, please check custom tags page for more details.

```
{$declare[CriticalInternalFindings][0]}
{$declare[HighInternalFindings][0]}
{$declare[MediumInternalFindings][0]}
{$declare[LowInternalFindings][0]}
{$declare[InfoInternalFindings][0]}
{#vulnerabilities | filterBy:’AffectedAssetCustomTags’:[‘Source:Internal’]}
{#priority == ‘Critical’}{$increment[CriticalInternalFindings][1]}{/}
{#priority == ‘High’}{$increment[HighInternalFindings][1]}{/}
{#priority == ‘Medium’}{$increment[MediumInternalFindings][1]}{/}
{#priority == ‘Low’}{$increment[LowInternalFindings][1]}{/}
{#priority == ‘Info’}{$increment[InfoInternalFindings][1]}{/}
{/}

{#json}{=< >=}</><%$chart[
   {
      “type”: “pie”,
      “name”: “unique_internal_findings_pie”,
      “options”: {
         “title”: “”, 
         “data”: {
            “ignore_falsy_values”: true,
            “names”: [
	       “Critical”,
	       “High”,
	       “Medium”,
 	       “Low”
	    ],
            “values”: [
               “$(CriticalInternalFindings)”,
	       “$(HighInternalFindings)”,
	       “$(MediumInternalFindings)”,
	       “$(LowInternalFindings)”
	    ],
            “colors”: [
               “#7030A0”,
	       “#FF0000”,
	       “#FFC000”,
	       “#0070C0”
            ]
         },
         “width”: 600,
         “height”: 400
      }
   }
]><#json><={ }=>{/}
```

The following chart is produced:

<figure><img src="/files/9CCJAj8ViNbBeJFxbSWy" alt="" width="563"><figcaption></figcaption></figure>

### Example 3: Donut Pie Chart showing unique vulnerabilities

The example will produce a donut pie chart using data from existing tags e.g. {*totalCriticalVulns}, {totalHighVulns}*, etc. to populate the values in the chart. It sets the "inner\_radius" to a value to create the donut hole in the center of the chart.

```
{#json}{=< >=}</><%$chart[
   {
      “type”: “pie”,
      “name”: “unique_vulns_by_priority_pie”,
      “options”: {
         “title”: “”, 
         “inner_radius”: 130,
         “data”: {
            “ignore_falsy_values”: true,
            “names”: [
	       “Critical”,
	       “High”,
	       “Medium”,
 	       “Low”,
   	       “Info”
	    ],
            “values”: [
               “%(totalCriticalVulns)”,
	       “%(totalHighVulns)”,
	       “%(totalMediumVulns)”,
	       “%(totalLowVulns)”,
	       “%(totalInfoVulns)”
	    ],
            “colors”: [
               “#7030A0”,
	       “#FF0000”,
	       “#FFC000”,
	       “#0070C0”,
	       “#00B050”
            ]
         },
         “width”: 600,
         “height”: 400
      }
   }
]><#json><={ }=>{/}
```

The following chart is produced:

<figure><img src="/files/9mUg5wcZjisLwbjaLebf" alt="" width="563"><figcaption></figcaption></figure>

### Configuration Options

* **type** (*required*): pie.
* **name** (*required*): must be unique name for the chart. String.
* **options** (*required*)
  * **inner\_radius** (*optional*): used to create a donut chart. Sets the radius of the inner white circle. Number. Default is 0.&#x20;
  * **title** (*optional*): display a title above the chart. String.
  * **title\_font\_size** (*optional*): sets font size for title. Number. Default is 18.
  * **title\_offset** (*optional*): sets the distance between the title and the top boundary. Number. Default is 25.
  * **labels\_inside\_chart** (optional): includes labels inside chart. true or false. defaults to false.
  * **data** (*required*)
    * **ignore\_falsy\_values** *(optional)*: true/false. Remove values and labels in chart for data with 0 value.
    * **names** (*required*): labels for each of the sections within the chart. List of strings.
    * **names\_font\_size** (*optional*): sets font size for names. Number. Default is 14.
    * **values** (*required*): data for each of the sections within the chart. List of strings, numbers, %(scope), $(variables).
    * **values\_font\_size** (*optional*): sets font size for values. Number. Default is 14.
    * **colors** (*optional*): colors to assign for each value. Supports hex codes and system color names. List of strings. Default is black.
  * **width** (*optional*): sets width of chart in pixels. Number. Default is 640.
  * **height** (*optional*): sets height of chart in pixels. Number. Default is 400.
  * **margin** (*optional*)
    * **top** (*optional*): sets the distance between the top of the chart, and the top boundary. Number. Default is 0.
    * **bottom** (*optional*): sets the distance between the bottom of the chart, and the bottom boundary. Number. Default is 0.
    * **left** (*optional*): sets the distance between the left-end of the chart, and the left boundary. Number. Default is 0.
    * **right** (*optional*): sets the distance between the right-end of the chart, and the right boundary. Number. Default is 0.

## Bar Chart

<figure><img src="/files/QeMtsJgXZJLnxxoXFTK7" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="/files/vWSjYk9wGNBycTTYny2T" alt="" width="563"><figcaption></figcaption></figure>

### Example 1: Horizontal Bar Chart showing unique vulnerabilities

This example will produce a horizontal bar chart using data from existing tags e.g. {*totalCriticalVulns}, {totalHighVulns}*, etc. to populate the values in the chart.

```
{#json}{=< >=}</><%$chart[
   {
      “type”: “bar”,
      “name”: “unique_vulns_by_priority_bar_horizontal”,
      “options”: {
         “orientation”: “horizontal”,
         “title”: “Unique Vulnerabilities”, 
         “data”: {
            “names_label”: “Priority”,
            “names”: [
	       “critical”,
	       “high”,
	       “medium”,
 	       “low”,
   	       “info”
	    ],
            “values_label”: “Vulnerabilities”,
            “values”: [
               “%(totalCriticalVulns)”,
	       “%(totalHighVulns)”,
	       “%(totalMediumVulns)”,
	       “%(totalLowVulns)”,
	       “%(totalInfoVulns)”
	    ],
            “colors”: [
               “#7030A0”,
	       “#FF0000”,
	       “#FFC000”,
	       “#0070C0”,
	       “#00B050”
            ]
         },
         “width”: 400,
         “height”: 300
      }
   }
]><#json><={ }=>{/}
```

The following chart is produced:

<figure><img src="/files/QeMtsJgXZJLnxxoXFTK7" alt="" width="563"><figcaption></figcaption></figure>

### Example 2: Vertical Bar Chart showing unique vulnerabilities

This example will produce a vertical bar chart using data from existing tags e.g. {*totalCriticalVulns}, {totalHighVulns}*, etc. to populate the values in the chart.

```
{#json}{=< >=}</><%$chart[
   {
      “type”: “bar”,
      “name”: “unique_vulns_by_priority_bar_horizontal”,
      “options”: {
         “orientation”: “vertical”,
         “title”: “Unique Vulnerabilities”, 
         “data”: {
            “names_label”: “Priority”,
            “names”: [
	       “critical”,
	       “high”,
	       “medium”,
 	       “low”,
   	       “info”
	    ],
            “values_label”: “Vulnerabilities”,
            “values”: [
               “%(totalCriticalVulns)”,
	       “%(totalHighVulns)”,
	       “%(totalMediumVulns)”,
	       “%(totalLowVulns)”,
	       “%(totalInfoVulns)”
	    ],
            “colors”: [
               “#7030A0”,
	       “#FF0000”,
	       “#FFC000”,
	       “#0070C0”,
	       “#00B050”
            ]
         },
         “width”: 400,
         “height”: 300
      }
   }
]><#json><={ }=>{/}
```

The following chart is produced:

### Example 3: Bar Chart using data from custom variables

This example will produce a horizontal and vertical bar chart for unique 'Internal Findings'. This example uses filters and functions in order to pre-calculate 'Internal Findings' into variables, then refers to these variables in the chart to populate the values in the chart.

This example uses custom tags to filter 'Internal Findings'. If you are not sure how to use custom tags, please check custom tags page for more details.

<pre><code>{$declare[CriticalInternalFindings][0]}
{$declare[HighInternalFindings][0]}
{$declare[MediumInternalFindings][0]}
{$declare[LowInternalFindings][0]}
{$declare[InfoInternalFindings][0]}
{#vulnerabilities | filterBy:’AffectedAssetCustomTags’:[‘Source:Internal’]}
{#priority == ‘Critical’}{$increment[CriticalInternalFindings][1]}{/}
{#priority == ‘High’}{$increment[HighInternalFindings][1]}{/}
{#priority == ‘Medium’}{$increment[MediumInternalFindings][1]}{/}
{#priority == ‘Low’}{$increment[LowInternalFindings][1]}{/}
{#priority == ‘Info’}{$increment[InfoInternalFindings][1]}{/}
{/}

<strong>{#json}{=&#x3C; >=}&#x3C;/>&#x3C;%$chart[
</strong>   {
      “type”: “bar”,
      “name”: “unique_vulns_by_priority_bar_horizontal”,
      “options”: {
         “orientation”: “horizontal”,
         “title”: “Unique Vulnerabilities”, 
         “data”: {
            “names_label”: “Priority”,
            “names”: [
	       “critical”,
	       “high”,
	       “medium”,
 	       “low”,
   	       “info”
	    ],
            “values_label”: “Vulnerabilities”,
            “values”: [
               “$(CriticalInternalFindings)”,
	       “$(HighInternalFindings)”,
	       “$(MediumInternalFindings)”,
	       “$(LowInternalFindings)”,
	       “$(InfoInternalFindings)”
	    ],
            “colors”: [
               “#7030A0”,
	       “#FF0000”,
	       “#FFC000”,
	       “#0070C0”,
	       “#00B050”
            ]
         },
         “width”: 400,
         “height”: 300
      }
   }
]>&#x3C;#json>&#x3C;={ }=>{/}
</code></pre>

The following chart is produced:

<figure><img src="/files/ObIHbn9s2whXsP1W6SGb" alt="" width="563"><figcaption></figcaption></figure>

Switching horizontal to vertical will produce the following:

<figure><img src="/files/KGQX4uOPaER8AmkpiX6K" alt="" width="563"><figcaption></figcaption></figure>

### Configuration Options

* **type** (*required*): bar.
* **name** (*required*): must be unique name for the chart. String.
* **options** (*required*)
  * **orientation** (*required*): must be vertical or horizontal. String.
  * **chart\_color** *(optional)*: color to assign to all labels and lines. Supports hex codes and system color names. Default is black.
  * **title** (*optional*): display a title above the chart. String.
  * **title\_font\_size** (*optional*): sets font size for title. Number. Default is 18.
  * **title\_offset** (*optional*): sets the distance between the title and the top boundary. Number. Default is 25.
  * **data** (*required*)
    * **names\_label** (*optional*): sets a label for the names. String.
    * **names\_label\_font\_size** (*optional*): sets font size for the names label. Number. Default is 18.
    * **names\_label\_offset** (*optional*): sets the distance between the names label and the left-boundary (for horizontal charts) or bottom-boundary (for vertical charts). Number. Default is 5 for vertical bar chart and 15 for horizontal bar chart.
    * **names** (*required*): labels for each of the bars within the chart. List of strings.
    * **names\_font\_size** (*optional*): sets font size for names. Number. Default is 10.
    * **values\_label** (*optional*): sets a label for the values. String.
    * **values\_label\_font\_size** (*optional*): sets font size for the values label. Number. Default is 18.
    * **values\_label\_offset** (*optional*): sets the distance between the values label and the bottom-boundary (for horizontal charts) or left-boundary (for vertical charts). Number. Default is 15 for vertical bar chart and 5 for horizontal bar chart.
    * **values** (*required*): data for each of the bars within the chart. List of strings, numbers, %(scope), $(variables)
    * **values\_font\_size** (*optional*): sets font size for values. Number. Default is 10.
    * **colors** (*optional*): colors to assign for each value. Supports hex codes and system color names. List of strings. Default is black.
  * **y\_axis\_increase\_ticks\_by\_percentage** *(optional)*: For vertical bar charts, increase the y-axis by a percentage.
  * **width** (*optional*): sets width of chart in pixels. Number. Default is 640.
  * **height** (*optional*): sets height of chart in pixels. Number. Default is 400.
  * **margin** (*optional*)
    * **top** (*optional*): sets the distance between the top of the chart, and the top boundary. Number. Default is 50.
    * **bottom** (*optional*): sets the distance between the bottom of the chart, and the bottom boundary. Number. Default is 50.
    * **left** (*optional*): sets the distance between the left-end of the chart, and the left boundary. Number. Default is 50.
    * **right** (*optional*): sets the distance between the right-end of the chart, and the right boundary. Number. Default is 0 for vertical charts and 10 for horizontal charts.

## Stacked Bar Chart

<figure><img src="/files/5zIEpQJELircYo565pxY" alt=""><figcaption></figcaption></figure>

### Example 1: Vertical Bar Chart

<pre><code>{#json}{=&#x3C; >=}&#x3C;/>&#x3C;%$chart[
    {
        “type”:“stacked-bar”,
<strong>        “name”:“all_vulns”,
</strong>        “options”: {
            “data”: {
                “names_label”: “OWASP Top 10 API Security Risks 2023”,
                “names”: [
                    “API1”,
                    “API2”,
                    “API3”,
                    “API4”,
                    “API5”,
                    “API6”,
                    “API7”,
                    “API8”,
                    “API9”,
                    “API10”
                ],
                "names_font_size":15,
                “groups”: {
                    “Critical”: “#001C57”,
                    “High”: “#1F3864”,
                    “Medium”: “#2F5496”,
                    “Low”: “#8EAADB”
                },
                “values_label”: “Vulnerabilities”,
                “values”:[
                    {
                        “Critical”: “1”,
                        “High”: “4”,
                        “Medium”: “6”,
                        “Low”: “8”
                    },
                    {
                        “Critical”: “0”,
                        “High”: “0”,
                        “Medium”: “3”,
                        “Low”: “5”
                    },
                    {
                        “Critical”: “0”,
                        “High”: “0”,
                        “Medium”: “0”,
                        “Low”: “1”
                    },
                    {
                        “Critical”: “6”,
                        “High”: “2”,
                        “Medium”: “1”,
                        “Low”: “5”
                    },
                    {
                        “Critical”: “9”,
                        “High”: “6”,
                        “Medium”: “7”,
                        “Low”: “4”
                    },
                    {
                        “Critical”: “2”,
                        “High”: “8”,
                        “Medium”: “9”,
                        “Low”: “2”
                    },
                    {
                        “Critical”: “6”,
                        “High”: “7”,
                        “Medium”: “0”,
                        “Low”: “2”
                    },
                    {
                        “Critical”: “0”,
                        “High”: “0”,
                        “Medium”: “0”,
                        “Low”: “0”
                    },
                    {
                        “Critical”: “0”,
                        “High”: “2”,
                        “Medium”: “7”,
                        “Low”: “8”
                    },
                    {
                        “Critical”: “1”,
                        “High”: “2”,
                        “Medium”: “8”,
                        “Low”: “0”
                    }
                ],
                "values_font_size":15
            },
            “y_axis_increase_ticks_by_percentage”: 20,
            “width”:700, 
            “height”:400, 
            "margin": {
                "top":10,
                "bottom":70,
                "left":80,
                "right":30
            }
        }
    }
]>&#x3C;#json>&#x3C;={ }=>{/}

</code></pre>

### Configuration Options

* **type** (*required*): stacked-bar.
* **name** (*required*): must be unique name for the chart. String.
* **options** (*required*)
  * **chart\_color** *(optional)*: color to assign to all labels and lines. Supports hex codes and system color names. Default is black.
  * **title** (*optional*): display a title above the chart. String.
  * **title\_font\_size** (*optional*): sets font size for title. Number. Default is 18.
  * **title\_offset** (*optional*): sets the distance between the title and the top boundary. Number. Default is 25.
  * **data** (*required*)
    * **names\_label** (*optional*): sets a label for the names. String.
    * **names\_label\_font\_size** (*optional*): sets font size for the names label. Number. Default is 18.
    * **names\_label\_offset** (*optional*): sets the distance between the names label and the left-boundary (for horizontal charts) or bottom-boundary (for vertical charts). Number. Default is 5 for vertical bar chart and 15 for horizontal bar chart.
    * **names** (*required*): labels for each of the bars within the chart. List of strings.
    * **names\_font\_size** (*optional*): sets font size for names. Number. Default is 10.
    * **groups** (required): details for each of the groups used in the values.
      * "Group Name": "HEX Color"
    * **values\_label** (*optional*): sets a label for the values. String.
    * **values\_label\_font\_size** (*optional*): sets font size for the values label. Number. Default is 18.
    * **values\_label\_offset** (*optional*): sets the distance between the values label and the bottom-boundary (for horizontal charts) or left-boundary (for vertical charts). Number. Default is 15 for vertical bar chart and 5 for horizontal bar chart.
    * **values** (*required*): data for each of the bars within the chart. List of objects with values for each group. Values must be numbers, %(scope), or $(variables)
      * "Group Name": number
      * "Group Name": %(scope)
      * "Group Name": $(variables)
    * **values\_font\_size** (*optional*): sets font size for values. Number. Default is 10.
  * **y\_axis\_increase\_ticks\_by\_percentage** *(optional)*: For vertical bar charts, increase the y-axis by a percentage.
  * **width** (*optional*): sets width of chart in pixels. Number. Default is 640.
  * **height** (*optional*): sets height of chart in pixels. Number. Default is 400.
  * **margin** (*optional*)
    * **top** (*optional*): sets the distance between the top of the chart, and the top boundary. Number. Default is 50.
    * **bottom** (*optional*): sets the distance between the bottom of the chart, and the bottom boundary. Number. Default is 50.
    * **left** (*optional*): sets the distance between the left-end of the chart, and the left boundary. Number. Default is 50.
    * **right** (*optional*): sets the distance between the right-end of the chart, and the right boundary. Number. Default is 0 for vertical charts and 10 for horizontal charts.


# Template - Conditions

> UPDATED: Please head over to our new GitHub Support Site for help, examples, tips and tricks: <https://github.com/AttackForge/ReportGen>

```
{#users.length>1}
There are multiple users
{/}

{#userName == "John"}
Hello John, welcome back
{/}
```

The first condition will render the section only if there are 2 or more users.

The second condition will render the section only if the userName is the string “John”.

It also handles the boolean operators AND `&&`, OR `||`, `+`, `-`, the ternary operator `a ? b : c`, operator precedence with parenthesis `(a && b) || c`, and many other javascript features.

For example, it is possible to write the following template:

```
{#generalCondition}
{#cond1 || cond2}
Paragraph 1
{/}
{#cond2 && cond3}
Paragraph 2
{/}
{#cond4 ? users : usersWithAdminRights}
Paragraph 3
{/}
There are {users.length} users.
{/generalCondition}
```

## IF Statements

You can create IF conditions in your template by inserting a hashtag and immediately proceeding with the condition i.e. {#... == "..."}&#x20;

An example is included below to only print vulnerabilities in the report which have a critical priority:

```
{#vulnerabilities}
{#priority == "Critical"}
Critical Vulnerability: {title}
{/}{/}
```

## IF AND Statements

You can create IF AND conditions in your template by inserting a hashtag and immediately proceeding with the condition i.e. {#... == "..." && ... == "..."}&#x20;

An example is included below to only print vulnerabilities in the report which have a critical priority and title is *SQL Injection*:

```
{#vulnerabilities}
{#priority == "Critical" && title == "SQL Injection"}
Critical Vulnerability: SQL Injection
{/}{/}
```

## IF OR Statements

You can create IF OR conditions in your template by inserting a hashtag and immediately proceeding with the condition i.e. {#... == "..." || ... == "..."}&#x20;

An example is included below to only print vulnerabilities in the report which have a critical or high priority:

```
{#vulnerabilities}
{#priority == "Critical" || priority == "High"}
{priority} - {title}
{/}{/}
```

## IF ELSE Statements

You can create IF ELSE conditions in your template by inserting a hashtag and immediately proceeding with the condition i.e. {#... == "..."} and then after the closing tag {/} you can insert your else statement {^...}...{/}&#x20;

An example is included below to print each project note if they exist, or to indicate that no project notes were found:

```
{#projectNotes}
{note}
{/}
{^projectNotes}
No project notes found.
{/}
```

Using the example above, the report will either print the details for each project note; or it will print *No project notes found.*

## Conditional Logic & Counting

You can combine conditions with [Filters](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-filters) and [Functions](https://support.attackforge.com/attackforge-enterprise/modules/reporting/template-functions) to create powerful logic conditions in your reports.

For example, lets say you want to&#x20;

* count every vulnerability instance (affected asset);&#x20;
* count every vulnerability instance (affected asset) which is *Open/Not Fixed* AND *Not Informational*
* count every vulnerability instance (affected asset) which is *Ready For Retest* AND *Not Informational*
* count every vulnerability instance (affected asset) which is *Closed/Fixed* AND *Not Informational*

You can achieve this using the following:

```
{$declare[TotalVulnerabilities][0]}
{$declare[TotalFixedVulnsExInfo][0]}
{$declare[TotalRetestVulnsExInfo][0]}
{$declare[TotalNotFixedVulnsExInfo][0]}
{#vulnerabilities}
{#affected_assets}
{$increment[TotalVulnerabilities][1]}
{#remediation_status | includes:[“Open”]}
{#priority !== “Info”}
{$increment[TotalNotFixedVulnsExInfo][1]}
{/}{/}
{#remediation_status | includes:[“Ready for Retest”]}
{#priority !== “Info”}
{$increment[TotalRetestVulnsExInfo][1]}{/}{/}
{#remediation_status | includes:[“Closed”]}
{#priority !== “Info”}
{$increment[TotalFixedVulnsExInfo][1]}{/}{/}
{/affected_assets}
{/vulnerabilities}

Total Vulnerabilities for All Affected Assets:
{$value[TotalVulnerabilities]}

Total Open/Not Fixed Vulnerabilities which are Not Informational:
{$value[TotalNotFixedVulnsExInfo]}

Total Vulnerabilities for All Affected Assets:
{$value[TotalRetestVulnsExInfo]}

Total Vulnerabilities for All Affected Assets:
{$value[TotalFixedVulnsExInfo]}
```

The logic above works as follows:

* Declare dynamic variables that we will use as counters e.g. {$declare..}
* Loop through every vulnerability
* Loop through every instance (affected asset) for every vulnerability
* Increment the counter for total vulnerabilities by 1
* Check if remediation status for the affected asset includes 'Open' and check if the priority is not "Info" - if condition is met, increment the counter for total Open/Not Fixed Not-Info vulnerabilities by 1
* Check if remediation status for the affected asset includes 'Ready for Retest' and check if the priority is not "Info" - if condition is met, increment the counter for total Ready for Retest Not-Info vulnerabilities by 1
* Check if remediation status for the affected asset includes 'Closed' and check if the priority is not "Info" - if condition is met, increment the counter for total Closed/Fixed Not-Info vulnerabilities by 1
* Print the values of the counters

## Data Aggregation

If your data is the following:

```
{
    "items": [
        {
            "name": "Acme Computer",
            "price": 1000,
        },
        {
            "name": "Mouse & Keyboard",
            "price": 150,
        }
    ],
}
```

And you would like to show the total price, you can use:

```
{#items}
{name} for a price of {price} €
{/}
Total Price of your purchase : {items | sumby:'price'}€
```

## Data Formatting

This example is to format numbers in the format: “150.00” (2 digits of precision):

```
{
    "items": [
        {
            "name": "Acme Computer",
            "price": 1000,
        },
        {
            "name": "Mouse & Keyboard",
            "price": 150,
        }
    ],
}
```

And you would like to show the price with two digits of precision, you can write in your template :

```
{#items}
{name} for a price of {price | toFixed:2} €
{/}
```

## Assignments

It is possible to assign a value to a variable directly from your template. For example, in your template, write:

```
{full_name = first_name + last_name}
```

The problem with this expression is that it will return the value of full\_name. There are two ways to fix this issue, either if you still would like to keep this as the default behaviour, add ; ‘’ after your expression, for example

```
{full_name = first_name + last_name; ''}
```

This will first execute the expression, and then execute the second statement which is an empty string, and return it.

An other approach is to automatically silence the return values of expression containing variable assignments.


# Template - Images

## Image Downscaling

AttackForge utilises image downscaling for any dynamic images to help improve performance and speed for report generation. Dynamic images are images that are uploaded to AttackForge, not statically input into your report template.

Images are downscaled by default to `975 pixels`. This is an ideal balance for most images for reporting purposes.

However, you can increase or decrease this threshold.

**Increasing** the `Max Image Width` will improve image quality, however may increase report generation time.

**Decreasing** the `Max Image Width` may decrease report generation time, however image quality will also be decreased.

Images can be downscaled to any value between `100 pixels` and `2000 pixels`.

<figure><img src="/files/sUzZU4EsVk6V7q9uWRz4" alt=""><figcaption></figcaption></figure>


# ReportGen CLI

The **ReportGen Command Line Interface (CLI)** tool is ideal for people who prefer to build pentest reports on the command line; and combine ReportGen into an existing automation or pipeline.&#x20;

You can create automations combining ReportGen CLI with Self-Service API Events.

For example, you can create *real-time automated PDF reports* and have them securely emailed to your customers, posted to a Slack/Teams channel, or uploaded to a ticket.

For more information on how to do this, check out our [Blog](https://blog.attackforge.com/blog/reportgencli).

You can download ReportGen CLI and NodeJS library directly from [NPM](https://www.npmjs.com/search?q=attackforge).


# ReportGen Library

The **ReportGen NodeJS library** is ideal for people who want to simply "import" ReportGen into their existing codebase or scripts and build custom penetration testing reports easily and effortlessly natively in your own code.

You can download ReportGen CLI and NodeJS library directly from [NPM](https://www.npmjs.com/search?q=attackforge).


# Users

## Overview

Users module provides administrative control over all users in the system. This module is only available to Administrators.&#x20;

Users module makes it easy to:

* Create and Invite New Users
* Manage User Access to Projects, Groups and Self-Service APIs
* Perform User Updates including Name, Email, Username, Role, Password Change, Reset 2FA, Disable 2FA
* Block Sign In or Activate Sign In for users
* View Login History for a user
* View Audit Logs for a user
* Delete a user

<figure><img src="/files/ZAGwObx8NHuniiiYuupS" alt=""><figcaption></figcaption></figure>

## User Roles

AttackForge is built on two primary access models:

* **Application Roles** – determines what modules the user has access to, and functionality within those modules.
* **Data Object Privileges** – determines what privileges and functions a Role, [Group](https://support.attackforge.com/attackforge-enterprise/modules/groups) or User has access to with respect to the data object. Access control on the following data objects is supported:
  * Portfolios and Streams
  * Project Requests
  * Projects (inherits Vulnerabilities)
  * Asset Libraries
  * Writeup Libraries
  * Report Templates
  * Groups
  * Flows
  * Self-Service RESTful APIs and Events APIs (access per API Endpoint/Event)
  * Custom Field-Level Access Control
  * Project Feature-Level Access Control (Attack Chains, Retests, Test Cases, Pages)<br>

***A non-Admin user will only see vulnerabilities and projects they have explicitly been invited to or inherit access via Groups.***

There are currently five (5) application roles an Administrator can assign to a user:

* **Client (standard user)**
* **Consultant (standard user)**
* **Library Moderator&#x20;*****(deprecated)***
* **Project Coordinator**
* **Administrator**

## **Clients / Consultants**

**Clients / Consultants** can do the following:

* Access Global Dashboard
* Access Analytics module
  * can see analytics for data they have access to
* Access Vulnerabilities module
  * can see all vulnerabilities they have access to
* Access Projects module
  * can only see any projects they have been invited to
  * can only see project requests they have made
* Access Scheduling module
  * can only see their projects
* Access Portfolios module
  * has view access only to portfolios/streams based on configuration
* Access Assets module (if enabled)
  * has view/edit access to assets based on asset library access controls and uncatalogued assets access controls
* Access Writeups module
  * has view/edit access only to writeup libraries based on configuration
* Access Flows module
  * has info/view/run/edit permissions to individual flows based on flow settings
* Access Self-Service API
  * by default has no access to API methods/endpoints
* Access Attack Chains module
* Access Help & Support

## **Library Moderators**

**Library Moderators** (*deprecated*) can do the following:

> **!IMPORTANT**: We do not recommend using Library Moderator role. Access to libraries can now be managed via access controls per individual library.

* Access Global Dashboard
* Access Analytics module
  * can see analytics for data they have access to
* Access Vulnerabilities module
  * can see all vulnerabilities they have access to
* Access Projects module
  * can only see any projects they have been invited to
  * can only see project requests they have made
* Access Scheduling module
  * can only see their projects
* Access Portfolios module
  * has view access only to portfolios/streams based on configuration
* Access Assets module (if enabled)
  * has view/edit access to assets based on asset library access controls and uncatalogued assets access controls
* Access Writeups module
  * has view/edit access only to writeup libraries based on configuration
* Access Flows module
  * has info/view/run/edit permissions to individual flows based on flow settings
* Access Self-Service API
  * by default has no access to API methods/endpoints
* Access Attack Chains module
* Access Help & Support

## **Project Coordinators**

**Project Coordinators** can do the following:

* Access Global Dashboard
* Access Analytics module
  * can see analytics for data they have access to
* Access Vulnerabilities module
  * can see all vulnerabilities they have access to
* Access Projects module
  * can create new projects
  * can update projects
  * gets access to all new projects (optional)
  * can invite users to projects
  * can manage user access to projects
  * can access all pending & actioned project requests
  * can approve new project requests
  * can request more information on project requests
  * can reject new project requests
* Access Scheduling module
  * can only see their projects
  * can filter schedule by user and project role
* Access Portfolios module
  * has view access only to portfolios/streams based on configuration
* Access Report Templates module
  * has full access to this module, including CRUD operations
* Access Assets module (if enabled)
  * has view/edit access to assets based on asset library access controls and uncatalogued assets access controls
* Access Writeups module
  * has view/edit access only to writeup libraries based on configuration
* Access Test Suites module
  * has full access to this module, including CRUD operations
* Access Flows module
  * has info/view/run/edit permissions to individual flows based on flow settings
* Access Self-Service API
  * by default has no access to API methods/endpoints
* Access Attack Chains module
* Access Help & Support

## **Administrators**

**Administrators** are Super Users and can access all functionality & workflows, including:

* Access Global Dashboard
* Access Analytics module
  * can see analytics for all data
* Access Vulnerabilities module
  * can see all vulnerabilities
* Access Projects module
  * can create new projects
  * can update projects
  * can archive & unarchive projects
  * can destroy projects
  * can invite users to projects
  * can manage user access to projects
  * can access all pending & actioned project requests
  * can approve new project requests
  * can request more information on project requests
  * can reject new project requests
  * can perform all workflows on a project
* Access Scheduling module
  * can see all projects
* Access Portfolios module
  * has full access to this module, including CRUD operations
* Access Report Templates module
  * has full access to this module, including CRUD operations
* Access Assets module (if enabled)
  * has access to CRUD operations for all assets
  * can see vulnerabilities for all assets
* Access Writeups module
  * has full access to this module, including CRUD operations
* Access Test Suites module
  * has full access to this module, including CRUD operations
* Access Groups module
  * has full access to this module, including CRUD operations
  * can manage Group Membership for users
* Access Users module
  * has full access to this module, including CRUD operations
  * can perform all administrative tasks, such as user management, access logs, etc.
* Access Flows module
  * has info permissions to all flows
  * has view/run/edit permissions to individual flows based on flow settings
* Access Self-Service API
  * by default has no access to API methods/endpoints
  * can assign access to self or other users via Users module
* Access Attack Chains module
* Access Administration module
  * has full access. Can purchase add-ons, update tenant configuration and view licensing and support information.
* Access Help & Support

## Registering A New User

Where enabled on your tenant, new users can self-register accounts from the login page. They will need to activate their account (via email activation link) before they can sign in.

The first time a user logs into AttackForge, the user will receive a QR code to scan with their mobile authenticator app to enable two-factor authentication (2FA) for their account. By default, 2FA is mandatory and enforced on all accounts. This can be disabled on a user-by-user basis by Administrators.

<figure><img src="/files/QKGwhMi2YyFB2MOvM40l" alt=""><figcaption></figcaption></figure>

## Creating A New User

Administrators can create new users in the system directly, without having to go through registration workflow. This is useful when you need to create accounts fast or when you don't have access to an email address for activation.&#x20;

You can create a new user in the system by clicking on `New User` button.

When creating a new user, you can assign any user role available in the system. Newly created account are already verified and activated so they can log in immediately.

The user will receive a `Welcome Email` to inform them that their account has been created, and instructions on how they can log in.

<figure><img src="/files/hfzT7YWfszryewihv1qF" alt=""><figcaption></figcaption></figure>

## Update User Profile

You can update a user's profile.

<figure><img src="/files/pDHigRYyd9fY5v0MO3FZ" alt=""><figcaption></figcaption></figure>

## Update User Account

You can update a users account settings. This includes:

* Change email address
* Change username
* Change password
* Enable/Disable/Reset MFA
* Set expiry date (user will be blocked after that date)
* Resend welcome invitation email
* Block/Unblock user from signing in
* Delete user

<figure><img src="/files/sNy1WSwHnV07CbORnK8h" alt=""><figcaption></figcaption></figure>

## Managing Access

You can change a users' role. For information on roles, see [User Roles](#user-roles).

You can also manage a users' access to the following:

* Groups
* Projects
* Delegations
* Self-Service Events API
* Self-Service RESTful API

### Groups

Here you can update a users' permissions on any groups they are members of, or you can remove their access.

If you update a users' access to a group, the new privileges will apply immediately across all projects linked to the group.

You can also bulk add a user to groups.

<figure><img src="/files/o466L9y2JoMDjBDQNOu0" alt=""><figcaption></figcaption></figure>

### Projects

Here you can update a users' permissions on any projects they are an existing team member, or you can remove their access.

If you update a users' access to a project, the new privileges will apply immediately on the project.

If you remove a users' access to a project, this will remove the user from the project immediately. They will no longer be able to see any of the projects' vulnerabilities and data.

You can also bulk add a user to projects.

<figure><img src="/files/RPCqFpB4POyA6Xv2wozm" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/LDgEsHJjYMDU2lJqTQdL" alt=""><figcaption></figcaption></figure>

### Delegations

Admins can delegate specific workflows and functions to individual users.

Delegations can be assigned to any user, and can help to:

* Reduce the burden placed on admins and privileged users.
* Empower trusted individuals with autonomy to perform more tasks in AttackForge.

Here you can update a users' delegations, or remove their delegations.

You can also bulk apply delegations to a user.

<figure><img src="/files/izFOtd31GCGJgEByL8mp" alt=""><figcaption></figcaption></figure>

Certain delegations can also be assigned to entire application user roles via Administration module.

### Self-Service Events API

Here you can manage a users' access to the Self-Service Events API.

Here you can update a users' permissions on any events or you can remove their access.

You can also bulk add events for a user.

<figure><img src="/files/740Yfy96jfh8NLzf0org" alt=""><figcaption></figcaption></figure>

> **!IMPORTANT:** By default, *all users* have *no access* to the Self-Service API. Access is granted explicitly by the Administrators to a user from within this module. Access to the SSAPI is controlled & applied on an individual event basis.

Every user in the system can generate or regenerate their User Key.

> **Tip:** If you would like to create a service account in the system with non-interactive access to the application interface - you can grant the user permissions via the SSAPI, then block the user so they cannot login to the application. They will still be able to access the SSAPI.

### Self-Service RESTful API

Here you can manage a users' access to the Self-Service RESTful API.

Here you can update a users' permissions on any endpoints or you can remove their access.

You can also bulk add endpoints for a user.

<figure><img src="/files/YRKT5vO4cCLZfxOoI1dU" alt=""><figcaption></figcaption></figure>

> **!IMPORTANT:** By default, *all users* have no access to the Self-Service API. Access is granted explicitly by the Administrators to a user from within this module. Access to the SSAPI is controlled & applied on an individual endpoint basis.

Every user in the system can generate or rotate their API key by visiting Self-Service API module.

> **Tip:** If you would like to create a service account in the system with non-interactive access to the application interface - you can grant the user permissions via the SSAPI, then block the user so they cannot login to the application. They will still be able to access the SSAPI.

## Flows

Flows is included in all AttackForge Enterprise plans, and in the AttackForge Core SME plan. For all others plans, Flows can be add-on from the `Administration -> Subscriptions` page.&#x20;

To get started with building a Flow:

* You must have **Create** access to [Event Triggers](#internal-events) or [HTTP Triggers](#external-events).

<figure><img src="/files/4f5feP188odufbMzfsN8" alt=""><figcaption></figcaption></figure>

### Event Trigger Access Level

* **None** - user is unable to create or import flows with the [Event Trigger](#internal-events) type. Any existing Event Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the [Event Trigger](#internal-events) type they are the owner of, so long as the flow is enabled and they have access to the matching [event type](#internal-events).
* **Create** - user is able to create or import flows with the [Event Trigger](#internal-events) type. Any existing Event Trigger flows for which they are the owner of will run so long as the flow is enabled and the user has access to the matching [event type](#internal-events).

### HTTP Trigger - Authentication (None) Access Level

HTTP Triggers can be [configured with no authentication](#http-trigger-authentication). This means that the flow will not check for authentication before the flow runs.

* **None** - user is unable to create or import flows with the non-authenticated [HTTP Trigger](#external-events) type. Any existing non-authenticated HTTP Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the non-authenticated [HTTP Trigger](#external-events) type they are the owner of, so long as the flow is enabled.
* **Create** - user is able to create or import flows with the non-authenticated [HTTP Trigger](#external-events) type. Any existing non-authenticated HTTP Trigger flows for which they are the owner of will run so long as the flow is enabled.

### HTTP Trigger - Authentication (User API Key) Access Level

HTTP Triggers can be [configured with authentication](#http-trigger-authentication). This means that the flow will check for authentication before the flow runs.

* **None** - user is unable to create or import flows with the authenticated [HTTP Trigger](#external-events) type. Any existing authenticated HTTP Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the authenticated [HTTP Trigger](#external-events) type they are the owner of, so long as the flow is enabled and an authorized user key is supplied in the nominated header.
* **Create** - user is able to create or import flows with the authenticated [HTTP Trigger](#external-events) type. Any existing authenticated HTTP Trigger flows for which they are the owner of will run so long as the flow is enabled and an authorized user key is supplied in the nominated header.

> **IMPORTANT: Administrators have implicit&#x20;*****Create*****&#x20;on all Triggers. This is non revokable.**

## Notifications

Admins can update the project email notifications options on behalf of another user, to tailor project notifications for the user.

<figure><img src="/files/UxXVFqF7uDI2lzok2K1u" alt=""><figcaption></figcaption></figure>

## Logs

Admins can view audit logs for a user.

<figure><img src="/files/hYaqwlwpR8FYssGCooFn" alt=""><figcaption></figcaption></figure>

## Preferences

Admins can update the login landing page for a user. The login landing page is the first page a user sees when they log into AttackForge.

The default login landing page is the `Global Dashboard`.

<figure><img src="/files/JvApO8jkUU5cflptRMqm" alt=""><figcaption></figcaption></figure>

## Secrets

Secrets are any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

User Secrets make it easy to store and update that sensitive information in one place, for example rotating passwords and credentials without having to update other places where that information is used.

User Secrets can only be created and viewed by the user which created the secret.

You can also choose to share your secret with other users if you would need to.

<figure><img src="/files/uo3FtDM9PDMzHqMbGKLd" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/prL1aUV53h7lHOStHzYL" alt=""><figcaption></figcaption></figure>

### User API Key

Every user in AttackForge can generate one User API Key. This key can be used in multiple places, including:

* Authentication in the [RESTful Self-Service API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api)
* Authentication in the [Events API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api)
* Authentication in [HTTP Trigger Flows](https://support.attackforge.com/attackforge-enterprise/modules/flows#external-events)

The key is static and does not expire. You can regenerate your key at any time by clicking on the `regenerate` button.

<figure><img src="/files/jTO1WYUYdGQEwfEXlxZR" alt=""><figcaption></figcaption></figure>

## Single Sign On (SSO)

SSO can be configured in AttackForge.&#x20;

AttackForge supports SSO via `OAuth2 Open ID Connect (OIDC)`.

AttackForge supports two (2) concurrent Identity Provider (IdP) integrations, meaning you can have two separate SSO login workflows.

> SSO is only available on Enterprise plans and must be configured and enabled by AttackForge

### Auto Create User on Login

AttackForge supports Just-in-Time user creation via SSO, meaning you do not need to pre register or create user accounts - they will be automatically created for the user on their first login and assigned with the configured `Default Role for New Users`.

You can however disable this feature if required, to prevent anyone with an account on your IdP being able to create an AttackForge user account.

<figure><img src="/files/725nz2XjAaBxp0t15DWF" alt=""><figcaption></figcaption></figure>

### Require MFA on Admin SSO Logins

If your IdP does not enforce MFA, you can enable this option to require Admin users in AttackForge to go through AttackForge's 2FA process.

### Manage Roles via SSO Groups

You can opt into having AttackForge automatically manage your SSO application user roles.&#x20;

This feature allows you to map your `Identity Provider Groups` or `Active Directory Groups` to AttackForge Application Roles, meaning the user will be granted the relevent role each time they log in.

If a user's groups do not meet any of the specified groups, the user will be subjected to the `Default Role/Action For SSO Users`.

The Default Role/Action For SSO Users supports the following:

* **Blocked** - User will be denied log in to AttackForge.
* **Client** - User will be assigned the Client role.
* **Consultant** - User will be assigned the Consultant role.

<figure><img src="/files/yeREfk57QCYJOl1A1UYV" alt=""><figcaption></figcaption></figure>

### Manage Group Membership via SSO Groups

You can link `Identity Provider (IdP)` or `Active Directory (AD) Groups` to AttackForge Groups.

This feature is available for Single-Sign-On (SSO) enabled tenants to help automate provisioning and removal of users to AttackForge Groups and their related projects, based on the users' IdP/AD groups.

This feature can help to ensure that users accessing AttackForge receive sufficient access to projects based on the Enterprises' own access control groups, and remove access to projects which they should not have access - upon each login.

This option is Disabled by default. It is only enabled, on an AttackForge Group-by-Group basis, when an IdP/AD group is linked to the AttackForge Group.

> How it works:&#x20;

1. When creating a new group in AttackForge, or when editing an existing group - you can link one or more IdP/AD groups. This is an optional field.
2. Once a group is linked, **all group membership** will be controlled via SSO.&#x20;

If the user signing into AttackForge via SSO has IdP/AD groups returned in their SSO profile, the following checks will apply:

* For each AttackForge Group with linked IdP/AD groups - check to see if any linked groups match any of the users' IdP/AD groups:
  * **If match exists**
    * If the user is not already a member of the group, add the user as a member to the group.
    * Assign the access level according to the mapping.
  * **If no match exists**
    * If the user is already a member of the group - remove their access to the group.

<figure><img src="/files/4rqT4DsuAMiXpF1XttS8" alt=""><figcaption></figcaption></figure>


# Administration

## Overview

The Administration module helps you to:

* Track & manage licensing
* Configure & customize your tenant

## Licence

The Licence section provides overview of your AttackForge licence, including:

* Licence Type, Plan, End date
* Credits consumption, remaining, and ability to top up credits
* Subscriptions and ability to enable/disable subscriptions

<figure><img src="/files/B19xImkQFdWNuSouvbR6" alt=""><figcaption></figcaption></figure>

## Portfolios

Here you can configure the settings for your [Portfolios](https://support.attackforge.com/attackforge-enterprise/modules/portfolios).

<figure><img src="/files/QxtJNGiiULbBUXMY5r1e" alt=""><figcaption></figcaption></figure>

## Projects

Here you can configure the settings for your [Projects](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-and-managing-projects).

<figure><img src="/files/T3zcAvmd2VxWkWwy0khO" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/yLHWPbzNC43MXpqt7GRd" alt=""><figcaption></figcaption></figure>

## Project Requests

Here you can configure the settings for your [Project Requests](https://support.attackforge.com/attackforge-enterprise/getting-started/requesting-a-project).

<figure><img src="/files/Mn9LLUmu9i7uSye7kgDr" alt=""><figcaption></figcaption></figure>

## Vulnerabilities

Here you can configure the settings for your [Vulnerabilities](https://support.attackforge.com/attackforge-enterprise/getting-started/creating-vulnerabilities).

<figure><img src="/files/llorl03EkFwAMIVy53O3" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/RjZVvE9wBdxT9Yo7CWzJ" alt=""><figcaption></figcaption></figure>

## Project Test Cases

Here you can configure the settings for your [Project Test Cases](https://support.attackforge.com/attackforge-enterprise/getting-started/test-cases).

<figure><img src="/files/1xVwlWCBuzmibmwsS8ue" alt=""><figcaption></figcaption></figure>

## Retesting

Here you can configure the settings for your [Retesting Rounds](https://support.attackforge.com/app/getting-started/retesting).

<figure><img src="/files/znbv4GEd3NGWAWCBnYt6" alt=""><figcaption></figcaption></figure>

## Reporting

Here you can configure the settings for your [Reporting](https://support.attackforge.com/app/getting-started/reporting).

<figure><img src="/files/ghRBG0t8lmSltn7JJBLt" alt=""><figcaption></figcaption></figure>

## Pages

Here you can configure the settings for your [Project Pages](https://support.attackforge.com/app/getting-started/project-pages).

<figure><img src="/files/lRbP2Ee7ykZ8y86CnBk5" alt=""><figcaption></figcaption></figure>

## Groups

Here you can configure the settings for your [Groups](https://support.attackforge.com/app/modules/groups).

<figure><img src="/files/ZdAEpns0BijXJb5UUhEU" alt=""><figcaption></figcaption></figure>

## Assets

Here you can configure the settings for your [Assets](https://support.attackforge.com/attackforge-enterprise/modules/assets).

<figure><img src="/files/DGlIjsnnfK9TAjAjTNRV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/q5sBizO22miT2tsAisd0" alt=""><figcaption></figcaption></figure>

## Writeups

Here you can configure the settings for your [Writeups](https://support.attackforge.com/attackforge-enterprise/modules/vulnerability-library).

<figure><img src="/files/BL0qkyZDIZSNZXTJx3h7" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Mc2svNcc2f9uMvk0Jm3A" alt=""><figcaption></figcaption></figure>

## Test Suites

Here you can configure the settings for your [Test Suites](https://support.attackforge.com/attackforge-enterprise/modules/test-suite-builder).

<figure><img src="/files/S41PLbpwmerGZ1Pfcxnh" alt=""><figcaption></figcaption></figure>

## Notifications

Here you can configure the settings for your [Notifications](https://support.attackforge.com/attackforge-enterprise/getting-started/notifications).

<figure><img src="/files/s4i4ytwtQL0fRJRY9QuQ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/pLAFFB7bkRIslVvhqwHk" alt=""><figcaption></figcaption></figure>

## Login and Security

Here you can configure the settings for login, SSO and security settings.

<figure><img src="/files/aPPSmu3AF0IA3NpMxLIy" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/JEVJFrawmJ6v8PQzUEwr" alt=""><figcaption></figcaption></figure>

## Users

Here you can configure the settings for [Users](https://support.attackforge.com/attackforge-enterprise/modules/users).

<figure><img src="/files/tsk0flaC0mZWoc1J7iMf" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/o9k1FKpefEqt0gQpDB1t" alt=""><figcaption></figcaption></figure>

## Integrations

Here you can configure your integrations.

<figure><img src="/files/EBTeLKn1Ud1pKlirvI73" alt=""><figcaption></figcaption></figure>

## Preferences

Here you can configure miscellaneous settings such as custom logo and colors.

<figure><img src="/files/OCw5lxXRSuEm0LLHAais" alt=""><figcaption></figcaption></figure>


# Flows

## Overview

<figure><img src="/files/Ekvba3Ge2uoMUQ56Thki" alt=""><figcaption></figcaption></figure>

Flows is a comprehensive, end-to-end workflow automation engine powered by [AFScript](/app/afscript).

Flows can help you to automate AttackForge with *nearly unlimited systems*. You can streamline processes across your organization to save time and focus on what's important.

Some examples you can do with Flows:

* Create bespoke workflows - QA, risk management, reporting, bi-directional integrations, and more.
* Prioritize vulnerabilities with full context, including threat-intelligence like [VulnDB](https://flashpoint.io/ignite/vulnerability-intelligence/)
* Integrate your vulnerability data with ticketing tools like [Atlassian JIRA](https://www.atlassian.com/software/jira), [ServiceNow](https://www.servicenow.com/), [Azure DevOps](https://azure.microsoft.com/en-us/products/devops), [BMC Helix](https://www.bmc.com/it-solutions/bmc-helix.html) and others.
* Visualize your pentesting data in powerful tools like [Power BI](https://www.microsoft.com/en-us/power-platform/products/power-bi) and [Tableau](https://www.tableau.com/)
* Integrate your Bug Bounty and VDP data from platforms like [HackerOne](https://hackerone.com/) and [BugCrowd](https://www.bugcrowd.com/)
* Help make better risk decisions by sending your vulnerability data to GRC platforms like [RSA Archer](https://www.archerirm.com/), [MetricStream](https://www.metricstream.com/), [OneTrust](https://www.onetrust.com/) and [LogicGate](https://www.logicgate.com/).
* Create workflow automations by chaining together [AttackForge Self-Service APIs](/app/modules/self-service-restful-api)
* Trigger automated scanning activities in your security toolset like [Rapid7](https://www.rapid7.com/), [Tenable](https://www.tenable.com/) and [Qualys](https://www.qualys.com/).
* Create messages on collaboration platforms like [Slack](https://slack.com/intl/en-au/) and [Teams](https://www.microsoft.com/en-au/microsoft-teams/group-chat-software).
* Create custom webhooks.
* Send custom email notifications on events.

{% embed url="<https://youtu.be/tTbq4Dbh-A8>" %}

<figure><img src="/files/zdpblx1zFvb5DDoktfZL" alt=""><figcaption></figcaption></figure>

## Getting Access to Flows

Flows is included in all AttackForge Enterprise plans, and in the AttackForge Core SME plan. For all others plans, Flows can be add-on from the `Administration -> Subscriptions` page.&#x20;

To get started with building a Flow:

* You must have **Create** access to [Event Triggers](#internal-events), [HTTP Triggers](#external-events), [Schedule Triggers](#scheduled-events) or [Action Triggers](#action-events)

As an Administrator, go to `Users > (Select User) > Access > Flows`  and enable access to the desired triggers.

<figure><img src="/files/ociT5xPkjhgXFsO2Gd4X" alt=""><figcaption></figcaption></figure>

### Event Trigger Access Level

> **IMPORTANT**: You will also need [access to the relevent Events](#getting-access-to-events)

* **None** - user is unable to create or import flows with the [Event Trigger](#internal-events) type. Any existing Event Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the [Event Trigger](#internal-events) type they are the owner of, so long as the flow is enabled and they have access to the matching [event type](#internal-events).
* **Create** - user is able to create or import flows with the [Event Trigger](#internal-events) type. Any existing Event Trigger flows for which they are the owner of will run so long as the flow is enabled and the user has access to the matching [event type](#internal-events).

### HTTP Trigger - Authentication (None) Access Level

HTTP Triggers can be [configured with no authentication](#http-trigger-authentication). This means that the flow will not check for authentication before the flow runs.

* **None** - user is unable to create or import flows with the non-authenticated [HTTP Trigger](#external-events) type. Any existing non-authenticated HTTP Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the non-authenticated [HTTP Trigger](#external-events) type they are the owner of, so long as the flow is enabled.
* **Create** - user is able to create or import flows with the non-authenticated [HTTP Trigger](#external-events) type. Any existing non-authenticated HTTP Trigger flows for which they are the owner of will run so long as the flow is enabled.

### HTTP Trigger - Authentication (User API Key) Access Level

HTTP Triggers can be [configured with authentication](#http-trigger-authentication). This means that the flow will check for authentication before the flow runs.

* **None** - user is unable to create or import flows with the authenticated [HTTP Trigger](#external-events) type. Any existing authenticated HTTP Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the authenticated [HTTP Trigger](#external-events) type they are the owner of, so long as the flow is enabled and an authorized [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) is supplied in the nominated header.
* **Create** - user is able to create or import flows with the authenticated [HTTP Trigger](#external-events) type. Any existing authenticated HTTP Trigger flows for which they are the owner of will run so long as the flow is enabled and an authorized [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) is supplied in the nominated header.

### Schedule Trigger

* **None** - user is unable to create or import flows with the [Schedule Trigger](#scheduled-events) type. Any existing Schedule Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the [Schedule Trigger](#scheduled-events) type they are the owner of, so long as the flow is enabled.
* **Create** - user is able to create or import flows with the [Schedule Trigger](#scheduled-events) type. Any existing Schedule Trigger flows for which they are the owner of will run so long as the flow is enabled.

### Action Trigger

* **None** - user is unable to create or import flows with the [Action Trigger](#action-events) type. Any existing Action Trigger flows for which they are the owner of will also not run.
* **Run** - user is able to trigger flows with the [Action Trigger](#action-events) type they are the owner of, so long as the flow is enabled.
* **Create** - user is able to create or import flows with the [Action Trigger](#action-events) type. Any existing Action Trigger flows for which they are the owner of will run so long as the flow is enabled.

### Getting Access to Events

As an Administrator, go to `Users > (Select User) > Access > Events`  and enable access to the desired Events.

<figure><img src="/files/paLutLRdPZoKHXjCSbQB" alt=""><figcaption></figcaption></figure>

## Flow Overview

A Flow is comprised of the following:

* **Name** - the name of the Flow.
* [**Trigger**](#triggers) - the trigger which initiates a [Run](#run-overview).
* [**Actions**](#actions) - a sequence of steps which are executed in order during a [Run](#run-overview).
* [**Secrets**](#secrets) - any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

<figure><img src="/files/Zca3rQsWUfGwH1kFoR4d" alt=""><figcaption></figcaption></figure>

## Run Overview

A Run refers to a single execution of a Flow, meaning when a set of actions defined in your Flow is triggered and carried out from start to finish, that is considered one "Run" of the flow; essentially, it's a single instance of your Flow being executed.&#x20;

Key points about a Run:

* **Triggered by a Trigger -** A Run is initiated by a [Trigger](#triggers), like a new vulnerability or an update to a project, or a manual action.
* **Trackable status -** You can monitor the status of a Run, including whether it succeeded, failed, or is currently running.
* **Provides details -** Each Run has details like start time, duration, and the specific [Actions](#actions) taken within the Flow.

> **IMPORTANT:** A normal Run will only be executed in the context of the [Flow Owner](#sharing-flows-with-teams) and related [Trigger](#triggers). For example, if the [Trigger](#triggers) was "vulnerability-created" - the Run will only initiate for the vulnerability for which the [Flow Owner](#sharing-flows-with-teams) has access to the vulnerability. Test runs can be manually executed with test data at any time.

<figure><img src="/files/t2kI42Ebj347YdvA3Tsk" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/WIa1kQp5RPa16EDFNdrm" alt=""><figcaption></figcaption></figure>

## Sharing Flows with Teams

When a Flow is created, it belongs to the user who created the Flow (the Flow Owner). Flow Owners can be [transferred](#transferring-flows) however a Flow can only ever run under the context of a single user.

Only Flow Owners are allowed to share their Flows with other users.

To share your Flow:

* Open your Flow and click on the `Settings` button

<figure><img src="/files/3oaZyItJbFhJdSGEnPBq" alt=""><figcaption></figcaption></figure>

* Click on `Add Access`

<figure><img src="/files/OtivZxDhPhSDYi6tydsu" alt=""><figcaption></figcaption></figure>

* For Administrators - select either `Roles, Groups or Users`. Users can be selected from a drop-down list. All Groups are available.
* For non-administrators - select either `Groups or Users`. Users must be entered in via email address. Users can only select Groups that they are members of.

<figure><img src="/files/aecuqA1L8JwOY48TUkXe" alt=""><figcaption></figcaption></figure>

Select `Access Level`:

<figure><img src="/files/dZ7S4TTGOYUnWHhnyDaT" alt=""><figcaption></figcaption></figure>

### Access Levels

* **None** - the user explicitly does not have access to the flow.
* **Info** - the user is able to view basic information about the flow, including:
  * Name of the flow
  * Current status, last run, trigger, enabled/disabled
  * Flow owner
  * Readme
  * If [HTTP Trigger](#external-events) - HTTP Method, Trigger URL, Trigger Id&#x20;
* **View** - the user is able to access read-only information for the flow, including:
  * Name of the flow
  * Current status, last run, trigger, enabled/disabled
  * Flow owner
  * Readme
  * If [HTTP Trigger](#external-events) - HTTP Method, Trigger URL, Trigger Id
  * View all [Runs](#runs)
  * View all [Actions](#actions)
  * View [Run details](#run-logs) and logs
  * [Export](#importing-exporting-flows) the flow
* **Edit** - the user is able to modify the flow, including:
  * Name of the flow
  * Current status, last run, trigger, enabled/disabled
  * Flow owner
  * Readme
  * If [HTTP Trigger](#external-events) - HTTP Method, Trigger URL, Trigger Id
  * View all [Runs](#runs)
  * View all [Actions](#actions)
  * View [Run details](#run-logs) and logs
  * Edit the flow
  * Save and Run the flow
  * [Export](#importing-exporting-flows) the flow
  * Disable the flow

Select `Trigger Access`:

<figure><img src="/files/3okF3kr2auWY2ggeQaFU" alt=""><figcaption></figcaption></figure>

### Trigger Access

Trigger access is used for controlling authentication to triggering a [Flow Run](#runs) using authenticated [HTTP Triggers](#external-events) based on the [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) supplied in the nominated header.

* **No** - user is not able to trigger the flow.
* **Yes** - user is able to trigger the flow provided they enter their valid [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) into the flows' nominated authentication header.
* **Yes + Delegate** - user is able to trigger the flow provided they enter their valid [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) into the flows' nominated authentication header. User can also link this Flow into their own Actions, and then share their Actions with other others - allowing other users to be able to implicitly trigger this flow from the shared Action.

For Users - select the `Override`:

<figure><img src="/files/rS6V0ll1ODkeNneifFQn" alt=""><figcaption></figcaption></figure>

### Override

Override is an option that will guarantee the assigned access will prevail for the specified user.&#x20;

This option is particularly useful as Flows can be shared with Roles and Groups, therefore users can inherit access to Flows, and in some cases, this may not be desired for certain users. Overwrite can be used to ensure the user will not be able to access the Flow irrespective of whether they inherited access through their Role or Groups.

Once you are done, click on `Add` to assign the access.

> **IMPORTANT:** Administrators have implicit permissions to view access for all flows.

### Effective Access

Effective access is the place where you can definitively determine which users have access to a Flow - depending on which Roles, Groups and Users have been assigned to the Flow.

Effective access is available to the Flow Owners and can be accessed from the Flow settings page, then click on `Effective Access`:

<figure><img src="/files/WkN7heqlCD7cCVCDb52a" alt=""><figcaption></figcaption></figure>

## Triggers

A Trigger is an action which initiates a [Run](#run-overview). Triggers can be initiated from [Events](/app/modules/self-service-events-api) or manually initiated.

<figure><img src="/files/ty66OFNilMqOwlBfyDo8" alt=""><figcaption></figcaption></figure>

### Internal Events

Internal Events (or Event Triggers) are events which occur when something *inside* AttackForge changes.

The following [Internal Events](https://support.attackforge.com/attackforge-enterprise/modules/self-service-events-api) are currently supported.

Access to events can be granted by Administrators in [`Users > (Select User) > Access > Events (Flows / Self Service API)`](https://support.attackforge.com/attackforge-enterprise/modules/users#self-service-events-api) or via [Groups](https://support.attackforge.com/attackforge-enterprise/modules/groups).

### External Events

External Events (or HTTP Triggers) are events which occur when something *outside* of AttackForge changes.

For example, if an update happens in an external system - that system can use [Webhooks](https://hookdeck.com/webhooks/guides/what-are-webhooks-how-they-work) to send the message to AttackForge in *real-time*.

External Events can also be called from within any other Flow, creating possibilities for modularisation of your flows.

### Scheduled Events

Time-Based Events are events which occur at a specified time for example each day at 9am, or on a specified frequency for example every hour. Time-Based Events are particularly useful when something needs to happen on a automated time basis.

For example, *each day - find all vulnerabilities which have just exceeded their risk-acceptance date, change their status to open, create a ticket in an external system and notify the vulnerability owner(s) and security team by email and by chat message.*

<figure><img src="/files/jMyBT7YhPVYsBAfIxymV" alt=""><figcaption></figcaption></figure>

### Action Events

Action Events are triggered when [Actions](#actions) are clicked by a user within the application user interface.

Action Events can be restricted to `Entities` which helps to scope where Action Events can be accessed from within the application user interface by Actions. This helps to avoid Action Events getting linked to Actions which are not related to the Action Event.

<figure><img src="/files/JpGnYou2x9dW3N9IcV9f" alt=""><figcaption></figcaption></figure>

### Assigning Events

A Flow can be assigned to only one Trigger.&#x20;

Triggers can be assigned to a Flow when either creating or editing the Flow.

<figure><img src="/files/70EXryAar13foAaml1vw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Ln6GhSDCVa6BgD9EbsxR" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/VP2CaIHB6aAw4x26uGlr" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/jMyBT7YhPVYsBAfIxymV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MgWNV2gBm5Zf8AFcWdtn" alt=""><figcaption></figcaption></figure>

### HTTP Trigger - Authentication

[External Events](#external-events) (HTTP Triggers) can be either authenticated or non-authenticated.

Authentication is a way to protect the flow by ensuring that the user who is sending data to the [Trigger URL](#http-trigger-url) is authorized to do so. Authentication is recommended where it is practical to implement on the system sending data to AttackForge, via custom headers.

An authenticated HTTP Trigger flow will check for authentication before the flow runs.

A non-authenticated HTTP Trigger flow will not check for authentication before the flow runs.

Authentication is configured as a custom header. You have control over what the header name should be.

A user interacting with an authenticated HTTP Trigger flow must include their [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) in the specified header. They must also be [granted access to the Flow with Trigger access](#trigger-access), unless they are the [Flow Owner](#sharing-flows-with-teams) whom has implicit access to trigger their flows.

<figure><img src="/files/fsu92nVTXsXF8OxmMyzi" alt=""><figcaption></figcaption></figure>

### HTTP Trigger URL

After creating a HTTP Trigger flow, you will receive the Trigger URL and Trigger Id. The Trigger URL is the URL which your external systems and scripts will use to interact and send data to your flow.

The Trigger URL and Trigger ID are unique for every flow.

<figure><img src="/files/3IUoNsUHze3BdgLvccKm" alt=""><figcaption></figcaption></figure>

> **IMPORTANT**: For non-authenticated HTTP Trigger flows, protect your Trigger URL and Trigger ID as if it was a secret. Without authentication, anybody who knows the URL and HTTP Method can attempt to trigger your flow.

You can rotate your Trigger URL and Trigger ID by clicking on the regenerate button:

<figure><img src="/files/bmBk6GHxNGg1HJDylKX1" alt=""><figcaption></figcaption></figure>

### Trigger Configuration

Triggers have different configuration options which are supported.

<figure><img src="/files/9dqbRcj8QS8yAy8rcint" alt=""><figcaption></figcaption></figure>

* **Redact user API Key** - this option can be used to prevent anybody working on an authenticated HTTP Trigger flow from gaining access to the [User Key](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#user-api-key) supplied in the authentication header, such as through logging.
* **Redact all headers except specified** - this option can be used to specify a [*whitelist*](https://en.wikipedia.org/wiki/Whitelist) of headers which are allowed to be passed into the flow. This option is useful to prevent anybody working on an authenticated HTTP Trigger flow from gaining access to headers which they should not have access to, for example session tokens from external systems.
* **Redact specified headers** - this option can be used to specify a [*blacklist*](https://en.wikipedia.org/wiki/Blacklist_\(computing\)) of headers which will not be passed into the flow. This option is useful to prevent anybody working on an authenticated HTTP Trigger flow from gaining access to headers which they should not have access to, for example session tokens from external systems.

## Secrets

Secrets are any piece of sensitive information that needs to be kept confidential, such as passwords and API keys.

You can create Secrets which belong to the Flow. Only users with access to the Flow would be able to view the associated Secrets.

You can also reference [Secrets which belong to users](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#secrets). User Secrets make it easy to rotate passwords and credentials without having to update flows.

To create a Secret, start by clicking on the `Secrets` button when creating or editing a Flow.

<figure><img src="/files/cX2njKYNLjHVIlqpLbw7" alt=""><figcaption></figcaption></figure>

From here, you can see and manage all of the existing Secrets associated to the Flow.

Click on `Add Secret` to create a new Secret.

<figure><img src="/files/95O3Lt01fsh43yxHeaeM" alt=""><figcaption></figcaption></figure>

Note the Key must be letters, numbers and underscores only.

You can create a Local Secret:

<figure><img src="/files/uSYyHyp1owOWejBY9y7c" alt=""><figcaption></figcaption></figure>

Or select from one of your [User Secrets](https://support.attackforge.com/attackforge-enterprise/getting-started/manage-user#secrets):

<figure><img src="/files/VxlcacAWCJlaCOivVWrt" alt=""><figcaption></figcaption></figure>

You can also view, manage and create secrets in the [Request Script](#request-script) and in the [Response Script](#response-script):

<figure><img src="/files/HmG5yPXnhKTngYwfCmvc" alt=""><figcaption></figcaption></figure>

> **NOTE:** Secrets are stored encrypted in the database.

There are two (2) ways in which you can refer to your Secrets in your Flow:

1. Select the Secret directly in the [Headers](#headers)
2. Refer to the Secret in the [Request Script](#request-script) or [Response Script](#response-script)

### Secrets in Headers

When creating or modifying [Headers](#headers) within the [Action](#actions), you can select 'Secret' for the header type. This will then allow you to select from an existing Secret, or create a new Secret.

<figure><img src="/files/MXkyNl3J5ZAVW60IiKfV" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/DLb2sHMU5q1XJLg8d7GD" alt=""><figcaption></figcaption></figure>

### **Secrets in Request/Response Scripts**

When creating or modifying the [Request Script](#request-script) or the [Response Script](#response-script), you can refer to secrets using the following syntax:

```javascript
secrets.<KEY>
```

Where `<KEY>` is replaced with the Key associated with the Secret.

> **IMPORTANT**: Make sure to select `Use Secrets` to ensure your secrets are used in your script.

<figure><img src="/files/AjwPiYIhFXwy2kJhYTyF" alt=""><figcaption></figcaption></figure>

## Actions

Actions are either one activity, or a sequence of activities, which are executed in order during a [Run](#run-overview).

For example, if the use case for your Flow is:

* *to create a JIRA Issue every time a Vulnerability is created*

You may choose to include two (2) Actions in your Flow:

* **Action 1 - Create JIRA Issue**
  * This involves formatting the vulnerability into the necessary [JIRA Create Issue API](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post) format, and making a HTTPS request to the JIRA API to create the issue.
* **Action 2 - Update Vulnerability with JIRA Issue Key**
  * This involves making a HTTPS request to the [Update Vulnerability Self-Service API](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/updatevulnerability) to set the JIRA Issue Key custom field.

### Action Types

The following Action Types are supported:

* [**HTTP Action**](#http-action)
  * The primary purpose of a HTTP Action is to make a HTTP request. The HTTP request can be against the AttackForge [Self-Service APIs](/app/modules/self-service-restful-api) or any external system or endpoint outside of AttackForge.
* [**Script Action**](#script-action)
  * The primary purpose of a Script Action is to execute user-defined logic.

<figure><img src="/files/M5X6K8qe8MKTxUn66LXf" alt=""><figcaption></figcaption></figure>

### Script Action

Script Actions can be leveraged to execute user-defined logic.

Script Actions can receive input from [Data](#data) and output [Data](#data) into the proceeding Action.

In the example below:

* **Action 1 (HTTP)**: Retrieves vulnerabilities from the AttackForge Self-Serviced APIs.
* **Action 2 (Script)**: Takes the vulnerabilities from Action 1; groups them into the required format, then passes them into Action 3.
* **Action 3 (HTTP)**: Sends the formatted vulnerabilities to an external security platform.

<figure><img src="/files/byJZwX5ww6ZuaeAtOJ7a" alt=""><figcaption></figcaption></figure>

The Script Action contains a single code editor where the script can be input.&#x20;

<figure><img src="/files/B5EER3Hr4V3vvLF0dXkr" alt=""><figcaption></figcaption></figure>

### HTTP Action

Every HTTP Action is made up of a [**Request**](#request) and a [**Response**](#response).

* The [Request](#request) is the HTTP request which is made by the Action.
* The [Response](#response) is the HTTP response from the server which received the HTTP request.

Every HTTP Action is made up of the following components:

* [**Method**](#methods) - this is the HTTP method i.e. GET, POST, PUT, etc. that will be used for the [Request](#request)
* [**URL**](#url) - this is the URL that will be used for the [Request](#request).
* **Verify Certificate** - this determines whether to verify if the TLS certificate is valid for the URL.
* [**Headers**](#headers) - these are the headers that will be sent when the [Request](#request) is made.
* [**Request Script**](#request-script) - this is the script which will execute *before* the [Request](#request) is made.
* [**Response Script**](#response-script) - this is the script which will execute *after* the [Response](#response) is returned.

> **IMPORTANT:** When more than one Action is included in a Flow, the *output* of an Action will become the *input* into the next Action.

<figure><img src="/files/ddw04nUtDUW0tOKLj1aT" alt=""><figcaption></figcaption></figure>

### Methods

Methods are the HTTP methods/verbs that will be used for the [Request](#request) i.e. GET, POST, PUT, etc.

The following methods are supported:

* GET
* POST
* PUT
* PATCH
* DELETE

Methods can be selected when editing the HTTP [Action](#actions):

<figure><img src="/files/AQ1DZ92nwI6Oli3E2kT4" alt=""><figcaption></figcaption></figure>

Methods can also be programatically set in your [Request Script](#request-script) in the [Return Statement](#the-return-statement):

```javascript
return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': secrets.jira_auth
        },
        body: {
            fields: {
                summary: summary,
                description: description,
                priority: {
                    name: priority
                },
                issuetype: {
                    name: 'Bug'
                },
                labels: labels
            }
        }
    }
};
```

### URL

The URL is the web address that will be used for the [Request](#request), for example `https://acmecorp.atlassian.net/rest/api/2/issue`

The URL can be entered in when editing the HTTP [Action](#actions):

<figure><img src="/files/pg40LBxE8jTvTHamE5oc" alt=""><figcaption></figcaption></figure>

The URL can also be programatically set in your [Request Script](#request-script) in the [Return Statement](#the-return-statement). This is useful if your URL has a dynamic component which needs to be computed:

```javascript
const url = 'https://demo.attackforge.com/api/ss/vulnerability/' + data.vulnerability_id;

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
            'x-ssapi-key': secrets.af_auth
        },
        body: {
            project_id: project_id,
            custom_fields: [
                {
                    key: 'jira_issue_key',
                    value: jira_issue_key
                }
            ]
        }
    }
};
```

### Headers

The Headers are the HTTP headers that will be sent when the [Request](#request) is made.

The Headers can be manually entered in when editing the HTTP [Action](#actions):

<figure><img src="/files/I4pdAxBiENkdcYy26XTN" alt=""><figcaption></figcaption></figure>

The Headers can also be programatically set in your [Request Script](#request-script) in the [Return Statement](#the-return-statement). This is useful if your Headers have a dynamic component which needs to be computed:

```javascript
const customHeaderName = 'X_CUSTOM_HEADER_' + customHeader;
const customHeaderValue = customValue;

return {
    decision: { 
        status: 'continue'
    },
    request: {
        url: url,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            customHeaderName: customHeaderValue,
            'Authorization': secrets.custom_auth
        },
        body: {
            super_secret_something: "..."
        }
    }
};
```

### Request

The Request is a HTTP/HTTPS request to a web address.&#x20;

The Request is made up of the following components:

* [URL](#url)
* [Method](#methods)
* [Headers](#headers)
* Body - an optional HTTP body. This is typically required for POST, PUT and PATCH HTTP requests.
* [Request Script](#request-script)

> **IMPORTANT:** Requests can be made over both HTTP and HTTPS.

### Response

The Response is the HTTP server response to a [Request](#request).

The Response is made of the of the following components:

* [Response Object](#the-response-object)
* [Response Script](#response-script)

### Request Script

The Request Script is the script which will execute *before* the [Request](#request) is made.

The Request Script is made up of the following components:

* [Code](#code)
* [Data](#data)
* [Return Statement](#the-return-statement)

### Response Script

The Response Script is the script which will execute *after* the [Response](#response) is returned.

The Request Script is made up of the following components:

* [Code](#code)
* [Data](#data)
* [Return Statement](#the-return-statement)

### Code

Flows support [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) - a powerful interpreted programming language created by AttackForge.

This makes it possible to write logic to help you handle all various use cases for how you want your Flows to work.

<figure><img src="/files/sCeCCnRe6ghDWDiBcHC2" alt=""><figcaption></figcaption></figure>

You can take advantage of [Logging](https://support.attackforge.com/attackforge-enterprise/afscript#logging) in [AFScript](https://support.attackforge.com/attackforge-enterprise/afscript) to help you to debug and test your code.

<figure><img src="/files/C4KDpJOSgOCqwyjPbA7s" alt=""><figcaption></figcaption></figure>

You can test and debug your code using the `Run` option:

<figure><img src="/files/kRIKdJSVMu8PEtpCtAcN" alt=""><figcaption></figcaption></figure>

If your code fails after running it, you will see an error message with the relevant stack trace:

<figure><img src="/files/sexGxqLT0jXQr1GC3q04" alt=""><figcaption></figcaption></figure>

### Data

Data is contextually relevent information for your [Request Script](#request-script) and [Response Script](#response-script).

You can reference the information included within Data as follows:

```javascript
data.<KEY>
```

Where `<KEY>` is replaced with the associated key on the [Data Object](#the-data-object).

For more information on Data, please see [Data Object](#the-data-object).

### The Data Object

[Data](#data) is contextually relevant information for your [Request Script](#request-script) and [Response Script](#response-script).

The Data Object is an [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) that holds the [Data](#data).

The first [Action](#actions) in your Flow will contain [Data](#data) in the [Request Script](#request-script) which is relevent to your [Trigger Event](#triggers). For example, if your Flow was assigned to the "vulnerability-created" Event, then your Data Object will contain all of the information relating to the vulnerability.

However from this point forward, you can control how you would like your Data Object to look for the [Response Script](#response-script) and any subsequent [Actions ](#actions)going forward.

In the following example, we can see that Data Object has vulnerability-related information due to the "vulnerability-created" Event.

<figure><img src="/files/5V7dNomFnuqAkzwhUGgM" alt=""><figcaption></figcaption></figure>

You can refer to keys on the Data Object using the following syntax:

```javascript
data.<KEY>
```

Using the example above, if you wanted to store the vulnerability Id in a constant, you could do the following:

```javascript
const vuln_id = data.vulnerability_id;
```

Keeping with the example above, if you wanted to extract the project Id for the vulnerability, you could do the following:

```javascript
let project_id = undefined;

if (data.vulnerability_projects) {
    for (let x = 0; x < data.vulnerability_projects.length; x++) {
        if (data.vulnerability_projects[x].id) {
            project_id = data.vulnerability_projects[x].id;
        }
    }
}
```

If you needed to pass this information to the next step of this Flow, which using the example above will be the [Response Script](#response-script) on the first [Action](#actions) - you can include the "data" key in your [Response Object](#the-response-object) and pass in an [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) with key/value pairs as follows:

```javascript
return {
    decision: { 
        status: 'continue'
    },
    data: {
        af_project_id: project_id,
        af_vuln_id: vuln_id,
        af_vuln: data
    }
}
   
```

Continuing with this example, the [Response Script](#response-script) will now have the following Data Object:

```javascript
data = {
    af_project_id: "...",
    af_vuln_id: "...",
    af_vuln: {
        "vulnerability_title": "...",
        ...
    }
}
```

When viewing the details of a [Run](#run-overview) - you can see what [Data](#data) was passed as input and output into an [Action.](#actions)

<figure><img src="/files/A1BYMUSzIKKs3RtUW9kW" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/IMrnsIZV8UQnBK27MzPC" alt=""><figcaption></figcaption></figure>

If you would need to log the Data Object for visibility or debugging during execution of a [Run](#run-overview) you can do the following:

```javascript
Logger.debug('Data:');
Logger.debug(JSON.stringify(data));
```

You can then view the details in the [Run Logs](#runlogs)

<figure><img src="/files/3oVZrqIAlWBhdcO1H7dw" alt=""><figcaption></figcaption></figure>

When working on [Action Events](#action-events) - you can select which test context(s) you would like to access for the purposes of building and testing your flows.

For example, if your Action Event was restricted to `Project` and `Projects` entities - you can load a test context for either entity:

<figure><img src="/files/I8T03KIERHmWzZzEFA7E" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/0D66Qn9gpUWwcwrr0tpC" alt=""><figcaption></figcaption></figure>

### The Response Object

The Response Object is the HTTP information which is sent back from the server during the [Response](#response).

The Response Object is available in the [Response Script](#response-script).

The Response Object is made up of the following:

* [HTTP Response Status Code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
* [HTTP Response Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
* [HTTP Response Body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)

<figure><img src="/files/IigEaeRub3H0uRnqf7y8" alt=""><figcaption></figcaption></figure>

You can refer to keys on the Response Object using the following syntax:

```javascript
response.<KEY>
```

An example of the Response Object:

```json
response = {
    "statusCode": 200,
    "headers": {},
    "body": ""
}
```

* The **statusCode** will be accessible as a [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number).
* The **headers** will be accessible as a [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object).
* The **body** will be accessible as a [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String).

If you are expecting the body to be returned as a [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) payload (which is common for [RESTful APIs](https://developer.mozilla.org/en-US/docs/Glossary/REST)) - you must first parse the body into JSON format before you can access it using [dot or bracket notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors), see example below:

```javascript
const body = JSON.parse(response.body);
```

When viewing the details of a [Run](#run-overview) - you can see the [Response](#response) [Status Code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) and Response Headers and Response Body:

<figure><img src="/files/4IiDPc1KOrc7toqX8l4B" alt=""><figcaption></figcaption></figure>

If you would need to log the Response Object for visibility or debugging during execution of a [Run](#run-overview) you can do the following:

```javascript
Logger.debug('Response:');
Logger.debug(JSON.stringify(response));
```

You can then view the details in the [Run Logs](#runlogs)

<figure><img src="/files/j4GgbWuIL49jhf0xydXp" alt=""><figcaption></figcaption></figure>

### The Return Statement

The Return Statement is the action to take for your [Request Script](#request-script) and [Response Script](#response-script).

<figure><img src="/files/fGChWfyk0AUbP8IkUtMT" alt=""><figcaption></figcaption></figure>

The Return Statement is made up of the following components:

* [Decision](#decision)
* [Request](#request)
* [Data](#data-1)

```javascript
return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }, 
   request: { 
     url: 'https://www.attackforge.com/api',
     body: {}, 
     headers: {}, 
     method: 'GET', 
   }, 
   data: {} 
};
```

#### Decision

The following Flow Return Decisions are supported:

* Continue
* Finish
* Abort
* Next
* Repeat

```javascript
return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }
};
```

The decision is the action that your script will take. A decision is made up of the following components:

* status - a supported status (see below)
* message - an optional message to display in the logs
* delay - an optional number in milliseconds to wait until proceeding with the [Request](#request) or the next [Action](#actions). The minimum delay is 0. The maximum delay is 86400000 (24 hours).

```javascript
return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, wait 5 seconds then proceed to submit request',
     delay: 5000 
   }
};
```

You can also include the decision as a string if you do not need to include a message:

```javascript
return {
   decision: "continue" //also supports "next", "abort", "finish"
};
```

The following statuses are supported:

**CONTINUE**

Continue will instruct your script to continue with normal execution. For example, continuing in the [Request Script](#request-script) will result in the [Request](#request) being made. Continuing in the [Response Script](#response-script) will result in executing the next [Action](#actions).

Example using Continue:

```javascript
return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }
};
```

**NEXT**

Next will instruct your [Request Script](#request-script) or [Response Script](#response-script) to move to the next [Action](#actions). This is useful if a [Request](#request) in your Flow is conditional i.e. it may or may not need to be made.

Example using Next:

```javascript
return {
   decision: { 
     status: 'next', 
     message: 'Asset already exists. Move to create vulnerability', 
   }
};
```

**REPEAT**

Repeat will instruct your [Action](#actions) to repeat. This is useful for the following use cases:

* Create a 'for loop' Action over a list of data - for example create a new vulnerability for each record in a long list.
* Interact with paginated endpoints to retrieve the full list of results.
* Request failed; update the payload and try again.

When you repeat an action, you can also modify its [Data](#the-data-object). This means you can pass in new data and context to the Action.

**Example Action using Repeat**&#x20;

***Request Script***

<figure><img src="/files/C1OdGgY4p6yehLJHMv6U" alt=""><figcaption></figcaption></figure>

***Response Script***

<figure><img src="/files/eZ6zlZqeZbZquLn0JzCF" alt=""><figcaption></figcaption></figure>

***Example 1: Process list of data***

Say you have 100 vulnerabilities in a list. The purpose of your Action is to create a new vulnerability.

On the *first iteration* of the Action, you create vulnerability `1 out of 100` - leaving 99 more to go.

You can then repeat the Action, updating the list to remove the vulnerability which was just created.

On the *second iteration* of the Action, you create vulnerability `2 out of 100` - leaving 98 more to go.

This process repeats until you have 0 vulnerabilities left in the list to process, then you call *next* to move on to the next Action in your Flow, or *finish* to gracefully end your Flow.

***Example 2: Interact with paginated endpoints***

Say you have 1000 vulnerabilities you need to access, however the page length of the API endpoint only returns 50 at a time. The purpose of your Action is to fetch a page of vulnerabilities.

On the *first iteration* of the Action, you fetch vulnerabilities `1 to 50 out of 1000` - leaving 950 more to go.

You can then repeat the Action, updating the page marker to fetch the next page.

On the *second iteration* of the Action, you fetch vulnerabilities `51 to 100 out of 1000` - leaving 900 more to go.

This process repeats until you have 0 pages left to fetch, then you call *next* to move to the next Action in your Flow, or *finish* to gracefully end your Flow.

When an Action repeats, the logs for each iteration will be visible to you in the [Flow Run](https://support.attackforge.com/attackforge-enterprise/modules/flows#run-overview) logs.

<figure><img src="/files/OL4ui9Sqa5AiYhXfn4hr" alt=""><figcaption></figcaption></figure>

**ABORT**

Abort will terminate your Flow as an error condition. This is useful in cases where your Flow can no longer proceed due to various reasons.

Example using Abort:

```javascript
return {
   decision: { 
     status: 'abort', 
     message: 'Auth token not generated. Check if credentials are valid?', 
   }
};
```

**FINISH**

Finish will terminate your Flow as an success condition. This is how you would normally terminate a Flow.

Example using Finish:

```javascript
return {
   decision: { 
     status: 'finish', 
     message: 'Vulnerability created!', 
   }
};
```

#### Request

The Request is the HTTP Request information that your [Request Script](#request-script) will use. Request is made up of the following components:

* url
* body
* headers
* method

```javascript
return {
   decision: { 
     status: 'continue', 
     message: 'Payload is valid, proceed to submit request', 
   }, 
   request: { 
     url: 'https://www.attackforge.com/api',
     body: {}, 
     headers: {}, 
     method: 'GET', 
   }
};
```

**URL**

The URL is the URL that the [Request](#request) will be sent to. This field is optional. If it is not specified, the [URL](#url) set in the [Action](#actions) will prevail. If it is specified, it will take precedence over the [URL](#url) set in the [Action](#actions).

**BODY**

The Body is the payload body that will be sent in the [Request](#request). This field is optional. If it is not specified, no body will be sent in the [Request](/app/modules/self-service-restful-api/getprojectrequest#request).

Example with a JSON Body:

```javascript
return {
    decision: { 
        status: 'continue'
    },
    request: {
        body: {
            fields: {
                project: {
                    key: jiraProjectKey
                },
                summary: summary,
                description: description,
                priority: {
                    name: priority
                },
                issuetype: {
                    name: 'Bug'
                },
                labels: labels
            }
        }
    }
};
```

**HEADERS**

The Headers are the HTTPS Headers that the [Request](#request) will use. This field is optional. If it is not specified, the [Headers](#headers) set in the [Action](#actions) will prevail. If it is specified, it will take precedence over the [Headers](#headers) set in the [Action](#actions).

Example Headers:

```javascript
return {
    decision: { 
        status: 'continue',
    },
    request: {
        headers: {
            'Content-Type': 'application/json',
            'x-ssapi-key': secrets.af_auth,
        }
    }
};
```

**METHOD**

The Method is the HTTPS Method that the [Request](#request) will be sent to. This field is optional. If it is not specified, the [Method](#methods) set in the [Action](#actions) will prevail. If it is specified, it will take precedence over the [Method](#methods) set in the [Action](#actions).

Example Headers:

```javascript
return {
    decision: { 
        status: 'continue',
    },
    request: {
        method: 'GET' //supports GET, POST, PUT, PATCH, DELETE
    }
};
```

#### Data

Data is an object that can be used to pass information between [Request Script](#request-script) to [Response Script](#response-script), and from [Response Script](#response-script) to the next [Action](#actions).

The Data in the [Request Script](#request-script) for the first [Action](#actions) of the Flow will be the Event information, for example "vulnerability-created" fields. From then onwards, you can override what the data will be in the [Response Script](#response-script) and beyond.

Example with Data in the [Request Script](#request-script) of the first [Action](#actions) in the Flow:

```javascript
return {
    decision: { 
        status: 'continue',
    },
    data: {
        af_project_id: afProjectId,
        af_vuln: data
    }
};
```

Example with Data in the [Response Script](#response-script) of the first [Action](#actions) in the Flow. This example will pass on the "af\_project\_id" and "af\_vuln" that was passed in the Data from the [Request Script](#request-script) on to the next [Action](#actions) in the Flow.

```javascript
return {
    decision: { 
        status: 'continue',
    },
    data: {
        af_project_id: data.af_project_id,
        af_vuln: data.af_vuln
    }
};
```

### Downloading Files using Flows

Flows can be used to download [HTTP Response](#response) data as a file.

To download data from a HTTP Response, open the [Action](#actions) - click on **Options** then select **Download Response**.

<figure><img src="/files/GJbcqtnHyqk1Fdk0xU0y" alt=""><figcaption></figcaption></figure>

Then inside the HTTP Response, the response will contain a **fileId** which is the reference to the file after Flows has automatically downloaded the file. You can then refer to this fileId in subsequent Actions to use the file, for example upload it to some place.

<figure><img src="/files/MBz92ZgcqKqAnit63g91" alt=""><figcaption></figcaption></figure>

### Uploading Files using Flows

Flows can be used to upload [previously downloaded files](#downloading-files-using-flows).

To upload a file, you must have first downloaded the file and stored the **fileId** - see [Downloading Files using Flows](#downloading-files-using-flows).

Once you have the **fileId**, you can upload it using a **multipart/form-data** [HTTP Request](#request).

<figure><img src="/files/l3YlehAccaADMSDFmIeT" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Q03RMcSDQfonnhyiXgf8" alt=""><figcaption></figcaption></figure>

## Runs

Flow Runs is where you can view the history of each [Run](#run-overview) you have access to, across all of your Flows. It is a consolidated view for every [Run](#run-overview). You can access this page from the Flows module.

<figure><img src="/files/KY6dPs5EGqQhEaT8b1iG" alt=""><figcaption></figcaption></figure>

You can also view [Runs](#run-overview) for a specific Flow by clicking on Flows and then clicking on the name of a Flow.

<figure><img src="/files/trg3b9vrE2zBkOmgoW1R" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/e0niBn4iOWlJfrl13tN8" alt=""><figcaption></figcaption></figure>

### Run Logs

When clicking on a [Run](#run-overview), you will see an overview of the history for that [Run](#run-overview), including the following information:

* **Run Status -** Whether the [Run](#run-overview) was Completed or Failed.
* **Event -** the related Event which triggered the Flow.
* **Started -** the timestamp of when the [Run](#run-overview) started execution.
* **Duration -** the duration (in milliseconds) for execution of the [Run](#run-overview) until completion or failure.
* **Actions -** the [Actions](#actions) in-scope during the [Run](#run-overview).
* **Data -** the input and output of each [Action](#actions).
* **HTTP -** the [URL](#url), [Method](#methods), [Headers](#headers), [Request Body](#request-script), Response Status Code and [Response Body](#response-script) for each [Action](#actions) in the Flow.
* **Logs -** the logs for each [Action](#actions).

<figure><img src="/files/vHyQZlpteyPEN8Uw9stb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/QZCZx362srynCzv9KM8v" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/jIy3QubX5jNKHyH1kx6C" alt=""><figcaption></figcaption></figure>

If you include [Logging](https://support.attackforge.com/attackforge-enterprise/afscript#logging) in your [Request Script](#request-script) or [Response Script](#response-script), you will be able to see the logs here during execution of a Flow.

<figure><img src="/files/cKqW3ARwtwtPv3KwGZkE" alt=""><figcaption></figcaption></figure>

### Manually Run Flow

You can manually run a Flow at any time. This is useful for testing your Flow. When you manually run a Flow, the input into the first [Action](#actions) will be test data. You can modify the test data to match your testing needs.

<figure><img src="/files/T2mtNmP8xhV087RNo9Gt" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/vzSpTppeyrNyWaLBOVxD" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/XsYDk1aka0wh6bEeZrIQ" alt=""><figcaption></figcaption></figure>

### Re-Run

You can manually re-run a Flow at any time. When you Re-Run a Flow, it will execute with exactly the same input data into the first [Action](#actions).&#x20;

<figure><img src="/files/qMbFFclxIr8RlGBYyLYF" alt=""><figcaption></figcaption></figure>

After a Flow has Re-Run, you will notice the [Event Trigger](#triggers) will show that it was Re-Run.

<figure><img src="/files/dRdlPg3JN0SsIoXio0RE" alt=""><figcaption></figcaption></figure>

### Accessing Previous Runs

When a HTTP Triggered Flow is invoked, the Flow will Run asynchronously.

Due to the asynchronous nature of the [Flow Run](#runs), will you immediately receive a **Flow Run Id** in the response:

<figure><img src="/files/JgDqSQySAwxMQgrYF425" alt=""><figcaption></figcaption></figure>

The Flow Run Id can then be used to poll and fetch the results of the Flow Run once it has reached its terminal state.

To fetch the results of the Flow Run, you would need to make the following API request:

```bash
curl --request GET \
	--url 'https://{{tenant}}/api/flows/runs/{{flowRunId}}' \
	--header 'content-type: application/json' \
	--header 'x-user-key: {{api-key}}'
```

The result of the Flow Run will then be returned:

<figure><img src="/files/50PJZWAELdUZtWlEOBj6" alt=""><figcaption></figcaption></figure>

And we can confirm that this is what should have been returned based on a successful Flow Run:

<figure><img src="/files/MtPkk1eYdSWjjFEmUoov" alt=""><figcaption></figcaption></figure>

If accessing the Flow through the browser, for example if the Flow is a GET request and attached to a button in an email - you can set a user friendly response page by appending `?format=html` to the URL:

```bash
curl --request GET \
	--url 'https://{{tenant}}/api/flows/runs/{{flowRunId}}?format=html' \
	--header 'content-type: application/json' \
```

A `successful` response will appear as follows:

<figure><img src="/files/gY625OnVzLW23pxKqTJl" alt=""><figcaption></figcaption></figure>

A `failed` response will appear as follows:

<figure><img src="/files/T3NEPNdHIJbUXOngmaBF" alt=""><figcaption></figcaption></figure>

### Flow Run Statuses

The following Flow Run Statuses are available:

* **Running -** the Flow is currently running.
* **Completed -** the Flow Run has completed.
* **Failed** - the Flow Run has failed. This happens when a Flow Return decision is set to Abort; or when there is a runtime error in the script.
* **Dropped** - the Flow Run was detected as a duplicate and dropped.
* **Missed** - the Flow Run was scheduled, however did not run as per schedule, for example if the AttackForge server was restarted.
* **Terminated** - the Flow Run was manually terminated.

## Flow Readme

Every flow can have a readme which helps to detail key information such as:

* How the flow works
* How to operate the flow
* Links to external documentation
* Contact information for key persons

<figure><img src="/files/4rF9DB9GfykWQbgBMbzE" alt=""><figcaption></figcaption></figure>

You can create and edit the readme when creating or editing the flow:

<figure><img src="/files/kGYM1JvCEWW09ThKLpjL" alt=""><figcaption></figcaption></figure>

## Importing/Exporting Flows

You can export your existing Flows and import Flows at any time. This utility can help to:

* Share Flows with others, without giving them access to your Flows
* Create a backup of your Flows
* Bootstrap a new Flow based on a template

To export a Flow, click on the `Export Flow` button. The Flow will be exported with a `.flow` extension format.

> **IMPORTANT**: Exported Flows do not contain the values of [Secrets](#secrets). However for compatibility and convenience, the [Secret](#secrets) key/name will be included in the export.

<figure><img src="/files/nEvhiDBybI954ox126dE" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Tx7Mco06s4lHaMHO9cEV" alt=""><figcaption></figcaption></figure>

To import a Flow, click on `Import Flow`:

<figure><img src="/files/jz5ctLKAYkmnUaLSk110" alt=""><figcaption></figcaption></figure>

Select the Flow you would like to import (.flow file):

<figure><img src="/files/PemG1ctzfSE9VZTkqpwz" alt=""><figcaption></figcaption></figure>

You will have an opportunity to review and adjust the Flow prior to saving.

<figure><img src="/files/k7pzWg1vFjSeR3GZrB8n" alt=""><figcaption></figcaption></figure>

## Transferring Flows

Flows run under the context of an Owner. That means, when the event happens for the Owner, the Flow will trigger.

You can transfer ownership of Flows when required, so that the Flow can operate under the context of another user.

To transfer ownership of your Flow - open the Flow settings page and click on `Transfer`

<figure><img src="/files/Od8YBkOXZeRcO4DUj6wq" alt=""><figcaption></figcaption></figure>

Select the user or enter in their email address and click `Transfer`

<figure><img src="/files/nY2evZ1x815IdsKfyKS0" alt=""><figcaption></figcaption></figure>

The user will immediately receive ownership of the Flow, however it will be disabled until they review the Flow and chose to enable it.


# Atlassian JIRA

## Create JIRA Issue

{% embed url="<https://youtu.be/-BfrTnCIoi0?si=2DqkyNw5Czb2b9X5>" %}

The purpose of this example is to create a [JIRA Issue](https://support.atlassian.com/jira-software-cloud/docs/what-is-an-issue/) when a Vulnerability is created in AttackForge, and to update AttackForge to assign the JIRA Issue Key against the Vulnerability.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).
  * jira\_auth - your [JIRA API token](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/)

**Action 1 - Create JIRA Issue**&#x20;

* **Method**: POST
* **URL**: https\://\<YOUR-JIRA>/rest/api/2/issue
* **Headers**:
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_auth
* **Request Script**:

```javascript
let jiraProjectKey = '';
let afProjectId = '';

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    const project = data.vulnerability_projects[i];

    if (project.custom_fields) {
      for (let j = 0; j < project.custom_fields.length; j++) {
        const projectCustomField = project.custom_fields[j];

        if (projectCustomField.key === 'jira_project_key') {
          jiraProjectKey = projectCustomField.value;
          break;
        }
      }
    }

    if (project.id) {
      afProjectId = project.id;
    }

    if (afProjectId && jiraProjectKey) {
      break;
    }
  }
}

jiraProjectKey = String.replace(jiraProjectKey, m/"/g, '\"');
jiraProjectKey = String.replace(jiraProjectKey, m/\\"/g, '\"');

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody(jiraProjectKey)
  }
};

function buildRequestBody(jiraProjectKey) {
  let summary = '';

  if (data.vulnerability_title) {
    summary = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vulnerability_title + ' \n\n ';
  }

  if (data.vulnerability_description) {
    description += '*Description* \n ' + data.vulnerability_description + ' \n\n ';
  }

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data.vulnerability_priority && data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vulnerability_priority + ' \n\n ';
    }
  }

  if (data.vulnerability_attack_scenario && data.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vulnerability_attack_scenario + ' \n\n ';
  }

  if (data.vulnerability_affected_asset_name) {
    description += '*Affected Asset* \n\n * ' + data.vulnerability_affected_asset_name + ' \n\n ';
  }
  else if (data.vulnerability_affected_assets) {
    description += '*Affected Asset(s)*: \n';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n * ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

  if (data.vulnerability_notes) {
    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  Logger.debug(JSON.stringify(tags));

  if (tags.length > 0) {
    description = description + '*Tags* \n\n ';
    
    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }    
  }

  description = String.replace(description, m/"/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      project: {
          key: jiraProjectKey
      },
      summary: summary,
      description: description,
      priority: {
          name: priority
      },
      issuetype: {
          name: 'Bug'
      },
      labels: labels
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing JIRA Issue Key (body.key)',
    }, 
  };
}
else if (!data?.af_project_id) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }  

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  if (data) {
    Logger.error(JSON.stringify(data));
  } 

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data.af_vuln_id,
      jira_issue_key: body.key
    } 
  };
}
```

**Action 2 - Update AF Vuln with JIRA Issue Key**

* **Method**: PUT
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-Key; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (data?.jira_issue_key && data?.af_vuln_id && data?.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.com/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
          project_id: data.af_project_id,
          custom_fields: [
            {
              key: 'jira_issue_key',
              value: data.jira_issue_key
            }
          ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}
```

## Update JIRA Issue

{% embed url="<https://youtu.be/2bps7vEcmVA?si=LbU0ywrOa1_J33Jl>" %}

The purpose of this example is to update a [JIRA Issue](https://support.atlassian.com/jira-software-cloud/docs/what-is-an-issue/) when a Vulnerability is updated in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event**: Vulnerability Updated
* **Secrets**:
  * jira\_auth - your [JIRA API token](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/)

**Action 1 - Get JIRA Issue**&#x20;

* **Method**: GET
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_auth
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted.',
      }
  };
}

let jiraIssueKey;

if (data.vulnerability_custom_fields) {
  for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
    if (data.vulnerability_custom_fields[i].key === 'jira_issue_key') {
      jiraIssueKey = data.vulnerability_custom_fields[i].value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'no JIRA Issue Key found',
      }
  };
}

if (jiraIssueKey) {
  return {
    data: {
      vuln: data
    },
    request: {
      url: 'https://attackforge.atlassian.net/rest/api/3/issue/' + jiraIssueKey,
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'finish',
      message: 'missing JIRA Issue Key (body.key) - Issue likely deleted',
    }
  };
}
else if (!data?.vuln) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.vuln',
    },
  };
}
else {
  return {
    data: {
      vuln: data?.vuln,
      jira_issue_key: body?.key
    } 
  };
}
```

**Action 2 - Update JIRA Issue**

* **Method**: \<defined in Request Script>
* **URL**: \<defined in Request Script>
* **Headers**:
  * \<defined in Request Script>
* **Request Script**:

```javascript
if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'finish',
      message: 'no JIRA Issue Key found',
    }
  };
}
else if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'missing data.vuln',
    }
  };
}
else {
  let url = 'https://attackforge.atlassian.net/rest/api/2/issue/' + data.jira_issue_key;

  return {
    request: {
      url: url,
      body: buildRequestBody(),
    }
  };
}

function buildRequestBody() {
  let summary = '';

  if (data.vuln.vulnerability_title) {
    summary = '[SECURITY][VULNERABILITY] ' + data.vuln.vulnerability_title;
  }

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data?.vuln?.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vuln.vulnerability_title + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_description) {
    description += '*Description* \n ' + data.vuln.vulnerability_description + ' \n\n ';
  }

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data?.vuln?.vulnerability_priority && data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      const tag = data.vuln.vulnerability_tags[i];

      if (tag =~ m/CVSSv3.1 Base Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Base Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Temporal Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Temporal Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Environmental Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Environmental Score: ', '');
      }

      if (tag =~ m/CVSS: 3.1\//) {
        cvssVector = String.replace(tag, 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vuln.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vuln.vulnerability_priority + ' \n\n ';
    }
  }

  if (data?.vuln?.vulnerability_attack_scenario && data?.vuln?.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vuln.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vuln.vulnerability_attack_scenario + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_affected_asset_name) {
    description += '*Affected Asset* \n\n * ' + data.vuln.vulnerability_affected_asset_name + ' \n\n ';
  }
  else if (data?.vuln?.vulnerability_affected_assets) {
    description += '*Affected Asset(s)*: \n';

    for (let i = 0; i < data.vuln.vulnerability_affected_assets.length; i++) {
      if (data.vuln.vulnerability_affected_assets[i].asset?.name) {
        description += '\n * ' + data.vuln.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data?.vuln?.vulnerability_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vuln.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

  if (data?.vuln?.vulnerability_notes) {
    for (let i = 0; i < data.vuln.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vuln.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vuln.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vuln.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vuln.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vuln.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  if (tags.length > 0) {
    description += '*Tags* \n\n ';

    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }
  }

  description = String.replace(description, m/ "/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data?.vuln?.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data?.vuln?.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data?.vuln?.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data?.vuln?.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data?.vuln?.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      summary: summary,
      description: description,
      priority: {
        name: priority
      },
      issuetype: {
        name: 'Bug'
      },
      labels: labels
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode === 204) {
  return {
    decision: 'finish'
  };
}
else {
  Logger.info(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'JIRA Issue update failed',
    },
  };
}
```

## JIRA Issue Retest -> Update Vuln to Ready for Retest

The purpose of this example is when a JIRA Issue is assigned the 'Retest' status - the matching vulnerability in AttackForge is also assigned as retest.

<figure><img src="/files/WkwFpHQn7PjJMuRvcerL" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **JIRA WebHooks**
  * Configure 'Issue Updated' [web hook](https://developer.atlassian.com/server/jira/platform/webhooks/) from *https\://\<your-jira-tenant>.atlassian.net/plugins/servlet/webhooks#*
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * jira\_webhook\_secret - your [JIRA WebHook secret](/app/modules/projects)
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)
  * x\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).

**Action 1 - Get Vulnerability**&#x20;

* **Method**: GET
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerabilities>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
// Validate HMAC
const JIRAWebHookSecret = secrets.jira_webhook_secret;
const JIRAWebHookSignature = String.replace(data.headers['X-Hub-Signature'], "sha256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, JIRAWebHookSecret, "SHA256", "base16"));

if (JIRAWebHookSignature !== payloadHMAC) {
  Logger.info('JIRAWebHookSignature: ' + JIRAWebHookSignature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (!data.jsonBody?.issue?.key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"jira_issue_key"},value:{$eq:"'+ data.jsonBody.issue.key + '"}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching vulnerability',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerabilities?' + query
  },
  data: {
    issue: data.jsonBody.issue
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 
  && response.jsonBody?.count === 1 
  && response.jsonBody.vulnerabilities?[0]
) {
  const vuln = response.jsonBody.vulnerabilities[0];
  
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      vuln: vuln,
      issue: data.issue
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - Update Vulnerability**&#x20;

* **Method**: PUT
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerability/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (data.issue?.fields?.status?.name !== 'Retest') {
  return {
    decision: {
      status: 'finish',
      message: 'JIRA issue status not set to "Retest"',
    }
  };
}

if (data.vuln?.vulnerability_status !== 'Open' && data.vuln?.vulnerability_retest !== 'No') {
  return {
    decision: {
      status: 'finish',
      message: 'Vuln status not set to "Open"',
    }
  };
}

let AFVulnId;
if (data.vuln.vulnerability_id) {
  AFVulnId = data.vuln.vulnerability_id;
}

if (!AFVulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln id missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability status to "Retest"',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + AFVulnId,
    body: {
      status: 'Retest'
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.result === 'Vulnerability Updated') {
  return {
    decision: {
      status: 'finish',
      message: 'Updated vulnerability status'
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed updating vulnerability status'
    },
  };
}
```

## Close JIRA Issue

The purpose of this example is when a vulnerability is closed in AttackForge, the matching JIRA Issue is also closed.

<figure><img src="/files/E7vq0JgHmbWmqObmQLlj" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get JIRA Issue**

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/3/issue/{issueIdOrKey}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted',
      }
  };
}

if (data.vulnerability_status !== 'Closed') {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is not Closed',
      }
  };
}

let jiraIssueKey;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'jira_issue_key' && customField.value) {
      jiraIssueKey = customField.value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'JIRA issue key missing',
      }
  };
}

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/3/issue/' + jiraIssueKey
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.key) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: { 
      status: 'abort',
      message: 'Vuln missing',
    },
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: response.jsonBody.key
  } 
};
```

**Action 2 - Get JIRA Transitions**&#x20;

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions'
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.transitions) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key,
    transitions: response.jsonBody.transitions
  } 
};
```

**Action 3 - Close JIRA Issue**

* **Method**: POST
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.transitions) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

let transitionId;
for (let x = 0; x < data.transitions.length; x++) {
  const transition = data.transitions[x];

  if (transition.name === 'Closed') {
    transitionId = transition.id;
  }
}

if (!transitionId) {
  return {
    decision: {
      status: 'abort',
      message: '"Closed" transition missing',
    }
  };
}

let url = 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions';

return {
  request: {
    url: url,
    body: {
      transition: {
        id: transitionId
      }
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 204) {
  return {
    decision: 'finish',
    message: 'Transitioned JIRA issue to "Closed"'
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA Issue transition failed'
    },
  };
}
```

## Re-Open JIRA Issue

The purpose of this example is when a vulnerability is re-opened in AttackForge, the matching JIRA Issue is also re-opened.

<figure><img src="/files/YNYqPuUnC5Kd0ojTh4WO" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get JIRA Issue**

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/3/issue/{issueIdOrKey}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted',
      }
  };
}

if (data.vulnerability_status === 'Closed' || data.vulnerability_retest === 'Yes') {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is Retest or Closed',
      }
  };
};

let jiraIssueKey;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'jira_issue_key' && customField.value) {
      jiraIssueKey = customField.value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'JIRA issue key missing',
      }
  };
}

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/3/issue/' + jiraIssueKey
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.key) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: { 
      status: 'abort',
      message: 'Vuln missing',
    },
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: response.jsonBody.key
  } 
};
```

**Action 2 - Get JIRA Transitions**&#x20;

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing'
    }
  };
}

if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing'
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions',
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.transitions) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key,
    transitions: response.jsonBody.transitions
  } 
};
```

**Action 3 - Re-Open JIRA Issue**

* **Method**: POST
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.transitions) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

let transitionId;
for (let x = 0; x < data.transitions.length; x++) {
  const transition = data.transitions[x];

  if (transition.name === 'Open') {
    transitionId = transition.id;
  }
}

if (!transitionId) {
  return {
    decision: {
      status: 'abort',
      message: '"Open" transition missing',
    }
  };
}

let url = 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions';

return {
  request: {
    url: url,
    body: {
      transition: {
        id: transitionId
      }
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 204) {
  return {
    decision: 'finish',
    message: 'Transitioned JIRA Issue to "Open"',
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue transition failed'
    },
  };
}
```

## JIRA Issue Comment Created -> Create Vuln Remediation Note

The purpose of this example is when a comment is created on a JIRA Issue, a remediation note is created on the matching vulnerability.

<figure><img src="/files/FQtFfl0Ik0OdMCjAPqG3" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **JIRA WebHooks**
  * Configure 'Issue Comment Created' [web hook](https://developer.atlassian.com/server/jira/platform/webhooks/) from *https\://\<your-jira-tenant>.atlassian.net/plugins/servlet/webhooks#*
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * x\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).

**Action 1 - Get Vulnerability**

* **Method**: GET
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerabilities>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
// Validate HMAC
const JIRAWebHookSecret = secrets.jira_webhook_secret;
const JIRAWebHookSignature = String.replace(data.headers['X-Hub-Signature'], "sha256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, JIRAWebHookSecret, "SHA256", "base16"));

if (JIRAWebHookSignature !== payloadHMAC) {
  Logger.info('JIRAWebHookSignature: ' + JIRAWebHookSignature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (!data.jsonBody?.issue?.key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"jira_issue_key"},value:{$eq:"'+ data.jsonBody.issue.key + '"}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching AttackForge Vulnerability',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerabilities?' + query
  },
  data: {
    comment: data.jsonBody.comment,
    issue: data.jsonBody.issue
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 
  && response.jsonBody?.count === 1 
  && response.jsonBody.vulnerabilities?[0]
) {
  const vuln = response.jsonBody.vulnerabilities[0];
  
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      vuln: vuln,
      issue: data.issue,
      comment: data.comment
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - Create Remediation Note**&#x20;

* **Method**: POST
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerability/{id}/remediationNote>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (!data.vuln?.vulnerability_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Vulnerability id missing',
    }
  };
}

if (!data.comment?.body) {
  return {
    decision: {
      status: 'abort',
      message: 'Comment missing',
    }
  };
}

let afVulnProjectId;
if (data.vuln.vulnerability_projects) {
  for (let x = 0; x < data.vuln.vulnerability_projects.length; x++) {
    const project = data.vuln.vulnerability_projects[x];

    if (project.id) {
      afVulnProjectId = project.id;
      break;
    }
  }
}

if (!afVulnProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Project id missing',
    }
  };
}

const comment = data.comment.body;
let remediationNoteExists = false;

if (data.vuln.vulnerability_remediation_notes) {
  for (let x = 0; x < data.vuln.vulnerability_remediation_notes.length; x++) {
    const remediationNote = data.vuln.vulnerability_remediation_notes[x];

    if (remediationNote.note === comment) {
      remediationNoteExists = true;
    }
  }
}

if (remediationNoteExists) {
  return {
    decision: {
      status: 'finish',
      message: 'Remediation note already exists',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Create remediation note',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.vuln.vulnerability_id + '/remediationNote',
    body: {
      projectId: afVulnProjectId,
      note: comment
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.note?.id) {
  return {
    decision: {
      status: 'finish',
      message: 'Remediation note created'
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating remediation note'
    },
  };
}
```

## Create JIRA Issue Comment

The purpose of this example is when a remediation note is created on a vulnerability, a comment is also created on the matching JIRA Issue.

<figure><img src="/files/hCpRpX4GwJNbQ1iQ7ymy" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Remediation Note Created
* **Secrets**:
  * x\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get Vulnerability**

* **Method**: GET
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerability/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Get vulnerability',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.remediation_note_vulnerability.vulnerability_id
  },
  data: {
    note: data
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.vulnerability?.vulnerability_id) {
  const vuln = response.jsonBody.vulnerability;
  
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      note: data.note,
      vuln: vuln
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - Get JIRA Comments**&#x20;

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/comment>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data?.note?.remediation_note_details) {
  return {
    decision: {
      status: 'abort',
      message: 'Note missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

let jiraIssueKey;

if (data.vuln.vulnerability_custom_fields) {
  for (let i = 0; i < data.vuln.vulnerability_custom_fields.length; i++) {
    if (data.vuln.vulnerability_custom_fields[i].key === 'jira_issue_key') {
      jiraIssueKey = data.vuln.vulnerability_custom_fields[i].value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'JIRA issue key missing',
      }
  };
}

return {
  data: {
    vuln: data.vuln,
    note: data.note,
    jiraIssueKey: jiraIssueKey
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + jiraIssueKey + '/comment'
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.comments) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue comments missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    note: data.note,
    jiraIssueKey: data.jiraIssueKey,
    comments: response.jsonBody.comments
  } 
};
```

**Action 3 - Create JIRA Issue Comment**

* **Method**: POST
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/comment>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data?.note?.remediation_note_details) {
  return {
    decision: {
      status: 'abort',
      message: 'Note missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.jiraIssueKey) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

if (!data?.comments) {
  return {
    decision: {
      status: 'abort',
      message: 'Comments missing',
    }
  };
}

const remediationNote = data.note.remediation_note_details;
let existingComment = false;

for (let x = 0; x < data.comments.length; x++) {
  const comment = data.comments[x];

  if (comment.body === remediationNote) {
    existingComment = true;
  }
}

if (existingComment) {
  return {
    decision: {
      status: 'finish',
      message: 'Comment already exists',
    }
  };
}

return {
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jiraIssueKey + '/comment',
    body: {
      body: remediationNote
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 201 && response.jsonBody?.id) {
  return {
    decision: 'finish',
    message: 'Comment created'
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating comment'
    },
  };
}
```

## Upload Vulnerability Evidence to JIRA Issue

The purpose of this example is when a evidence file is uploaded to a vulnerability, the file is also uploaded to the matching JIRA Issue.

<figure><img src="/files/p6idnfH4UtYOzAEmlwBg" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Evidence Created
* **Secrets**:
  * af\_hostname - e.g. acme.attackforge.io
  * af\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * jira\_hostname - e.g. acme.atlassian.net
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get Vulnerability**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_user\_key
* **Request Script**:

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Get vulnerability details',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + data.evidence_vulnerability?.vulnerability_id
  },
  data: {
    evidence: data
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerability) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability details',
    },
    data: {
      vuln: response.jsonBody.vulnerability,
      evidence: data.evidence
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vulnerability',
    }
  };
}
```

**Action 2 - Check if JIRA Issue Id exists**

* **Script**:

```javascript
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vuln is missing',
    }
  };
}

const evidence = data.evidence;
const vuln = data.vuln;

let vulnId;
let jiraIssueId;

if (vuln.vulnerability_id) {
  vulnId = vuln.vulnerability_id;
}

if (vuln.vulnerability_custom_fields) {
  for (let x = 0; x < vuln.vulnerability_custom_fields.length; x++) {
    const customField = vuln.vulnerability_custom_fields[x];

    if (customField.key === 'jira_issue_id' && customField.value) {
      jiraIssueId = customField.value;
    }
  }
}

if (!vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'vulnId missing',
    }
  };
}
if (!jiraIssueId) {
  return {
    decision: {
      status: 'finish',
      message: 'no linked JIRA Issue found on this vulnerability',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Retrieved vulnerability details',
  },
  data: {
    vulnId: vulnId,
    evidence: evidence,
    jiraIssueId: jiraIssueId
  }
};
```

**Action 3 - Get JIRA Issue**

* **Method**: GET
* **URL**: https\://{{jira\_hostname}}/rest/api/3/issue/{issueIdOrKey}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Get JIRA Issue',
  },
  request: {
    url: 'https://' + secrets.jira_hostname + '/rest/api/3/issue/' + data.jiraIssueId
  },
  data: {
    vulnId: data.vulnId,
    evidence: data.evidence,
    jiraIssueId: data.jiraIssueId
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.id) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved JIRA Issue',
    },
    data: {
      vulnId: data.vulnId,
      evidence: data.evidence,
      jiraIssueId: data.jiraIssueId,
      jiraIssue: response.jsonBody
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vulnerability',
    }
  };
}
```

**Action 4 - Check if File is already uploaded to JIRA Issue**

* **Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}
if (!data.jiraIssue) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssue is missing',
    }
  };
}

let fileUploaded = false;

const fileName = data.evidence.evidence_file_name;
const fileSize = data.evidence.evidence_file_size;

if (data.jiraIssue.fields?.attachment && data.jiraIssue.fields.attachment.length > 0) {
  for (let x = 0; x < data.jiraIssue.fields.attachment.length; x++) {
    const attachment = data.jiraIssue.fields.attachment[x];

    if (getFilenameWithoutExtension(attachment.filename) === getFilenameWithoutExtension(fileName) 
      && attachment.size === fileSize
    ) {
      fileUploaded = true;
    }
  }
}

if (fileUploaded) {
  return {
    decision: {
      status: 'finish',
      message: 'File already uploaded to JIRA Issue',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'File has not been uploaded to JIRA Issue',
  },
  data: {
    vulnId: data.vulnId,
    evidence: data.evidence,
    jiraIssueId: data.jiraIssueId
  }
};

function getFilenameWithoutExtension(filename) {
  // Matches a dot followed by one or more characters that are not a dot or slash, at the end of the string
  return String.toLowerCase(String.replace(filename, m/\.[^/.]+$/i, ''));
}
```

**Action 5 - Download Evidence File**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability/{vulnId}/evidence/{fileStorageName}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_user\_key
* **Options:**
  * Download Respons&#x65;**:** Yes
* **Request Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}

const fileStorageName = data.evidence.evidence_file_storage_name;

return {
  decision: {
    status: 'continue',
    message: 'Download vulnerability evidence file',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + data.vulnId + '/evidence/' + fileStorageName
  },
  data: {
    jiraIssueId: data.jiraIssueId
  }
};
```

* **Response Script**:

```javascript
if (response.fileId) {
  return {
    decision: {
      status: 'continue',
      message: 'Downloaded vulnerability evidence file',
    },
    data: {
      fileId: response.fileId,
      jiraIssueId: data.jiraIssueId
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error downloading vulnerability evidence file',
    }
  };
}
```

**Action 6 - Upload File to JIRA**

* **Method**: POST
* **URL**: https\://{{jira\_hostname}}/rest/api/3/issue/{issueIdOrKey}/attachments
* **Headers**:
  * Key = X-Atlassian-Token; Type = Value; Value = no-check
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = multipart/form-data
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.fileId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.fileId is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Uploading evidence to JIRA Issue',
  },
  request: {
    url: 'https://' + secrets.jira_hostname + '/rest/api/3/issue/' + data.jiraIssueId + '/attachments',
    multipart: {
      fields: [
        {
          name: 'file',
          fileId: data.fileId
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody && response.jsonBody[0]?.id) {
  return {
    decision: {
      status: 'finish',
      message: 'File uploaded to JIRA Issue',
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error uploading file to JIRA Issue',
    }
  };
}
```

## Upload JIRA Attachment to Vulnerability Evidence

The purpose of this example is when an attachment is uploaded to a JIRA Issue, the file is also uploaded to the matching vulnerability in AttackForge.

<figure><img src="/files/Pfs7ruL5Ce0NSzq5Dddd" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **JIRA WebHooks**
  * Configure 'Attachment Created' [web hook](https://developer.atlassian.com/server/jira/platform/webhooks/) from *https\://\<your-jira-tenant>.atlassian.net/plugins/servlet/webhooks#*
  * Append *?issueKey={issue.key}* to the end of the trigger url to ensure that your flow can identify which JIRA Issue the attachment belongs to
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * af\_hostname - e.g. acme.attackforge.io
  * af\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * jira\_hostname - e.g. acme.atlassian.net
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)
  * jira\_webhook\_secret - your JIRA webhook secret

**Action 1 - Validate Message from JIRA**

* **Script:**

```javascript
// Validate HMAC
const JIRAWebHookSecret = secrets.jira_webhook_secret;
const JIRAWebHookSignature = String.replace(data.headers['X-Hub-Signature'], "sha256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, JIRAWebHookSecret, "SHA256", "base16"));

if (JIRAWebHookSignature !== payloadHMAC) {
  Logger.info('JIRAWebHookSignature: ' + JIRAWebHookSignature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (!data.jsonBody?.attachment?.id) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA attachment id missing',
    }
  };
}
if (!data.query?.issueKey) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'JIRA message valid',
  },
  data: {
    attachment: data.jsonBody.attachment,
    issueKey: data.query.issueKey
  }
};
```

**Action 2 - Get Vulnerability**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_user\_key
* **Request Script**:

```javascript
if (!data.attachment) {
  return {
    decision: {
      status: 'abort',
      message: 'data.attachment is missing',
    }
  };
}
if (!data.issueKey) {
  return {
    decision: {
      status: 'abort',
      message: 'data.issueKey is missing',
    }
  };
}

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"jira_issue_id"},value:{$eq:"'+ data.issueKey + '"}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching vulnerability',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerabilities?' + query
  },
  data: {
    attachment: data.attachment
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 
  && response.jsonBody?.count === 1 
  && response.jsonBody.vulnerabilities?[0]
  && response.jsonBody.vulnerabilities?[0].vulnerability_id
) {
  const vuln = response.jsonBody.vulnerabilities?[0];
  const vulnId = vuln.vulnerability_id;

  const vulnFiles = [];
  if (vuln.vulnerability_evidence && vuln.vulnerability_evidence.length > 0) {
    for (let x = 0; x < vuln.vulnerability_evidence.length; x++) {
      const evidence = vuln.vulnerability_evidence[x];

      if (evidence.file_name && evidence.file_size) {
        Array.push(vulnFiles, {
          name: evidence.file_name,
          size: evidence.file_size
        });
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      vulnFiles: vulnFiles,
      vulnId: vulnId,
      attachment: data.attachment
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error searching vulnerabilities',
    }
  };
}
```

**Action 3 - Check if Attachment is already uploaded to Vulnerability**

* **Script**:

```javascript
if (!data.vulnFiles) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnFiles is missing',
    }
  };
}
if (!data.attachment) {
  return {
    decision: {
      status: 'abort',
      message: 'data.attachment is missing',
    }
  };
}
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}

let fileUploaded = false;

const attachmentName = data.attachment.filename;
const attachmentSize = data.attachment.size;

if (data.vulnFiles.length > 0) {
  for (let x = 0; x < data.vulnFiles.length; x++) {
    const evidenceFile = data.vulnFiles[x];

    if (getFilenameWithoutExtension(evidenceFile.name) === getFilenameWithoutExtension(attachmentName) 
      && evidenceFile.size === attachmentSize
    ) {
      fileUploaded = true;
    }
  }
}

if (fileUploaded) {
  return {
    decision: {
      status: 'finish',
      message: 'File already uploaded to vulnerability',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'File has not been uploaded to vulnerability',
  },
  data: {
    vulnId: data.vulnId,
    attachmentId: data.attachment.id
  }
};

function getFilenameWithoutExtension(filename) {
  // Matches a dot followed by one or more characters that are not a dot or slash, at the end of the string
  return String.toLowerCase(String.replace(filename, m/\.[^/.]+$/i, ''));
}
```

**Action 4 - Get JIRA Attachment Download Link**

* **Method**: GET
* **URL**: https\://{{jira\_hostname}}/rest/api/3/attachment/content/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.attachmentId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.attachmentId is missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Download attachment',
  },
  request: {
    url: 'https://' + secrets.jira_hostname + '/rest/api/3/attachment/content/' + data.attachmentId
  },
  data: {
    vulnId: data.vulnId
  }
};
```

* **Response Script**:

```javascript
if (response.headers && response.headers['Location']) {
  return {
    decision: {
      status: 'continue',
      message: 'Download attachment',
    },
    data: {
      downloadLink: response.headers['Location'],
      vulnId: data.vulnId
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error getting download link',
    }
  };
}
```

**Action 5 - Download JIRA Attachment**

* **Method**: GET
* **Options**:
  * Download Response: Yes
* **Request Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.downloadLink) {
  return {
    decision: {
      status: 'abort',
      message: 'data.downloadLink is missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Download attachment',
  },
  request: {
    url: data.downloadLink
  },
  data: {
    vulnId: data.vulnId
  }
};
```

* **Response Script**:

```javascript
if (response.fileId) {
  return {
    decision: {
      status: 'continue',
      message: 'Downloaded attachment',
    },
    data: {
      fileId: response.fileId,
      vulnId: data.vulnId
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error downloading attachment',
    }
  };
}
```

**Action 6 - Upload File to Vulnerability**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability/{id}/evidence
* **Headers**:
  * Key = Content-Type; Type = Value; Value = multipart/form-data
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_user\_key
* **Request Script**:

```javascript
if (!data.fileId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.fileId is missing',
    }
  };
}
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Uploading attachment to vulnerability',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + data.vulnId + '/evidence',
    multipart: {
      fields: [
        {
          name: 'file',
          fileId: data.fileId
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.status === 'File Uploaded') {
  return {
    decision: {
      status: 'finish',
      message: 'File uploaded to vulnerability',
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error uploading file to vulnerability',
    }
  };
}
```


# ServiceNow

## Create ServiceNow Incident

{% embed url="<https://youtu.be/eV1qxzcJ2Do?si=Q8f1ZPwFGmclXdHh>" %}

The purpose of this example is to create a [ServiceNow Incident](https://www.servicenow.com/au/products/itsm/what-is-incident-management.html) when a Vulnerability is created in AttackForge, and to update AttackForge to assign the SNOW Incident Id against the Vulnerability.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)

**Action 1 - Create SNOW Incident**&#x20;

* **Method**: POST
* **URL**: https\://\<YOUR-SNOW>/api/now/table/incident
* **Headers**:
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined'
    }
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const body = {};

  if (data.vulnerability_title) {
    body.short_description = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  let priority = 3;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 3;
  }

  body.impact = priority;
  body.urgency = priority;
  body.priority = priority;
  body.severity = priority;

  let description = '';

  if (data.vulnerability_affected_asset_name) {
    description += 'Affected Asset: ' + data.vulnerability_affected_asset_name + '\n\n';
  }
  else if (data.vulnerability_affected_assets) {
    description += 'Affected Asset(s): ';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n* ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_description) {
    description += 'Description: \n' + data.vulnerability_description + '\n\n';
  }

  if (data.vulnerability_likelihood_of_exploitation) {
    description += 'Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '\n\n';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }
  }

  if (cvssScore && cvssVector) {
    description += 'CVSS Score: ' + cvssScore + '\nCVSS Vector: ' + cvssVector + '\n\n';
  }

  if (data.vulnerability_attack_scenario) {
    description += 'Attack Scenario (Technical Risk): \n' + data.vulnerability_attack_scenario + '\n\n';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += 'Remediation Recommendation: \n' + data.vulnerability_remediation_recommendation + '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += 'Steps to Reproduce: \n' + data.vulnerability_steps_to_reproduce + '\n\n';
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += 'Notes:';

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        description += '\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (data.vulnerability_tags && data.vulnerability_tags.length > 0) {
    description += 'Tags:';

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '\n* ' + data.vulnerability_tags[i];
    }
  }

  body.description = description;

  return body;
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.result?.sys_id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing SNOW Incident SysId (body.result.sys_id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      snow_incident_number: body.result.number
    } 
  };
}
```

**Action 2 - Update AF Vuln with SNOW Incident Id**

* **Method**: PUT
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-Key; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (data.snow_incident_number && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'snow_incident_number',
            value: data.snow_incident_number
          }
        ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}
```

## ServiceNow Incident Retest -> Update Vuln to Ready for Retest

The purpose of this example is when a ServiceNow Incident is assigned the 'Resolved' status - the matching vulnerability in AttackForge is assigned as retest.

<figure><img src="/files/37mEB72oNrCTWa0mcrVC" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **SNOW WebHooks**
  * Configure 'incident.updated' [web hook](https://medium.com/@sebasqui1995/creating-a-webhook-in-servicenow-a-step-by-step-guide-a8de37ca22f0) in your ServiceNow
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: Enabled
* **Secrets**:
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)
  * x\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).

**Action 1 - Get Vulnerability**&#x20;

* **Method**: GET
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerabilities>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (!data.jsonBody?.incidentId) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW incident id missing',
    }
  };
}

if (!data.jsonBody?.state) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW state missing',
    }
  };
}

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"snow_incident_number"},value:{$eq:"'+ data.jsonBody?.incidentId + '"}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching vulnerability',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerabilities?' + query
  },
  data: {
    incident: data.jsonBody
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 
  && response.jsonBody?.count === 1 
  && response.jsonBody.vulnerabilities?[0]
) {
  const vuln = response.jsonBody.vulnerabilities[0];
  
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      vuln: vuln,
      incident: data.incident
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - Get SNOW Incident State**&#x20;

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/sys\\_choice?sysparm\\_query=name=incident\\&element=state\\&value={state}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

const query = 'sysparm_query=name=incident&element=state&value='+ data.incident.state;

return {
  decision: {
    status: 'continue',
    message: 'Fetching incident state',
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/sys_choice?' + query
  },
  data: {
    vuln: data.vuln,
    incident: data.incident
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?[0]?.label) { 
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved incident state'
    },
    data: {
      vuln: data.vuln,
      incident: data.incident,
      state: response.jsonBody.result[0]
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving incident state'
    }
  };
}
```

**Action 3 - Update Vulnerability**&#x20;

* **Method**: PUT
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerability/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (data.state.label !== 'Resolved') {
  return {
    decision: {
      status: 'finish',
      message: 'SNOW incident status not set to "Resolved"',
    }
  };
}

if (data.vuln?.vulnerability_status !== 'Open' && data.vuln?.vulnerability_retest !== 'No') {
  return {
    decision: {
      status: 'finish',
      message: 'Vuln status not set to "Open"',
    }
  };
}

let AFVulnId;
if (data.vuln.vulnerability_id) {
  AFVulnId = data.vuln.vulnerability_id;
}

if (!AFVulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln id missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability status to "Retest"',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + AFVulnId,
    body: {
      status: 'Retest'
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.result === 'Vulnerability Updated') {
  return {
    decision: {
      status: 'finish',
      message: 'Updated vulnerability status'
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed updating vulnerability status'
    },
  };
}
```

## Close ServiceNow Incident

The purpose of this example is when a vulnerability is closed in AttackForge, the matching ServiceNow Incident is also closed.

<figure><img src="/files/dxYhM6Fflj07luYhE0Eh" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)

**Action 1 - Get SNOW Incident**

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/incident?sysparm\\_query=GOTOnumber={incidentId}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted',
      }
  };
}

if (data.vulnerability_status !== 'Closed') {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is not Closed',
      }
  };
}

let snowIncidentId;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'snow_incident_number' && customField.value) {
      snowIncidentId = customField.value;
      break;
    }
  }
}

if (!snowIncidentId) {
  return {
      decision: { 
        status: 'finish',
        message: 'SNOW incident id missing',
      }
  };
}

const query = 'sysparm_query=GOTOnumber=' + snowIncidentId;

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/incident?' + query
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?[0]?.sys_id) {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: { 
      status: 'abort',
      message: 'Vuln missing',
    },
  };
}

return {
  data: {
    vuln: data.vuln,
    incident: response.jsonBody.result[0]
  } 
};
```

**Action 2 - Get SNOW Incident States**&#x20;

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/sys\\_choice?sysparm\\_query=name=incident\\&element=state\\&value={state}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

const query = 'sysparm_query=name=incident&element=state';

return {
  decision: {
    status: 'continue',
    message: 'Fetching incident states',
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/sys_choice?' + query
  },
  data: {
    vuln: data.vuln,
    incident: data.incident
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result) { 
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved incident states'
    },
    data: {
      vuln: data.vuln,
      incident: data.incident,
      states: response.jsonBody.result
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving incident states'
    }
  };
}
```

**Action 3 - Close SNOW Incident**

* **Method**: PUT
* **URL**: <https://dev310111.service-now.com/api/now/v1/table/incident/{sys\\_id}?sysparm\\_exclude\\_ref\\_link=true>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data?.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.states) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW incident states missing',
    }
  };
}

let stateId;
for (let x = 0; x < data.states.length; x++) {
  const state = data.states[x];

  if (state.label === 'Closed') {
    stateId = state.value;
  }
}

if (!stateId) {
  return {
    decision: {
      status: 'abort',
      message: '"Closed" state missing',
    }
  };
}

const path = '/api/now/v1/table/incident/' + data.incident.sys_id + '?';
const query = 'sysparm_exclude_ref_link=true';

let url = 'https://dev310111.service-now.com' + path + query;

return {
  request: {
    url: url,
    body: {
      state: stateId
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.sys_id) {
  return {
    decision: 'finish',
    message: 'Updated SNOW incident to "Closed"',
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident status update failed'
    },
  };
}
```

## Re-Open ServiceNow Incident

The purpose of this example is when a vulnerability is re-opened in AttackForge, the matching ServiceNow Incident is also re-opened.

<figure><img src="/files/S2vOPotq00GKzoX7AVVk" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)

**Action 1 - Get SNOW Incident**

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/incident?sysparm\\_query=GOTOnumber={incidentId}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted',
      }
  };
}

if (data.vulnerability_status === 'Closed' || data.vulnerability_retest === 'Yes') {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is Retest or Closed',
      }
  };
};

let snowIncidentId;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'snow_incident_number' && customField.value) {
      snowIncidentId = customField.value;
      break;
    }
  }
}

if (!snowIncidentId) {
  return {
      decision: { 
        status: 'finish',
        message: 'SNOW incident id missing',
      }
  };
}

const query = 'sysparm_query=GOTOnumber=' + snowIncidentId;

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/incident?' + query
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?[0]?.sys_id) {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: { 
      status: 'abort',
      message: 'Vuln missing',
    },
  };
}

return {
  data: {
    vuln: data.vuln,
    incident: response.jsonBody.result[0]
  } 
};
```

**Action 2 - Get SNOW Incident States**&#x20;

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/sys\\_choice?sysparm\\_query=name=incident\\&element=state\\&value={state}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

const query = 'sysparm_query=name=incident&element=state';

return {
  decision: {
    status: 'continue',
    message: 'Fetching incident states',
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/sys_choice?' + query
  },
  data: {
    vuln: data.vuln,
    incident: data.incident
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result) { 
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved incident states'
    },
    data: {
      vuln: data.vuln,
      incident: data.incident,
      states: response.jsonBody.result
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving incident states'
    }
  };
}
```

**Action 3 - Re-Open SNOW Incident**

* **Method**: PUT
* **URL**: <https://dev310111.service-now.com/api/now/v1/table/incident/{sys\\_id}?sysparm\\_exclude\\_ref\\_link=true>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data?.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.states) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW incident states missing',
    }
  };
}

let stateId;
for (let x = 0; x < data.states.length; x++) {
  const state = data.states[x];

  if (state.label === 'New') {
    stateId = state.value;
  }
}

if (!stateId) {
  return {
    decision: {
      status: 'abort',
      message: '"New" state missing',
    }
  };
}

const path = '/api/now/v1/table/incident/' + data.incident.sys_id + '?';
const query = 'sysparm_exclude_ref_link=true';

let url = 'https://dev310111.service-now.com' + path + query;

return {
  request: {
    url: url,
    body: {
      state: stateId
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.sys_id) {
  return {
    decision: 'finish',
    message: 'Updated SNOW incident to "New"',
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident status update failed'
    },
  };
}
```

## Create VR Item In ServiceNow

The purpose of this example is when a vulnerability is created in AttackForge, a vulnerability is also created in ServiceNow Vulnerability Response (VR) module.

<figure><img src="/files/v86auWLDFpIpM8Dt1thZ" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Prerequisites:**

**Configure OAuth on ServiceNow**

1. Navigate to `Inbound Integrations`

<figure><img src="/files/pnUij3rad3oUyeTLw8TJ" alt=""><figcaption></figcaption></figure>

2. Click on `New Integration`. Select `Client Credentials Grant`.

<figure><img src="/files/naxnOcHLslw61fumHo2l" alt=""><figcaption></figcaption></figure>

3. Configure the credentials as required. Copy the `Client Id` and `Client Secret`. These will be referred to in the secrets within the flow.

<figure><img src="/files/ipkbT5rZKGWAODnOtgDK" alt=""><figcaption></figcaption></figure>

**Create Scripted REST API**

1. Navigate to `Scripted REST APIs`

<figure><img src="/files/o38rhTkFZJ2hEQGk0Rgk" alt="" width="375"><figcaption></figcaption></figure>

2. Click on `New`. Enter a name e.g. AttackForge. Select `vulnerability_integration_svc` in Default ACLs.

<figure><img src="/files/jpyyUY0emlwCM0CNtswn" alt=""><figcaption></figcaption></figure>

3. Click `Submit`. Click `New`.&#x20;

<figure><img src="/files/stY6wWuIOgDvGL5invZM" alt=""><figcaption></figcaption></figure>

4. Enter `Create Vulnerable Item` in Name. Select `POST` for HTTP method. Enter `/create_vulnerable_item` in Relative Path. Copy the `Resource Path` - this will be referenced later in the flow secrets. Enter the following code, the click `Update`.

<figure><img src="/files/iUtkOIdERW4ozp2BPsgr" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MNYx4RTD1Igv6X7OovvT" alt=""><figcaption></figcaption></figure>

```javascript
/*
 * Tables:
 * - sn_vul_third_party_entry: Stores vulnerability definitions from external sources
 * - sn_vul_vulnerable_item: Stores instances of vulnerabilities
 * - sn_vul_cwe: CWE (Common Weakness Enumeration) reference table
 */

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
 let request_body;
 try {
   request_body = JSON.parse(request.body.dataString);
 } catch (error) {
   response.setStatus(400);
   response.setBody({ error: "Invalid JSON payload" });
   return;
 }

 const vul_table = "sn_vul_third_party_entry";
 const vul_item_table = "sn_vul_vulnerable_item";
 const vul_source = "AttackForge";
 const vul_entry_id = "AF-" + request_body.report_id.toString();

 function mapCVSSAttackVector(letter) {
   const mappings = {
     N: "NETWORK",
     A: "ADJACENT",
     L: "LOCAL",
     P: "PHYSICAL",
   };
   return mappings[letter] || "";
 }

 function mapCVSSScore(letter) {
   const mappings = {
     N: "NONE",
     L: "LOW",
     H: "HIGH",
     R: "REQUIRED",
     U: "UNCHANGED",
     C: "CHANGED",
   };
   return mappings[letter] || "";
 }

 let vul_entry = new GlideRecord(vul_table);
 let cwe_entry = new GlideRecord("sn_vul_cwe");
 const cvss_components = [
   request_body.cvss_calculation_method,
   `AV:${request_body.cvss_attack_vector}`,
   `AC:${request_body.cvss_attack_complexity}`,
   `PR:${request_body.cvss_privileges_required}`,
   `UI:${request_body.cvss_user_interaction}`,
   `S:${request_body.cvss_scope}`,
   `C:${request_body.cvss_confidentiality}`,
   `I:${request_body.cvss_integrity}`,
   `A:${request_body.cvss_availability}`,
 ];

 const cvss_vector_string = cvss_components.join("/");
 if (!vul_entry.get("id", vul_entry_id)) {
   vul_entry.initialize();
   vul_entry.setValue("id", vul_entry_id);
   vul_entry.setValue("source_severity", parseInt(request_body.severity_number));
   vul_entry.setValue("source", vul_source);
   vul_entry.setValue("summary", request_body.details);
   vul_entry.setValue("name", request_body.title);
   if (request_body.cvss_calculation_method.includes("CVSS:3.1") || request_body.cvss_calculation_method.includes("CVSS:3.0")) {
     vul_entry.setValue("v3_attack_vector", mapCVSSAttackVector(request_body.cvss_attack_vector));
     vul_entry.setValue("v3_attack_complexity", mapCVSSScore(request_body.cvss_attack_complexity));
     vul_entry.setValue("v3_privileges_required", mapCVSSScore(request_body.cvss_privileges_required));
     vul_entry.setValue("v3_user_interaction", mapCVSSScore(request_body.cvss_user_interaction));
     vul_entry.setValue("v3_scope_change", mapCVSSScore(request_body.cvss_scope));
     vul_entry.setValue("v3_confidentiality_impact", mapCVSSScore(request_body.cvss_confidentiality));
     vul_entry.setValue("v3_integrity_impact", mapCVSSScore(request_body.cvss_integrity));
     vul_entry.setValue("v3_availability_impact", mapCVSSScore(request_body.cvss_availability));
     vul_entry.setValue("v3_base_score", request_body.cvss_score);
     vul_entry.setValue("v3_vector_string", cvss_vector_string);
   }
   if (request_body.cwe && cwe_entry.get("cwe_id", request_body.cwe)) {
     vul_entry.setValue("cwe_id", cwe_entry.sys_id);
   }
   vul_entry.insert();
 }

 let vul_item = new GlideRecord(vul_item_table);
 vul_item.initialize();

 if (!vul_item.get("external_id", request_body.report_id.toString())) {
   vul_item.source = vul_source;
   vul_item.setValue("vulnerability", vul_entry.sys_id);
   vul_item.external_id = request_body.report_id.toString();
   vul_item.insert();
 }
 
 response.setBody({
   table_name: vul_item_table,
   sys_id: vul_item.sys_id || false,
   external_id: vul_item.number || false,
   link: vul_item.sys_id ? gs.getProperty("glide.servlet.uri") + vul_item.getLink() : false,
 });
})(request, response);
```

**Configure Severity Map**

1. Navigate to `Normalized Severity Maps`.

<figure><img src="/files/YvbuJ5fqussou7kmErhK" alt=""><figcaption></figcaption></figure>

2. Click `New`. Enter the following severity maps. Ensure that the `Source`, `Source Value` and `Target Value` below matches exactly.

<figure><img src="/files/NgGmvPUmmtrQB05Oec6a" alt=""><figcaption></figcaption></figure>

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_tenant - your AttackForge hostname e.g. demo.attackforge.com
  * af\_token - your AttackForge user API key
  * snow\_client\_id - your ServiceNow Client Id (see Prerequisites above)
  * snow\_client\_secret - your ServiceNow Client Id (see Prerequisites above)
  * snow\_hostname - your ServiceNow hostname e.g. company.service-now\.com
  * snow\_resource\_path - your ServiceNow Scripted REST API route (see Prerequisites above)

**Action 1 - Get OAuth Token**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}/oauth\_token.do
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
const body = "grant_type=client_credentials&client_id=" 
  + secrets.snow_client_id 
  + "&client_secret=" 
  + secrets.snow_client_secret;

return {
  decision: {
    status: 'continue',
    message: 'Fetching SNOW OAuth token',
  },
  request: {
    url: 'https://' + secrets.snow_hostname + '/oauth_token.do',
    body: body
  },
  data: {
    vuln: data
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.access_token) {
  return {
    decision: { 
      status: 'continue',
      message: 'Found OAuth token',
    },
    data: {
      token: response.jsonBody.access_token,
      vuln: data.vuln
    } 
  };
}
else {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'Missing OAuth token',
    }
  };
}
```

**Action 2 - Format SNOW Vuln Body**

* **Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Processed AttackForge Vulnerability information for ServiceNow Vulnerability Response.',
  },
  data: {
    token: data.token,
    vuln: prepareBody(data.vuln)
  }
};

function prepareBody(vuln){
  let body = {};

  if (vuln?.vulnerability_created){
    body.submission_date_y_m_d = String.split(vuln.vulnerability_created, 'T')[0];
  }
  if (vuln?.vulnerability_id){
    body.report_id = vuln.vulnerability_id;
  }

  // SNOW map
  const priorityMap = {
    'Critical': 1, // Critical
    'High': 2, // Major
    'Medium': 3, // Minor
    'Low': 4, // Warning
    'Info': 0 // Clear(OK)
  };

  // Severity may get modified by SNOW automatically
  if (vuln?.vulnerability_priority){
    body.severity_number = priorityMap[vuln.vulnerability_priority];
  }
  if (vuln?.vulnerability_title){
    body.title = vuln.vulnerability_title;
  }
  if (vuln?.vulnerability_description){
    body.details = 'Description:\n' + vuln.vulnerability_description;
  }

  if (vuln?.vulnerability_affected_assets){
    const assets = affectedAssetsParagraph(vuln.vulnerability_affected_assets);
    if (assets){
      body.details = body.details + '\n\n' + assets;
    }
  }

  if (vuln?.vulnerability_steps_to_reproduce){
    body.details = body.details 
      + '\n\nSteps To Reproduce:\n' 
      + vuln.vulnerability_steps_to_reproduce 
      + '\n';
  }

  if (vuln?.vulnerability_custom_fields){
    const custom_description = getCustomFieldData(vuln.vulnerability_custom_fields);

    if (custom_description?.technical_impact){
      body.details = body.details + '\n\n' + custom_description.technical_impact;
    }
    if (custom_description?.critical_step){
      body.details = body.details + '\n\n' + custom_description.critical_step;
    }
    if (custom_description?.attack_narrative){
      body.details = body.details + '\n\n' + custom_description.attack_narrative;
    }
    if (custom_description?.cwe){
      body.details = body.details 
        + '\n\n' 
        + 'Common Weakness Enumeration ID: CWE-' 
        + custom_description.cwe + '\n';
      body.cwe = 'CWE-' + custom_description.cwe;
    } 
    else if (vuln?.vulnerability_tags) {
      const cwe = Array.find(vuln.vulnerability_tags, findCwe);
      if (cwe){
        body.details = body.details 
          + '\n\n' 
          + 'Common Weakness Enumeration ID: ' 
          + cwe 
          + '\n';
        body.cwe = cwe;
      }
    }
  }
  if (vuln?.vulnerability_remediation_recommendation){
    body.details = body.details 
      + '\n\nRemediation Recommendation:\n' 
      + vuln.vulnerability_remediation_recommendation;
  }

  let cvss_version;
  if (vuln?.vulnerability_cvssv3_vector){
    cvss_version = 'CVSS:3.1';
    const cvss_detail = getCvssDetail(vuln.vulnerability_cvssv3_vector);
    body.cvss_calculation_method = cvss_version;
    body.cvss_attack_vector = cvss_detail.cvss_attack_vector;
    body.cvss_attack_complexity = cvss_detail.cvss_attack_complexity;
    body.cvss_privileges_required = cvss_detail.cvss_privileges_required;
    body.cvss_user_interaction = cvss_detail.cvss_user_interaction;
    body.cvss_scope = cvss_detail.cvss_scope;
    body.cvss_confidentiality = cvss_detail.cvss_confidentiality;
    body.cvss_integrity = cvss_detail.cvss_integrity;
    body.cvss_availability = cvss_detail.cvss_availability;

    if (vuln?.vulnerability_cvssv3_base_score){
      body.cvss_score = vuln.vulnerability_cvssv3_base_score;
    }
  }

  Logger.debug('body ',JSON.stringify(body));
  return body;
}

function getCvssDetail(cvss){
  const fieldMap = {
    'AV': 'cvss_attack_vector',
    'AC': 'cvss_attack_complexity',
    'PR': 'cvss_privileges_required',
    'UI': 'cvss_user_interaction',
    'S': 'cvss_scope',
    'C': 'cvss_confidentiality',
    'I': 'cvss_integrity',
    'A': 'cvss_availability'
  };
  
  const result = {};
  const cvss_split = String.split(cvss, '/');

  for (let i = 0; i < Array.length(cvss_split); i++){
    const parts = String.split(cvss_split[i], ':');
    const key = parts[0];
    const value = parts[1];
    
    if (fieldMap[key]) {
      result[fieldMap[key]] = value;
    }
  }
  return result;
}

function findCwe(tag){
  if (String.includes(tag, 'CWE-')){
    return tag;
  }
}

function getCustomFieldData(custom_fields){
  const result = {};
  for (let i = 0; i < Array.length(custom_fields); i++){
    if (custom_fields[i].key === 'critical_steps' 
      && Array.isArray(custom_fields[i].value) 
      && Array.length(custom_fields[i].value) > 0
    ){
      let critical_step = 'Critical Steps:\n';
      for (let j = 0; j < Array.length(custom_fields[i].value); j++){
        critical_step = critical_step 
          + custom_fields[i].value[j].step 
          + ': ' 
          + custom_fields[i].value[j].details 
          + '\n';
      }
      result.critical_step = critical_step;
    }
    
    if (custom_fields[i].key === 'technical_impact') {
      if (custom_fields[i].value === '<p></p>'){
        result.technical_impact = 'Technical Impact: Not Provided';
      }
      else {
        result.technical_impact = 'Technical Impact:\n' + removeRichFormat(custom_fields[i].value);
      }
    }
    
    if (custom_fields[i].key === 'attack_narrative'){
      if (custom_fields[i].value === '<p></p>'){
        result.attack_narrative = 'Attack Narrative: Not Provided';
      }
      else {
        result.attack_narrative = 'Attack Narrative:\n' + removeRichFormat(custom_fields[i].value);
      }
    }
    if (custom_fields[i].key === 'CWE'){
      result.cwe = custom_fields[i].value;
    }
  }
  return result;
}

function affectedAssetsParagraph(affected_assets){
  let asset_string = '';
  for (let i = 0; i < Array.length(affected_assets); i++){
    if (affected_assets[i]?.asset){
      const asset = affected_assets[i].asset;
      
      if (asset?.name){
        asset_string = asset_string + 'Asset Name: ' + asset.name + '\n';
      }
      
      if (asset?.custom_fields && Array.isArray(asset.custom_fields)){
        for (let j = 0; j < Array.length(asset.custom_fields); j++){
          if (asset.custom_fields[j].key === 'urls'){
            asset_string = asset_string 
              + 'Urls: ' 
              + Array.join(asset.custom_fields[j].value, '\n') 
              + '\n';
          }
          if (asset.custom_fields[j].key === 'internet_facing'){
            asset_string = asset_string 
              + 'Internet Facing: ' 
              + asset.custom_fields[j].value 
              + '\n';
          }
        }
      }
    }
    
    if (affected_assets?[i].components){
      let asset_component_string = 'Components: ';
      const components = [];
      for (let j = 0; j < Array.length(affected_assets[i].components); j++){
        if (affected_assets[i].components?[j].name){
          Array.push(components, affected_assets[i].components[j].name);
        }
      }
      asset_component_string = asset_component_string + Array.join(components, ', ');
      asset_string = asset_string + asset_component_string + '\n';
    }
  }
  return asset_string;
}

function removeRichFormat(text){
  let result = text;
  result = String.replaceAll(result, m/<span[^>]*>/gi, '');
  result = String.replaceAll(result, m/<p[^>]*>/gi, '');
  result = String.replaceAll(result, '</span>', '');
  result = String.replaceAll(result, '</p>', '');
  return result;
}
```

**Action 3 - Create SNOW Vulnerability**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}{{snow\_resource\_path}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Creating vulnerability in ServiceNow.',
  },
  request: {
    url: 'https://' + secrets.snow_tenant + secrets.snow_resource_path,
    headers: {
      Authorization: "Bearer " + data.token
    },
    body: data.vuln
  },
  data: {
    token: data.token,
    vuln: data.vuln
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to create Vulnerable item in ServiceNow has failed. Please check the logs.'
    }
  };
}

const custom = {};
if (response.jsonBody?.result?.sys_id){
  custom.sys_id = response.jsonBody.result.sys_id;
}
if (response.jsonBody?.result?.external_id){
  custom.external_id = response.jsonBody.result.external_id;
}
if (response.jsonBody?.result?.link){
  custom.link = response.jsonBody.result.link;
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully created Vulnerable Item on ServiceNow, proceeding to next step.',
  },
  data: {
    token: data?.token,
    vuln: data?.vuln,
    custom: custom
  }
};
```

**Action 4 - Insert SNOW Vuln Info on AF Vuln**

* **Method**: PUT
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_token
* **Request Script**:

```javascript
if (!data.vuln?.report_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'AttackForge Vulnerability ID not found. Please check the logs.',
    }
  };
}
if (!data.custom) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.custom'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating AF Vulnerability with custom SNOW fields.',
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vuln.report_id,
    body: {
      custom_fields: [
        {
          key: "snow_sys_id",
          value: data.custom.sys_id
        },
        {
          key: "snow_external_id",
          value: data.custom.external_id
        },
        {
          key: "snow_link",
          value: 'https://' + secrets.snow_tenant + '/sn_vul_vulnerable_item.do?sys_id=' + data.custom.sys_id
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?.result || response.jsonBody?.result?.result !== "Vulnerability Updated"){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to update Vulnerability failed. Please check the logs.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated Vulnerability with SNOW custom fields.'
  }
};
```

## Update Vuln Status When ServiceNow VR Item Status Changes

The purpose of this example is when a Vulnerability Item changes status in the ServiceNow Vulnerability Response (VR) module, the matching vulnerability in AttackForge also updates its status.&#x20;

<figure><img src="/files/YvCp8NxJ4070wxOhFg73" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: User API Key
    * **Header Key**: x-user-key
* **Secrets**:
  * af\_tenant - your AttackForge hostname e.g. demo.attackforge.com
  * af\_token - your AttackForge user API key

**Action 1 - Update Vulnerability**

* **Method**: PUT
* **URL**: https\://{{snow\_hostname}}/oauth\_token.do
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY: Type = Secret; Value = af\_token
* **Request Script**:

```javascript
if (!data?.jsonBody){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify('data: ', data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No payload information sent from ServiceNow found. Please check the debug log.'
    }
  };
}

const vuln_item = data.jsonBody;
let af_vuln_id;
let snow_vuln_state;

if (vuln_item?.third_party_id){
  af_vuln_id = String.split(vuln_item.third_party_id, 'AF-')[1];
}
if (vuln_item?.state){
  snow_vuln_state = vuln_item.state;
}

// Closed > Deferred > Resolved > In Review 
// > Awaiting Implementation > Under Investigation > Open

const snowStateMap = {
  'Closed': 'Closed',
  'Resolved': 'Closed',
  'In Review': 'Open',
  'Awaiting Implementation': 'Open',
  'Under Investigation': 'Retest',
  'Open': 'Open',
};

const af_state = snowStateMap[snow_vuln_state];

return {
  decision: {
    status: 'continue',
    message: 'Updating Vulnerability',
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + af_vuln_id,
    body: {
      status: af_state
    }
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?.result || response.jsonBody?.result?.result !== "Vulnerability Updated"){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to update vulnerability status has failed. Please check the logs.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated Vulnerability status.',
  }
};
```

**Postrequisites:**

**Create Rest Message**

1. Navigate to REST Messages.

<figure><img src="/files/oeWCNYOA9YvBAIZmmT7g" alt="" width="375"><figcaption></figcaption></figure>

2. Click `New`. Enter Name `AttackForge Vuln Update Webhook`. The Endpoint should reference your [AttackForge Flow Trigger URL](#http-trigger-url) (see flow created above). Click `Submit`.

<figure><img src="/files/s257tPvLkEpO6xo9hPlQ" alt=""><figcaption></figcaption></figure>

3. Click `New`.

<figure><img src="/files/zYPNW7bWwY5RldbUNHMd" alt=""><figcaption></figcaption></figure>

4. Enter `POST` for Name and select `POST` for HTTP method. The Endpoint should reference your [AttackForge Flow Trigger URL](#http-trigger-url) (see flow created above).

<figure><img src="/files/vi4Z9q4IjEV0r2iUrJa9" alt=""><figcaption></figcaption></figure>

5. Click `HTTP Request` tab. Enter `Content-Type` and `X-USER-KEY` headers. The value for the `X-USER-KEY` should be your AttackForge user API key which has access to trigger the flow you created (see above). Click `Update`.

<figure><img src="/files/78hYyeuLjvBMMnCLgmRo" alt=""><figcaption></figcaption></figure>

**Configure Business Rule**

1. Navigate to `Business Rules`.

<figure><img src="/files/CeHZp7M9iQ04sYnwTo0q" alt="" width="375"><figcaption></figcaption></figure>

2. Click New. Enter `Vuln Updated` for the Name. Select `Vulnerable Item [sn_vul_vulnerable_item]` for the Table. Tick `Active` and `Advanced`. In the `When to run` tab, select `after` for When, tick `Update`.

<figure><img src="/files/K9DTtsqpvU3mR0l1juKr" alt=""><figcaption></figcaption></figure>

3. Click on `Advanced` tab. Enter the following code, ensuring that the highlighted section in the image matches the name and HTTP method defined in ***Create Rest Message*** above. Click `Submit`.

<figure><img src="/files/p85u1NJxsX3K3qAp6j0b" alt=""><figcaption></figcaption></figure>

```javascript
(function executeRule(current, previous) {
    try {
		if (!current.state.changes()) {
			gs.info("State unchanged for VI: " + current.number + " - skipping");
			return;
		}
		gs.info('Vulnerability State Updated to: ', current.state.getDisplayValue());

        let r = new sn_ws.RESTMessageV2('AttackForge Vuln Update Webhook', 'POST'); 
        let payload = {
            vulnerability_id: current.sys_id.toString(),
            number: current.number.toString(),
            state: current.state.getDisplayValue(),
			third_party_id: current.vulnerability.getDisplayValue(),
            short_description: current.short_description.toString()
        };
        
        r.setRequestBody(JSON.stringify(payload));

        let response = r.execute();
        let httpStatus = response.getStatusCode();
        
        gs.info('AttackForge webhook sent for updaed Vulnerable Item: ' + current.number + 
                ', Status: ' + httpStatus);
        
    } catch (error) {
        gs.error('AttackForge webhook error: ' + error.message);
    }
})(current, previous);

/*
 * {
 *   vulnerability_id: Vulnerable Item sys_id (string)
 *   number: Vulnerable Item number (e.g., "VIT0010025")
 *   state: Display value of state field (e.g., "Open", "Closed", "Resolved")
 *   third_party_id: Reference to vulnerability entry (e.g., "AF-12345")
 *   short_description: Short description of the vulnerable item
 * }
 *
 * - current: GlideRecord object representing the updated record
 * - previous: GlideRecord object representing the record before update
 * - current.state.changes(): Built-in method to detect if state field was modified
 * - sn_ws.RESTMessageV2: ServiceNow REST client for outbound HTTP requests
 * - getDisplayValue(): Returns human-readable value instead of internal value
 */
```

## Update ServiceNow VR Item Status When Vuln Status Changes

The purpose of this example is when a vulnerability status is updated in AttackForge, the status is also updated for the linked Vulnerability Item in ServiceNow Vulnerability Response (VR) module.

<figure><img src="/files/H7ULEvis4UPPSeJxkdgf" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Prerequisites:**

**Configure OAuth on ServiceNow**

1. Navigate to `Inbound Integrations`

<figure><img src="/files/pnUij3rad3oUyeTLw8TJ" alt=""><figcaption></figcaption></figure>

2. Click on `New Integration`. Select `Client Credentials Grant`.

<figure><img src="/files/naxnOcHLslw61fumHo2l" alt=""><figcaption></figcaption></figure>

3. Configure the credentials as required. Copy the `Client Id` and `Client Secret`. These will be referred to in the secrets within the flow.

<figure><img src="/files/ipkbT5rZKGWAODnOtgDK" alt=""><figcaption></figcaption></figure>

**Create Scripted REST API**

1. Navigate to `Scripted REST APIs`

<figure><img src="/files/o38rhTkFZJ2hEQGk0Rgk" alt="" width="375"><figcaption></figcaption></figure>

2. Click on `New`. Enter a name e.g. AttackForge. Select `vulnerability_integration_svc` in Default ACLs.

<figure><img src="/files/jpyyUY0emlwCM0CNtswn" alt=""><figcaption></figcaption></figure>

3. Click `Submit`. Click `New`.&#x20;

<figure><img src="/files/stY6wWuIOgDvGL5invZM" alt=""><figcaption></figcaption></figure>

4. Enter `Get Vulnerable Item` in Name. Select `GET` for HTTP method. Enter `/vulnerable_item/{vulnId}` for the Relative Path. Copy the `Resource Path` - this will be referenced later in the flow secrets. Enter the following code, the click `Update`.

<figure><img src="/files/sUcg0SSiiCevAdzDg7Yq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MNYx4RTD1Igv6X7OovvT" alt=""><figcaption></figcaption></figure>

```javascript
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    let vulnId = request.pathParams.vulnId;
    
    if (!vulnId) {
        response.setStatus(400);
        response.setBody({error: 'Missing vulnId parameter'});
        return;
    }

    let vulnEntry = new GlideRecord('sn_vul_entry');
    vulnEntry.addQuery('id', vulnId);
    vulnEntry.query();
    
    if (!vulnEntry.next()) {
        response.setStatus(404);
        response.setBody({error: 'Vulnerability entry not found', vulnId: vulnId});
        return;
    }
    
    let entrySysId = vulnEntry.getUniqueValue();

    let vulnItem = new GlideRecord('sn_vul_vulnerable_item');
    vulnItem.addQuery('vulnerability', entrySysId);
    vulnItem.query();
    
    if (!vulnItem.next()) {
        response.setStatus(404);
        response.setBody({error: 'No vulnerable item found', vulnId: vulnId});
        return;
    }

    response.setStatus(200);
    response.setBody({
        sys_id: vulnItem.getUniqueValue(),
        number: vulnItem.getValue('number'),
        vulnerability_id: vulnId,
        state: vulnItem.getValue('state'),
        state_label: vulnItem.getDisplayValue('state'),
        configuration_item: vulnItem.getDisplayValue('cmdb_ci'),
        updated_at: vulnItem.getValue('sys_updated_on')
    });
    
})(request, response);
```

3. Click `New`.&#x20;

<figure><img src="/files/stY6wWuIOgDvGL5invZM" alt=""><figcaption></figcaption></figure>

4. Enter `Update Vulnerable Item` in Name. Select `POST` for HTTP method. Enter `/update_vulnerable_item` for the Relative Path. Copy the `Resource Path` - this will be referenced later in the flow secrets. Enter the following code, the click `Update`.

<figure><img src="/files/tjLVyOGF7MewZjQUvHaZ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MNYx4RTD1Igv6X7OovvT" alt=""><figcaption></figcaption></figure>

```javascript
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    try {
        let requestBody = request.body.data;
        let vulnId = requestBody.vuln_id;
        let newStatus = requestBody.update_status;

        let vulnItem = new GlideRecord('sn_vul_vulnerable_item');
		vulnItem.addQuery('external_id', vulnId);
        vulnItem.query();
        
        if (vulnItem.next()) {
            let stateMapping = {
                'Open': '1',
                'Retest': '2',
                'Closed': '3'
            };
            
            vulnItem.setValue('state', stateMapping[newStatus] || 'open');
            vulnItem.update();
            
            response.setStatus(200);
            response.setBody({success: true, message: 'Updated', external_id: vulnId});
        } else {
            response.setStatus(404);
            response.setBody({success: false, message: 'Not found: ' + vulnId});
        }
        
    } catch (e) {
        response.setStatus(500);
        response.setBody({success: false, message: e.message});
    }
    
})(request, response);
```

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * snow\_client\_id - your ServiceNow Client Id (see Prerequisites above)
  * snow\_client\_secret - your ServiceNow Client Id (see Prerequisites above)
  * snow\_hostname - your ServiceNow hostname e.g. company.service-now\.com
  * snow\_get\_vulnitem\_api - your ServiceNow Scripted REST API route for *Get Vulnerable Item* (see Prerequisites above)
  * snow\_update\_vulnitem\_api - your ServiceNow Scripted REST API route for *Update Vulnerable Item* (see Prerequisites above)

**Action 1 - Get OAuth Token**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}/oauth\_token.do
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
const body = "grant_type=client_credentials&client_id=" 
  + secrets.snow_client_id 
  + "&client_secret=" 
  + secrets.snow_client_secret;

return {
  decision: {
    status: 'continue',
    message: 'Fetching SNOW OAuth token',
  },
  request: {
    url: 'https://' + secrets.snow_hostname + '/oauth_token.do',
    body: body
  },
  data: {
    vuln: data
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.access_token) {
  return {
    decision: { 
      status: 'continue',
      message: 'Found OAuth token',
    },
    data: {
      token: response.jsonBody.access_token,
      vuln: data.vuln
    } 
  };
}
else {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'Missing OAuth token',
    }
  };
}
```

**Action 2 - Get SNOW VR Item Status**

* **Method**: GET
* **URL**: https\://{{snow\_hostname}}{{snow\_get\_vulnitem\_api}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

let vulnId = data.vuln.vulnerability_id;

if (!vulnId){
  return {
    decision: {
      status: 'abort',
      message: 'Error: no vulnerability_id found from data.vuln.'
    }
  };
}

vulnId = 'AF-' + vulnId;

return {
  decision: {
    status: 'continue',
    message: 'Fetching ServiceNow Vulnerable Item table for ID: ' + vulnId
  },
  request: {
    url: 'https://' + secrets.snow_hostname + secrets.snow_get_vulnitem_api + vulnId,
    headers: {
      Authorization: "Bearer " + data.token
    }
  },
  data: {
    token: data.token,
    vuln: data.vuln
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed retrieving Vulnerable Item details.'
    }
  };
}

if (!response.jsonBody?.result) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: returned body does not contain vulnerable item result.'
    }
  };
}

const vuln = response.jsonBody.result;
const vulnState = vuln.state_label;
const snow_vuln_state = vulnState;

return {
  decision: {
    status: 'continue',
    message: 'Successfully found Vulnerable Item record, proceeding to next action.',
  },
  data: {
    token: data.token,
    vuln: data.vuln,
    snow_vuln_state: snow_vuln_state
  }
};
```

**Action 3 - Detect If Status Changed**

* **Script:**

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Missing data.vuln'
    }
  };
}
if (!data.snow_vuln_state){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Missing data.snow_vuln_state'
    }
  };
}

const snowStateMap = {
  'Closed': 'Closed',
  'Resolved': 'Closed',
  'In Review': 'Open',
  'Awaiting Implementation': 'Open',
  'Under Investigation': 'Retest',
  'Open': 'Open',
};

let current_af_status;

if (data.vuln.vulnerability_status){
  current_af_status = data.vuln.vulnerability_status;
  if (current_af_status === 'Open' && data.vuln.vulnerability_retest === 'Yes'){
    current_af_status = 'Retest';
  }
}

const expected_af_status = snowStateMap[data.snow_vuln_state];

if (!expected_af_status){
  return {
    decision:{
      status: 'abort',
      message: 'Error: Could not find expected AttackForge state for provided ServiceNow state: ' 
        + data.snow_vuln_state
    }
  };
}

if (expected_af_status === current_af_status) {
  return {
    decision: {
      status: 'finish',
      message: 'AttackForge Vulnerability status and ServiceNow Vulnerable Item state is in sync.'
    }
  };
}

let vuln_id;
if (data.vuln.vulnerability_id){
  vuln_id = data.vuln.vulnerability_id;
}

return {
  decision: {
    status: 'continue',
    message: 'New status found, proceeding to next step.',
  },
  data: {
    token: data.token,
    vuln_id: vuln_id,
    update_status: current_af_status
  }
};
```

**Action 4 - Update SNOW VR Item**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}{{snow\_update\_vulnitem\_api}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln_id',
    }
  };
}
if (!data.update_status) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.update_status',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Update vulnerability on Service Now.',
  },
  request: {
    url: 'https://' + secrets.snow_hostname + secrets.snow_update_vulnitem_api,
    headers: {
      Authorization: "Bearer " + data.token
    },
    body: {
      vuln_id: data.vuln_id,
      update_status: data.update_status
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to update ServiceNow Vulnerable Item state has failed. Please check the logs.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated Vulnerability status.',
  }
};
```


# Azure DevOps (ADO)

## Create Azure DevOps Work Item

{% embed url="<https://youtu.be/qnlhLAT41f4?si=ZsmAenXI8VwElo8u>" %}

The purpose of this example is to create a [Azure DevOps Work Item](https://learn.microsoft.com/en-us/azure/devops/boards/work-items/about-work-items?view=azure-devops\&tabs=agile-process) when a Vulnerability is created in AttackForge, and to update AttackForge to assign the Azure DevOps Work Item Id against the Vulnerability.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).
  * ado\_auth - your [ADO Personal Access Token](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\&tabs=Windows)

**Action 1 - Create ADO Work Item**&#x20;

* **Method**: POST
* **URL**: <https://dev.azure.com/\\><YOUR-ADO-TENANT>/\<YOUR-ADO-PROJECT>/\_apis/wit/workitems/$Issue?api-version=6.1-preview\.3
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json-patch+json
  * Key = Authorization; Type = Secret; Value = ado\_auth
* **Request Script**:

```javascript
let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined',
    },
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const fields = [];

  let title = '';

  if (data.vulnerability_title) {
    title = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  Array.push(fields, {
    from: null,
    op: 'add',
    path: '/fields/System.Title',
    value: title
  });

  let priority = 4;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 4;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 4;
  }

  Array.push(fields, {
    from: null,
    op: 'add',
    path: '/fields/Microsoft.VSTS.Common.Priority',
    value: priority
  });

  if (data.vulnerability_priority) {
    Array.push(fields, {
      from: null,
      op: 'add',
      path: '/fields/System.Tags',
      value: data.vulnerability_priority + ', Security Vulnerability'
    });
  }

  let description = '';

  if (data.vulnerability_title) {
    description += '<h1>[SECURITY][VULNERABILITY] ' + data.vulnerability_title + '</h1><br/>';
  }

  if (data.vulnerability_description) {
    const sanitizedDescription = String.replace(data.vulnerability_description, m/\r\n|\n|\r/gi, '<br>');
    description += '<h2>Description</h2><p>' + sanitizedDescription + '</p>';
  }

  if (data.vulnerability_affected_asset_name) {
    description += '<h2>Affected Asset</h2><ul><li>' + data.vulnerability_affected_asset_name + '</li></ul>';
  }
  else if (data.vulnerability_affected_assets) {
      description += '<h2>Affected Asset(s)</h2><ul>';

      for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
        if (data.vulnerability_affected_assets[i].asset?.name) {
          description += '<li>' + data.vulnerability_affected_assets[i].asset.name + '</li>';
        }
      }

      description += '</ul>';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_priority && data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }

    if (cvssScore && cvssVector) {
      description +=  
        "<h2>Technical Severity</h2>" +
        "<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
          "<thead>" +
            "<td><b>Rating</b></td>" +
            "<td><b>CVSSv3.1 Score</b></td>" +
          "</thead>" +
          "<tbody>" +
            "<td>" + data.vulnerability_priority + "</td>" +
            "<td>" + cvssScore + "</td>" +
          "</tbody>" +
        "</table>" +
        "<p>CVSS 3.1 Vector String: <a href='https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=" + cvssVector + "'>" + cvssVector + "</a></p>";
    }
    else {
      description += 
        "<h2>Technical Severity</h2>" +
        "<table style='text-align: center; vertical-align: middle; font-size:15px;'>" +
          "<thead>" +
            "<td><b>Rating</b></td>" +
          "</thead>" +
          "<tbody>" +
            "<td>" + data.vulnerability_priority + "</td>" +
          "</tbody>" +
        "</table>";
    }
  }

  if (data.vulnerability_likelihood_of_exploitation && data.vulnerability_attack_scenario) {
    const sanitizedAttackScenario = String.replace(data.vulnerability_attack_scenario, m/\r\n|\n|\r/gi, '<br>');

    description += 
      "<h2>Attack Scenario (Technical Risk)</h2>" +
      "<p>Likelihood of Exploitation: " + data.vulnerability_likelihood_of_exploitation + "/10</p>" +
      "<p>" + sanitizedAttackScenario + "</p>";
  }

  if (data.vulnerability_remediation_recommendation) {
    const sanitizedRemediationRecommendation = String.replace(data.vulnerability_remediation_recommendation, m/\r\n|\n|\r/gi, '<br>');
    description += "<h2>Recommendations</h2><p>" + sanitizedRemediationRecommendation + "</p>";
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += "<h2>Notes</h2>";

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        const sanitizedNote = String.replace(data.vulnerability_notes[i].note, m/\r\n|\n|\r/gi, '<br>');
        description += '<p>' + sanitizedNote + '</p>';
      }
    }
  }

  if (data.vulnerability_steps_to_reproduce) {
    const sanitizedPOC = String.replace(data.vulnerability_steps_to_reproduce, m/\r\n|\n|\r/gi, '<br>');
    description += "<h2>Steps to Reproduce</h2><p>" + sanitizedPOC + "</p>";
  }

  if (data.vulnerability_tags) {
    description += "<h2>Tags</h2><ul>";

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '<li>' + data.vulnerability_tags[i] + '</li>';
    }

    description += '</ul>';
  }

  Array.push(fields, {
      from: null,
      op: 'add',
      path: "/fields/System.Description",
      value: description
  });

  return fields;
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8; api-version=6.1-preview.3') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8; api-version=6.1-preview.3'
    }
  };
}

if (!body?.id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing ADO Work Item Id (body.id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data ?? {}));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    }, 
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data ?? {}));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  Logger.debug('normal result');

  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      ado_work_item_id: body.id
    } 
  };
}
```

**Action 2 - Update AF Vuln with ADO Work Item Id**

* **Method**: PUT
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-Key; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (data.ado_work_item_id && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'ado_work_item_id',
            value: JSON.stringify(data.ado_work_item_id)
          }
        ]
      }
    }
  };
}
else {
    return {
        decision: { 
            status: 'abort',
            message: 'missing required fields'
        }
    };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}
```


# Flashpoint (VulnDB)

## Prioritize Vulnerability with Threat Intelligence from VulnDB

{% embed url="<https://youtu.be/onvSZShtEB4?si=yNulV32SL5qczdR>\_" %}

The purpose of this example is to prioritize a vulnerability based on threat intelligence information harnessed from [FlashPoint VulnDB](https://flashpoint.io/ignite/vulnerability-intelligence/) and to apply a custom score/rating to the vulnerability.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).
  * vulndb\_client\_id - your VulnDB Client Id
  * vulndb\_client\_secret - your VulnDB Client Secret

**Action 1 - Get VulnDB OAuth Token**&#x20;

* **Method**: POST
* **URL**: <https://vulndb.flashpoint.io/oauth/token>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let cve;

if (data.vulnerability_custom_fields) {
    for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
      if (data.vulnerability_custom_fields[i].key === 'cve') {
        cve = data.vulnerability_custom_fields[i].value;
        break;
      }
    }
}

if (!cve) {
  return {
    decision: { 
      status: 'finish',
      message: 'No CVE found'
    }
  };
}

return {
  data: {
    cve: cve,
    vuln: data
  },
  request: {
    body: {
      client_id: secrets.vulndb_client_id,
      client_secret: secrets.vulndb_client_secret,
      grant_type: 'client_credentials'
    }
  }
};
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (body?.access_token) {
  return {
    data: {
      vulnDBToken: body.access_token,
      cve: data?.cve,
      vuln: data?.vuln
    } 
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      cause: 'VulnDB access token not found'
    }
  };
}
```

**Action 2 - Get Threat Intel for Vuln from VulnDB**

* **Method**: \<defined in Request Script>
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * \<others defined in Request Script>
* **Request Script**:

```javascript
if (data?.vulnDBToken && data?.cve && data?.vuln) {
  return {
    data: {
      cve: data.cve,
      vuln: data.vuln
    },
    request: { 
      url: 'https://vulndb.flashpoint.io/api/v2/vulnerabilities/' + data.cve + '/find_by_cve_id',
      headers: {
        Authorization: 'Bearer ' + data.vulnDBToken
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'VulnDB access token not found'
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (!body?.results?[0]) {
  return {
    decision: { 
      status: 'abort',
      message: 'VulnDB CVE not found'
    }
  };
}

return {
  data: {
    cve: data?.cve,
    vulnDB: body.results[0],
    vuln: data?.vuln
  } 
};
```

**Action 3 - Apply Threat Intel, Prioritize Vulnerability and Update Vulnerability**

* **Method**: \<defined in Request Script>
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (data?.vuln && data?.vulnDB) {
  const score = calculateScore(data.vuln, data.vulnDB);
  const priority = convertScoreToPriority(score);

  return {
      request: {
        url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.vuln.vulnerability_id,
        body: {
          priority: priority,
          custom_fields: [
            {
              key: 'threat_score',
              value: JSON.stringify(score)
            }
          ]
        }
      }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Missing required vuln data'
    }
  };
}

function calculateScore(vuln, vdb) {
  let score = 0;

  // Maximum score = 140
  // Higher the score = Higher Priority to fix!

  // [ THREAT CONTEXT ]

  const internetFacing = isInternetFacing(vuln);

  // Is the threat related to exposed/internet-facing services?
  if (internetFacing) {
    score += 10;
  }

  if (vdb.cvss_version_three_metrics) {
    for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
      const metrics = vdb.cvss_version_three_metrics[i];

      // Is the CVSS score 8 or higher?
      if (metrics.score !== undefined && metrics.score >= 8) {
        score += 5;
      }

      // Is the threat easily exploitable?
      if (metrics.attack_complexity === 'HIGH') {
        score += 1;
      }
      else if (metrics.attack_complexity === 'MEDIUM') {
        score += 5;
      }
      else if (metrics.attack_complexity === 'LOW') {
        score += 10;
      }

      // No user interaction required?
      if (metrics.user_interaction === 'NONE') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
    }
  }
  else if (vdb.cvss_metrics) {
    for (let i = 0; i < vdb.cvss_metrics.length; i++) {
      const metrics = vdb.cvss_metrics[i];

      // Is the CVSS score 8 or higher?
      if (metrics.score !== undefined && metrics.score >= 8) {
        score += 5;
      }

      // Is the threat easily exploitable?
      if (metrics.attack_complexity === 'HIGH') {
        score += 1;
      }
      else if (metrics.attack_complexity === 'MEDIUM') {
        score +=  5;
      }
      else if (metrics.attack_complexity === 'LOW') {
        score += 10;
      }
    }
  }

  if (vdb.classifications) {
    for (let i = 0; i < vdb.classifications.length; i++) {
      const classifications = vdb.classifications[i];

      // Is there a public exploit available for this threat?
      if (classifications.name === 'exploit_public') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // Does this threat require configuration changes?
      else if (classifications.name === 'solution_workaround') {
        score += 5;
      }
      // Does the threat grant unauthorized access?
      else if (classifications.name === 'location_remote') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // Disclosure in the wild?
      else if (classifications.name === 'disclosure_in_wild') {
        score += 5;
      }
      else if (classifications.name === 'disclosure_uncoordinated_disclosure') {
        score += 5;
      }
      // Wormable?
      else if (classifications.name === 'exploit_wormified') {
        if (isInternetFacing) {
          score += 15;
        }
        else {
          score += 10;
        }
      }
      // Virus / Malware?
      else if (classifications.name === 'exploit_virus_malware') {
        if (isInternetFacing) {
          score += 10;
        }
        else {
          score += 5;
        }
      }
      // PoC Public?
      else if (classifications.name === 'exploit_poc_public') {
        if (isInternetFacing) {
          score += 15;
        }
        else {
          score += 10;
        }
      }
    }
  }

  // Is a patch available for this threat?
  let patchExists = false;

  if (vdb.classifications) {
    for (let i = 0; i < vdb.classifications.length; i++) {
      const classifications = vdb.classifications[i];

      if (classifications.name === 'solution' || classifications.name === 'solution_upgrade') {
        patchExists = true;
        break;
      }
    }
  }
  else if (vdb.cvss_version_three_metrics) {
    for (let i = 0; i < vdb.cvss_version_three_metrics.length; i++) {
      const metrics = vdb.cvss_version_three_metrics[i];

      if (metrics.remediation_level === 'OFFICIAL_FIX' || metrics.remediation_level === 'TEMPORARY_FIX') {
        patchExists = true;
        break;
      }
    }
  }

  if (!patchExists) {
    score += 10;
  }

  // Social Risk Score (Vuln DB)
  if (vdb.social_risk_score === 'High') {
    score += 5;
  }
  else if (vdb.social_risk_score === 'Medium') {
    score += 3;
  }
  else if (vdb.social_risk_score === 'Low') {
    score += 1;
  }

  // What is the likelihood that this threat could be used in a ransomware attack?
  if (vdb.ransomware_likelihood === 'High') {
    if (isInternetFacing) {
      score += score + 10;
    }
    else {
      score += 5;
    }
  }
  else if (vdb.ransomware_likelihood === 'Medium') {
    if (isInternetFacing) {
      score += 5;
    }
    else {
      score += 3;
    }
  }
  else if (vdb.ransomware_likelihood === 'Low') {
    score += 1;
  }

  if (vdb.tags) {
    for (let i = 0; i < vdb.tags.length; i++) {
      // Is this a known CISA KEV vulnerability?
      if (vdb.tags[i] === 'cisa_kev') {
        score += 10;
        break;
      }
    }
  }

  return score;
}

function convertScoreToPriority(score) {
  if (score > 0 && score <= 30) {
    return 'Low';
  }
  else if (score > 30 && score <= 70) {
    return 'Medium';
  }
  else if (score > 70 && score <= 110) {
    return 'High';
  }
  else if (score > 110 && score <= 140) {
    return 'Critical';
  }
  else {
    return 'Info';
  }
}

function isInternetFacing(vuln) {
  if (vuln.vulnerability_affected_assets) {
    for (let i = 0; i < vuln.vulnerability_affected_assets.length; i++) {
      if (vuln.vulnerability_affected_assets[i].asset?.custom_fields) {
        const assetCustomFields = vuln.vulnerability_affected_assets[i].asset?.custom_fields;

        for (let j = 0; j < assetCustomFields.length; j++) {
          if (assetCustomFields[j].key === 'internet_facing' && assetCustomFields[j].value === 'Yes') {
            return true;
          }
        }
      }
    }
  }

  return false;
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'Vulnerability not updated'
    }
  };
}
```


# HackerOne

## Create Vuln from HackerOne Report

The purpose of this example is to create a new vulnerability immediately when a new report is submitted on HackerOne.

{% embed url="<https://youtu.be/lLhXPshWe1g>" %}

<figure><img src="/files/HjS5ebwmNhpirYarlWKg" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * h1\_webhook\_secret - your [HackerOne Webhook Secret](https://api.hackerone.com/webhooks/#webhooks)
  * hackerone\_api\_key - your [HackerOne API Key](https://api.hackerone.com/getting-started/#getting-started)
  * x\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).

**Action 1 - Get HackerOne Report**&#x20;

* **Method**: GET
* **URL**: <https://api.hackerone.com/v1/reports/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = hackerone\_api\_key
* **Request Script**:

```javascript
// Validate HMAC
const h1Secret = secrets.h1_webhook_secret;
const h1Signature = String.replace(data.headers['X-H1-Signature'], "SHA256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, h1Secret, "SHA256", "base16"));

if (h1Signature !== payloadHMAC) {
  Logger.info('h1Signature: ' + h1Signature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (data.jsonBody?.data?.report?.id) {
  return {
    decision: {
      status: 'continue',
      message: 'Fetching HackerOne Report',
    },
    request: {
      url: 'https://api.hackerone.com/v1/reports/' + data.jsonBody.data.report.id
    }
  };
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing "data.report.id"',
    }
  };
}

```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (response.statusCode === 200 && body?.data) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved HackerOne Report'
    },
    data: {
      report: body.data
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving HackerOne Report'
    },
  };
}
```

**Action 2 - Get CWE Details**&#x20;

* **Method**: GET
* **URL**: <https://cwe-api.mitre.org/api/v1/cwe/weakness/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let cweId = undefined;
if (data.report?.relationships?.weakness?.data?.attributes?.external_id) {
  const cweField = data.report.relationships.weakness.data.attributes.external_id;

  if (String.startsWith(cweField, 'cwe-')) {
    cweId = String.replace(cweField, "cwe-", "");
  }
}

if (cweId) {
  return {
    decision: {
      status: 'continue',
      message: 'Fetching CWE [ ' + cweId + ' ] details',
    },
    request: {
      url: 'https://cwe-api.mitre.org/api/v1/cwe/weakness/' + cweId
    },
    data: {
      report: data.report
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No CWE found. Skip fetching CWE details.',
    }
  };
}

```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json; charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=UTF-8'
    }
  };
}

if (response.statusCode === 200 && body?.Weaknesses?[0]) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved CWE details'
    },
    data: {
      report: data.report,
      cwe: body?.Weaknesses[0]
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving CWE details'
    },
  };
}
```

**Action 3 - Create Vulnerability**&#x20;

* **Method**: POST
* **URL**: <https://acme.attackforge.com/api/ss/vulnerability>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (data.report) {
  const report = data.report;
  const cwe = data.cwe;

  return {
    decision: {
      status: 'continue',
      message: 'Creating Vulnerability',
    },
    request: {
      body: buildRequestBody(report, cwe)
    },
    data: {}
  };
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing HackerOne Report',
    }
  };
}
  
function buildRequestBody (report, cwe) {
  const vuln = {
    projectId: "685500711d6a44e61f90db4e",
    title: "New Submission",
    affected_asset_name: "AttackForge-TEST H1B",
    priority: "Info",
    likelihood_of_exploitation: 1,
    description: "TBD",
    attack_scenario: "TBD",
    remediation_recommendation: "TBD",
    steps_to_reproduce: "TBD",
    tags: [
      'HackerOne',
      'H1'
    ],
    is_visible: true,
    custom_fields: []
  };

  if (cwe) {
    if (cwe.ID) {
      const name = cwe.ID;
      Array.push(vuln.tags, 'CWE-' + cwe.ID);
    }
    if (cwe.Name) {
      const name = cwe.Name;
      vuln.title = name;
    }
    if (cwe.Description) {
      let description = cwe.Description;
      vuln.description = formatVulnInfo(description);
    }
    if (cwe.ExtendedDescription) {
      let extendedDescription = cwe.ExtendedDescription;
      vuln.description = vuln.description + ' ' + formatVulnInfo(extendedDescription);      
    }
    if (cwe.BackgroundDetails) {
      const backgroundDetails = cwe.BackgroundDetails;
      vuln.attack_scenario = "";
      if (Array.isArray(backgroundDetails)) {
        for (let x = 0; x < backgroundDetails.length; x++) {
          const attackScenario = backgroundDetails[x];
          vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(attackScenario);
        }
      }
      else {
        vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(backgroundDetails);
      }
    }
    if (cwe.PotentialMitigations) {
      const potentialMitigations = cwe.PotentialMitigations;
      vuln.remediation_recommendation = "";
      if (Array.isArray(potentialMitigations)) {
        for (let x = 0; x < potentialMitigations.length; x++) {
          const mitigation = potentialMitigations[x];
          if (mitigation.Description) {
            vuln.remediation_recommendation = vuln.remediation_recommendation 
              + formatVulnInfo(mitigation.Description);
          } 
        }
      }
      else {
        if (potentialMitigations.Description) {
          vuln.remediation_recommendation = vuln.remediation_recommendation 
            + formatVulnInfo(potentialMitigations.Description);
        }
      }
    }
    if (report.attributes?.title && report.attributes.vulnerability_information) {
      vuln.steps_to_reproduce = '<p>' 
        + report.attributes.title + '</p><p>' 
        + formatVulnInfo(report.attributes.vulnerability_information) 
        + '</p>';
    }
  }
  else {
    if (report.attributes?.title) {
      vuln.title = report.attributes.title;
    }
    if (report.attributes?.vulnerability_information) {
      vuln.description = formatVulnInfo(report.attributes.vulnerability_information);
    }
  }

  if (report.id) {
    Array.push(vuln.custom_fields, {
      key: "hackerone_report_id",
      value: report.id
    });
    if (report.relationships?.program?.data?.attributes?.handle) {
      const handle = report.relationships.program.data.attributes.handle;
      Array.push(vuln.custom_fields, {
        key: "hackerone_report_url",
        value: 'https://hackerone.com/bugs?report_id=' + report.id + '&subject=' + handle
      });
    }
  }
  if (report.relationships?.severity?.data?.attributes) {
    const severity = report.relationships.severity.data.attributes;
    if (severity.rating === 'critical') {
      vuln.priority = 'Critical';
    }
    else if (severity.rating === 'high') {
      vuln.priority = 'High';
    }
    else if (severity.rating === 'medium') {
      vuln.priority = 'Medium';
    }
    else if (severity.rating === 'low') {
      vuln.priority = 'Low';
    }

    if (severity.score && severity.cvss_vector_string) {
      vuln.likelihood_of_exploitation = Number.parseInt(Math.ceil(severity.score));
      Array.push(vuln.tags, 'CVSSv3.1 Base Score: ' + severity.score);
      Array.push(vuln.tags, severity.cvss_vector_string);
    }
  }

  return vuln;
};

function formatVulnInfo (value) {
  if (String.startsWith(value, "\n\n")) {
    value = String.substring(value, 2);
  }
  else if (String.startsWith(value, "\n")) {
    value = String.substring(value, 1);
  }
  value = String.replaceAll(value, "\n\n", "<br/>");
  value = String.replaceAll(value, "\n", "<br/>");
  
  return value;
};
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.vulnerability?.vulnerability_id) {
  return {
    decision: {
      status: 'finish',
      message: 'Created Vulnerability'
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating Vulnerability'
    },
  };
}
```


# Bugcrowd

## Create Vulnerability on Bugcrowd Submission

<figure><img src="/files/FPH4lTxOt2TkrsX7oZHe" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create a vulnerability on a new a Bugcrowd submission.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_engagement\_name - your Bugcrowd engagement name
  * bugcrowd\_secret - your [Bugcrowd secret](https://docs.bugcrowd.com/api/webhooks/)
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * af\_project\_id - your AttackForge Project Id
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get Bugcrowd Submission**

* **Method**: GET
* **URL**: <https://api.bugcrowd.com/submissions/{id}?include=target,file\\_attachments\\&fields\\[target]=name,category\\&fields\\[submission]=remediation\\_advice,description,bug\\_url,severity,source,state,title,vrt\\_id,vulnerability\\_references>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
// Validate HMAC
const bcSecret = secrets.bugcrowd_secret;
const raw_digest = data.headers['X-Bugcrowd-Digest'];
const raw_body = data.body || '';

if (!raw_digest || !(raw_digest =~ m/timestamp=[0-9]+;sha256=[a-f0-9]+/i)) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing or invalid X-Bugcrowd-Digest'
    }
  };
}

const at_time = String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$1');
const expected_digest =  String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$2');
const payloadHMAC = 'sha256=' + String.toLowerCase(String.hmac(raw_body + at_time, bcSecret, 'SHA256','base16'));

if (expected_digest !== payloadHMAC) {
  Logger.error('Invalid HMAC, aborting process.');
  
  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (data.jsonBody.included?[0].id) {
  let options = '?include=target,file_attachments';
  options = options + '&fields[target]=name,category';
  options = options + '&fields[submission]=remediation_advice,description,bug_url,severity,source,state,title,vrt_id,vulnerability_references';
  
  return {
    decision: {
      status: 'continue',
      message: 'Fetching Bugcrowd Submission data with submission id',
    },
    request: {
      url: 'https://api.bugcrowd.com/submissions/' + data.jsonBody.included[0].id + options
    }
  };
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing Bugcrowd Submission Id',
    }
  };
}
```

* **Response Script**:

```javascript
const responseBody = response?.jsonBody;

if (!responseBody){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response?.jsonBody:', JSON.stringify(response?.jsonBody));
  }
  return {
    desicison: {
      status: 'abort',
      message: 'No jsonBody found from response'
    }
  };
}

if (response.headers['Content-Type'] !== 'application/json; charset=utf-8') {
  return {
    desicison: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (responseBody?.data?.attributes?.state !== 'new') {
  return {
    desicison: {
      status: 'abort',
      message: 'The submission state must be "new"'
    } 
  };
}

if (response.statusCode === 200) {
  Logger.info('Successful submission data fetch');
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved Bugcrowd Submission'
    },
    data: responseBody
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving Bugcrowd Submission'
    },
  };
}
```

**Action 2 - Prepare Vuln Data from Bugcrowd Submission**

* **Script**:

```javascript
let cwe_id;

return {
  decision: {
    status: 'continue',
  },
  data: {
    vulnerability: prepareInputData(data),
    cwe_id: cwe_id
  }
};

// Preparing Bugcrowd data for Vulnerability Creation
function prepareInputData (submissionFetched) {
  if (!submissionFetched?.data?.attributes) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Missing data.attribute for Vulnerability Submission Details');
    }
    
    return {
      decision: {
        status: 'abort',
        message: 'Missing data.attribute for Vulnerability Submission Details'
      }
    };
  }

  let vuln = {};
  
  vuln.projectId = secrets.af_project_id;
  vuln.import_source = "Bugcrowd";
  vuln.file_attachment = [];
  vuln.affected_assets = [];
  
  let affected_assets_obj = {};
  affected_assets_obj.components = [];
  let asset_components_obj = {};

  const submission = submissionFetched.data.attributes;
  
  asset_components_obj.name = submission.bug_url ? submission.bug_url : '';
  if (secrets.logging_level === 'debug') {
    Logger.debug('submission.bug_url:', submission?.bug_url);
  }
  
  if (submission.title){
    vuln.title = submission.title;
  }
  
  // if vrt_id exists - overwrite title
  if (submission.vrt_id) {
    const title_raw = String.replaceAll(String.replaceAll(submission.vrt_id, '_', ' '), '.', ' - ');
    const split_title = String.split(title_raw, ' ');
    if (secrets.logging_level === 'debug') {
      Logger.debug('split_title:', split_title);
    }
    
    let final_title = '';
    
    for (let i = 0; i < Array.length(split_title); i++){
      const split = split_title[i];
      if (split && split[0] && split[0] =~ m/[a-z]/){
        const first_letter_capital = String.toUpperCase(split[0]);
        final_title = final_title 
          + first_letter_capital 
          + String.substring(split_title[i], 1, split_title[i].length) 
          + ' ';
      }
      else {
        final_title = final_title + ' ' + split_title[i] + ' ';
      }
    }
    
    vuln.title = String.trim(final_title);
    if (secrets.logging_level === 'debug') {
      Logger.debug('vuln.title:', vuln.title);
    }
    
    vuln.original_title = submission.title;
  }

  if (submission.description){
    vuln.description = submission.description;
  }

  if (submission.vulnerability_references) {
    // Retrieve CWE
    const formatted_refs = formatVulnReferences(submission.vulnerability_references);
    const new_line_formatted = Array.join(formatted_refs.formatted_vuln_refs, '\n');
    vuln.description = vuln.description 
      + '\n\n' 
      + '<strong>Vulnerability References</strong>' 
      + '\n' 
      + new_line_formatted;
    cwe_id = formatted_refs.cwe_id ? formatted_refs.cwe_id : '';
  }

  if (submission.remediation_advice) {
    vuln.remediation_recommendation = submission.remediation_advice;
  }

  // Bugcrowd severity mapping
  const severityMap = {
    '1': 'Critical',
    '2': 'High',
    '3': 'Medium',
    '4': 'Low',
    '5': 'Info'
  };
  
  if (submission.severity){
    vuln.severity = severityMap[submission.severity] || 'Info';
  } else {
    vuln.severity = 'Info';
  }

  if (data.data?.id && data.data.links?.self) {
    vuln.custom = {
      bugcrowd_submission_id: data.data.id,
      bugcrowd_submission_url: 'https://tracker.bugcrowd.com/' + secrets.bugcrowd_engagement_name + data.data.links.self
    };
  }

  if (data.included){
    for (let i = 0; i < data.included.length; i++){
      if (data.included?[i].type === 'target' 
        && data.included[i].attributes?.name 
        && data.included[i].attributes.category
      ) {
        affected_assets_obj.assetName = data.included[i].attributes?.name;
        affected_assets_obj.tags = [];
        
        Array.push(affected_assets_obj.tags, data.included[i].attributes.category);
      }
      if (data.included?[i].type === 'file_attachment' 
        && data.included[i].attributes?.file_name 
        && data.included[i].attributes.file_type 
        && data.included[i].attributes.download_url
      ) {
        const s3_signed_url = data.included[i].attributes.s3_signed_url;
        const download_url = s3_signed_url ? s3_signed_url : data.included[i].attributes.download_url;
        const file_name = data.included?[i].attributes?.file_name;
        
        Array.push(vuln.file_attachment, {
          file_name: file_name,
          download_url: download_url
        });
      }
    }
  }

  // Push Vuln Asset Objects
  if (secrets.logging_level === 'debug') {
    Logger.debug('Vuln object created:', JSON.stringify(vuln));
  }
  Array.push(affected_assets_obj.components, asset_components_obj);
  Array.push(vuln.affected_assets, affected_assets_obj);

  return vuln;
}

function formatVulnReferences (ref_str){
  if (!ref_str) {
    return [];
  }
  
  const lines = String.split(ref_str, '\n');
  const result = [];

  for (let i = 0; i < Array.length(lines); i++){
    let line = lines[i];
    line = String.replaceAll(line, '* ', '');
    
    if (String.includes(line, 'https://cwe.mitre.org/data/definitions/')) {
      const cwe_url = String.replaceAll(line, m/\[([^\]]+)\]\(([^)]+)\)/g, '$2');
      cwe_id = String.split(cwe_url, 'https://cwe.mitre.org/data/definitions/')[1];
    }
    
    line = String.replaceAll(line, m/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" rel="noopener noreferrer" target="_blank">$1</a>');
    Array.push(result, line);
  }
  
  return {
    formatted_vuln_refs: result,
    cwe_id: cwe_id
  };
}
```

**Action 3 - Get CWE Information**&#x20;

* **Method**: GET
* **URL**: <https://cwe-api.mitre.org/api/v1/cwe/weakness/{id}>
* **Request Script**:

```javascript
if (data?.cwe_id) {
  return {
    decision: {
      status: 'continue',
      message: 'Fetching CWE [ ' + data.cwe_id + ' ] details',
    },
    request: {
      url: 'https://cwe-api.mitre.org/api/v1/cwe/weakness/' + data.cwe_id
    },
    data: {
      vulnerability: data.vulnerability
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No CWE found. Skip fetching CWE details.',
    }
  };
}
```

* **Response Script**:

```javascript
if (response.headers['Content-Type'] !== 'application/json; charset=UTF-8') {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=UTF-8'
    }
  };
}

if (response.statusCode === 200 && response?.jsonBody?.Weaknesses[0]) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved CWE details'
    },
    data: {
      vulnerability: data.vulnerability,
      cwe: response?.jsonBody?.Weaknesses[0]
    }
  };
}

else if (response.statusCode === 404){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Failed retreiving CWE details, the provided cwe_id may not be recommended for vulnerability mapping.'
    },
    data: data
  };
}

else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving CWE details'
    },
  };
}
```

**Action 4 - Create Vulnerability**

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.vulnerability) {
  return {
    decision: {
      status: 'abort',
      message: 'Bugcrowd Submission data does not exist on data.vulnerability',
    }
  };
}

const submission = data.vulnerability;
const cwe = data.cwe;

const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability';

return {
  request: {
    url: url,
    body: buildRequestBody(submission, cwe)
  },
  data: data
};

function buildRequestBody (submission, cwe) {
  const vuln = {
    projectId: submission.projectId, 
    title: "New Submission",
    affected_assets: submission.affected_assets,
    priority: submission.severity,
    likelihood_of_exploitation: 1,
    description: "",
    attack_scenario: "TBD",
    remediation_recommendation: "TBD",
    steps_to_reproduce: "TBD",
    tags: [],
    is_visible: true,
    import_source: "TBD",
    custom_fields: []
  };

  // custom field source to update comments
  Array.push(vuln.custom_fields, {
    key: 'source',
    value: 'Bugcrowd'
  });

  if (submission.import_source){
    vuln.import_source = submission?.import_source;
  }

  if (submission.custom?.bugcrowd_submission_id && submission.custom?.bugcrowd_submission_url) {
    Array.push(vuln.custom_fields, {
      key: 'bugcrowd_submission_id',
      value: submission.custom.bugcrowd_submission_id
    });
    Array.push(vuln.custom_fields, {
      key: "bugcrowd_submission_url",
      value: submission.custom.bugcrowd_submission_url
    });
  }

  if (submission.target?.name && submission.target.category) {
    Array.push(vuln.custom_fields, {
      key: 'target_name',
      value: submission.target.name
    });
    Array.push(vuln.custom_fields, {
      key: 'target_category',
      value: submission.target.category
    });
  }

  if (submission.tags && Array.isArray(submission.tags)) {
    for (let i = 0; i < submission.tags.length; i++){
      Array.push(vuln.tags, submission.tags[i]);
    }
  }

  if (submission.remediation_recommendation) {
    vuln.remediation_recommendation = submission.remediation_recommendation + '\n';
  }

  if (cwe) {
    if (cwe.ID) {
      const name = cwe;
      Array.push(vuln.tags, 'CWE-' + cwe.ID);
    }
    if (cwe.Name) {
      const name = cwe.Name;
      vuln.title = name;
    }
    if (cwe.Description) {
      let description = cwe.Description;
      if (secrets.logging_level === 'debug') {
        Logger.debug('description', description);
      }
      vuln.description = vuln.description + formatVulnInfo(description) + '\n';
    }
    if (cwe.ExtendedDescription) {
      let extendedDescription = cwe.ExtendedDescription;
      vuln.description = vuln.description + ' ' + formatVulnInfo(extendedDescription) + '\n';      
    }
    if (cwe.BackgroundDetails) {
      const backgroundDetails = cwe.BackgroundDetails;
      vuln.attack_scenario = "";
      if (Array.isArray(backgroundDetails)) {
        for (let x = 0; x < backgroundDetails.length; x++) {
          const attackScenario = backgroundDetails[x];
          vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(attackScenario);
        }
      }
      else {
        vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(backgroundDetails);
      }
    }
    if (cwe.PotentialMitigations) {
      const potentialMitigations = cwe.PotentialMitigations;
      if (vuln.remediation_recommentation === "TBD"){
        vuln.remediation_recommendation = "";
      }
      if (Array.isArray(potentialMitigations)) {
        for (let x = 0; x < potentialMitigations.length; x++) {
          const mitigation = potentialMitigations[x];
          if (mitigation.Description) {
            vuln.remediation_recommendation = vuln.remediation_recommendation 
              + formatVulnInfo(mitigation.Description);
          } 
        }
      }
      else {
        if (potentialMitigations.Description) {
          vuln.remediation_recommendation = vuln.remediation_recommendation 
            + formatVulnInfo(potentialMitigations.Description);
        }
      }
    }
    if (submission.title && submission.vulnerability_information) {
      vuln.steps_to_reproduce = '<p>' 
        + submission.title + '</p><p>' 
        + formatVulnInfo(submission_information) 
        + '</p>';
    }
  }
  else {
    if (submission.title) {
      vuln.title = submission.title;
    }
    if (submission.vulnerability_information) {
      vuln.description = formatVulnInfo(submission.vulnerability_information);
    }
  }

  let description = '';
  if (submission.original_title) {
    description = description + 'Bugcrowd Submission Title: ' + submission.original_title + '\n';
  }
  if (submission.description) {
    description = description + '\nBugcrowd Description: ' + submission.description;
  }
  
  vuln.description = vuln.description + description + '\n';

  if (secrets.logging_level === 'debug') {
    Logger.debug('Vuln payload:', JSON.stringify(vuln));
  }

  return vuln;
}

function formatVulnInfo (value) {
  if (String.startsWith(value, "\n\n")) {
    value = String.substring(value, 2);
  }
  else if (String.startsWith(value, "\n")) {
    value = String.substring(value, 1);
  }

  value = String.replaceAll(value, "\n\n", "<br/>");
  value = String.replaceAll(value, "\n", "<br/>");
  
  return value;
};
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify('Content-Type', response.headers['Content-Type']));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (data?.vulnerability?.file_attachment && Array.length(data.vulnerability.file_attachment) > 0 && response?.jsonBody?.vulnerability?.vulnerability_id){
  return {
    decision: {
      status: 'continue',
      message: 'File attachment found, continue.'
    },
    data: {
      files: data.vulnerability.file_attachment,
      vulnId: response?.jsonBody?.vulnerability?.vulnerability_id
      }
  };
}

if (Array.length(data.vulnerability.file_attachment) === 0 && response.statusCode === 200) {
  return {
    decision: {
      status: 'finish',
      message: 'Created Vulnerability'
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating Vulnerability'
    },
  };
}
```

**Action 5 - Download Evidence from Bugcrowd Submission**

* **Method**: GET
* **URL**: <https://defined-in-script>
* **Request Script**:

```javascript
const fileIds = data.fileIds || [];

if (!data.retry) {
  data.retry = 0;
}

if (data.files && Array.length(data.files) > 0) {
  const files = data.files;

  if(!data.iteration) {
    data.iteration = 0;
  }

  let i = data.iteration;
  
  if (i < Array.length(files)){
    return {
      decision: {
        status: 'continue',
        message: 'Downloading next attachment'
      },
      request: {
        url: files[i].download_url
      },
      data: {
        vulnId: data.vulnId,
        iteration: i,
        count: Array.length(files),
        files: files,
        fileIds: fileIds,
        retry: data.retry
      }
    }; 
  }
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'No more file to fetch, proceeding to next step.'
    },
    data: {
      vuln_id: data.vuln_id,
      files: files
    }
  }; 
}

```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  const file_name = data.files?[data.iteration].file_name;
  Logger.error('Failed downloading file', file_name);
  
  if (secrets.logging_level === 'debug') {
    Logger.debug(JSON.stringify(response));
  }

  if (!data.retry) {
    data.retry = 0;
  }
  
  if (data.retry < 3) {
    data.retry = data.retry + 1;
    return {
      decision: {
        status: 'repeat',
        message: 'Retrying to download file: ' + file_name,
        delay: 1000
      },
      data: data
    };
  }

  if (Array.length(data.files) >= data.iteration + 1){
    data.iteration = data.iteration + 1;
    data.retry = 0;
    
    return {
      decision: {
        status: 'repeat',
        message: 'Failed downloading file: ' + file_name + ', continuing to next file.'
      },
      data: data
    };
  } else {
    return {
      decision: {
        status: 'continue',
        message: 'Failed downloading file: ' + file_name + ', please check error logs. Proceeding to to upload step.'
      },
      data: data
    };
  }
}

const nextIteration = data.iteration + 1;
let fileIds = data.fileIds;
let fileId = response.fileId;
if (secrets.logging_level === 'debug') {
  Logger.info("Iteration:", nextIteration, JSON.stringify(fileIds));
}

Array.push(fileIds, {
  name: data.files[data.iteration].file_name, 
  fileId: fileId
});

if (nextIteration < data.count){
  data.iteration = nextIteration;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next file attachment.'
    },
    data: data
  };
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'Completed fetching attachments, proceeding to next step.'
    },
    data: {
      fileIds: fileIds,
      vulnId: data.vulnId
    } 
  };
}
```

**Action 6 - Upload Vulnerability Evidence**&#x20;

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}/evidence
* **Headers**:
  * Key = Content-Type; Type = Value; Value = multipart/form-data
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (!data.fileIds) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.fileIds',
    }
  };
}
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulnId',
    }
  };
}
if (data.fileIds.length === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No file to upload. Exiting process.',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vulnId + '/evidence';

return {
  decision: {
    status: 'continue',
    message: 'Uploading evidence to Vulnerability',
  },
  request: {
    url: url,
    multipart: {
      fields: [
        {
          name: 'file',
          fileId: data.fileIds[0].fileId
        }
      ]
    }
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200 || response.jsonBody?.status === 'error'){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while uploading evidence: ' + data.fileIds[0] + '. error: ' + response?.jsonBody?.error
    }
  };
}

Array.shift(data.fileIds);

if (Array.length(data.fileIds) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Next attachment found, proceeding to uploading.'
    },
    data: data
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No attachments left to upload. Exiting flow.'
    }
  };
}
```

## Update Vulnerability on Bugcrowd Submission Update

<figure><img src="/files/0lcF4ejRrXGHVqQurth3" alt=""><figcaption></figcaption></figure>

The purpose of this example is to update a vulnerability when a Bugcrowd submission is updated.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_engagement\_name - your Bugcrowd engagement name
  * bugcrowd\_secret - your [Bugcrowd secret](https://docs.bugcrowd.com/api/webhooks/)
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Determine Update Details**

* **Script**:

```javascript
// Validate HMAC
const bcSecret = secrets.bugcrowd_secret;
const raw_digest = data.headers['X-Bugcrowd-Digest'];
const raw_body = data.body || '';

if (!raw_digest || !(raw_digest =~ m/timestamp=[0-9]+;sha256=[a-f0-9]+/i)) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing or invalid X-Bugcrowd-Digest'
    }
  };
}

const at_time = String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$1');
const expected_digest =  String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$2');
const payloadHMAC = 'sha256=' + String.toLowerCase(String.hmac(raw_body + at_time, bcSecret, 'SHA256','base16'));

if (expected_digest !== payloadHMAC) {
  Logger.error('Invalid HMAC, aborting process.');
  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

let changes_keys;
let changed_type;
let changed_to = {};
let bc_sub_id;
let cwe_id;
const bugcrowd_submission_url_name = secrets.bugcrowd_engagement_name;

if (!data.jsonBody){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No jsonBody found in data.',
    }
  };
}

const changes = data.jsonBody?.data?.attributes?.data?.changes;

if (!changes){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(data.jsonBody?.data?.attributes?.data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No data.jsonBody?.data?.attributes?.data?.changes found in data.',
    }
  };
}

changes_keys = Object.keys(changes);

if (data.jsonBody?.data?.relationships?.resource?.data?.type === 'submission'){
  bc_sub_id = data.jsonBody.data.relationships.resource.data.id;
}

if (!bc_sub_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Bugcrowd submission id not found, aborting the process.'
    }
  };
}

if (Array.length(changes_keys) === 0) {
  Logger.error('Error: No changes found in data');
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(data?.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Event does not contain recognised change from Bugcrowd submission. Please check the log.'
    }
  };
}

// Case 1 - target change
if (determineUpdateType(['target_id'], changes_keys)){
  Logger.info('Found change type: Target');
  changed_type = 'target';
  if (changes.target_id?.to){
    changed_to.target_id = changes.target_id.to;
  }
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('changes.target_id: ', JSON.stringify(changes.target_id));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Changed target does not have an id, aborting the update.'
      }
    };
  }
}

// Case 2 - bug_url change
else if (determineUpdateType(['encrypted_bug_url'], changes_keys)){
  Logger.info('Found change type: bug_url');
  changed_type = 'bug_url';
  if (data.jsonBody?.included?[0].attributes?.bug_url){
    changed_to.component_name = data.jsonBody.included[0].attributes.bug_url;
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug(JSON.stringify(data?.jsonBody));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Updated bug_url not found.'
      }
    };
  }
}

// Case 3 - remediation advice change
else if (determineUpdateType(['remediation_advice'], changes_keys)){
  Logger.info('Found change type: remediation_advice');
  changed_type = 'remediation_advice';
  if (changes.remediation_advice?.to && changes.remediation_advice?.from){
    changed_to.remediation_recommendation = changes.remediation_advice.to;
    changed_to.remediation_recommendation_old = changes.remediation_advice.from;
  }
}

// Case 4 - References change
else if (determineUpdateType(['vulnerability_references'], changes_keys)){
  Logger.info('Found change type: vulnerability_references');
  if (changes.vulnerability_references?.to){
    changed_type = 'vulnerability_references';
    changed_to.vulnerability_references = changes.vulnerability_references.to;
  }
}

// Case 5 - CVSS change
else if (determineUpdateType(['cvss_vector_id', 'severity'], changes_keys)){
  changed_type = 'cvss';
  if (changes.cvss_vector_id?.to){
    changed_to.cvss_vector_id = changes.cvss_vector_id.to;
  }
  if (changes.severity?.to){
    changed_to.severity = changes.severity.to;
  }
}

// Case 6 - Marked as vrt update
else if (determineUpdateType(['vrt_id', 'cvss_vector_id', 'severity'], changes_keys) ||
  determineUpdateType(['vrt_id', 'cvss_vector_id'], changes_keys) ||
  determineUpdateType(['vrt_id', 'severity'], changes_keys)){

  changed_type = 'vrt';
  if (changes.vrt_id?.to){
    changed_to.vrt_id = convertVrtToTitleStyle(changes.vrt_id.to);
  }

  if (data.jsonBody?.included && data.jsonBody?.included[0]){
    const included = data.jsonBody?.included[0];
    if (included?.attributes){
      if (included.attributes?.remediation_advice){
        changed_to.remediation_recommendation = included.attributes?.remediation_advice;
      }
      if (included.attributes?.vulnerability_references){
        changed_to.vulnerability_references = included.attributes?.vulnerability_references;
        // Retrieve CWE ID if it exists
        const formatted_refs = findCweIdFromVulnRef(included.attributes?.vulnerability_references);
        const cwe_id = formatted_refs?.cwe_id;
        if (cwe_id){
          changed_to.cwe_id = cwe_id;
        }
      }
    }
  }
  if (changes.cvss_vector_id?.to){
    changed_to.cvss_vector_id = changes.cvss_vector_id.to;
  }
  if (changes.severity?.to){
    changed_to.severity = changes.severity.to;
  }
}

// Case 7 - Marked as duplicate update
else if (determineUpdateType(['vrt_id', 'duplicate', 'cvss_vector_id', 'duplicate_of_id', 'severity'], changes_keys) || determineUpdateType(['vrt_id', 'duplicate', 'cvss_vector_id', 'duplicate_of_id'], changes_keys)){
  Logger.info('Found change type: marked as duplicate.');
  changed_type = 'duplicate';
  if (changes?.duplicate_of_id?.to){
    changed_to.duplicate_of_id = changes.duplicate_of_id.to;
  }
}

// Case 8 - Title Change
else if (determineUpdateType(['title'], changes_keys)){
  changed_type = 'title';
  if (changes?.title?.to && changes?.title?.from){
    changed_to.title = changes.title.to;
    changed_to.title_old = changes.title.from;
  }
}

// Case 8 - Severity Change
else if (determineUpdateType(['severity'], changes_keys)){
  changed_type = 'severity';
  if (changes?.severity?.to){
    changed_to.severity = changes.severity.to;
  }
}

// Case 8 - Description Change
else if (determineUpdateType(['encrypted_description'], changes_keys)){
  changed_type = 'description';
  if (data.jsonBody?.included[0].attributes?.description){ 
    changed_to.description = data.jsonBody?.included[0].attributes?.description;
    changed_to.remediation_recommendation = data.jsonBody?.included[0].attributes?.remediation_advice;
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Found update details, proceeding to next step.',
  },
  data: {
    changed_type: changed_type,
    changed_to: changed_to,
    bc_sub_id: bc_sub_id,
    bugcrowd_submission_url_name: bugcrowd_submission_url_name
  },
};

function determineUpdateType(updateKeys, incomingKeys) {
  if (Array.length(updateKeys) !== Array.length(incomingKeys)) return false;
  for (let i = 0; i < Array.length(incomingKeys); i++) {
    if (!Array.includes(updateKeys, (incomingKeys[i]))) {
      return false;
    }
  }
  return true;
}

function convertVrtToTitleStyle(vrt){
  const changed_vrt_title = String.replaceAll(String.replaceAll(vrt, '_', ' '), '.', ' - '); 
  const split_title = String.split(changed_vrt_title, ' ');
  let final_title = '';
  
  for (let i = 0; i < split_title.length; i++){
    const split = split_title[i];
    if (split && split[0] && split[0] =~ m/[a-z]/){
      const first_letter_capital = String.toUpperCase(split[0]);
      final_title = final_title 
        + first_letter_capital 
        + String.substring(split_title[i], 1, split_title[i].length) 
        + ' ';
    }
    else {
      final_title = final_title + ' ' + split_title[i] + ' ';
    }
  }
  return String.trim(final_title);
}

function findCweIdFromVulnRef (ref_str){
  if (!ref_str) {
    return {cwe_id: ''};
  }
  
  const lines = String.split(ref_str, '\n');
  const result = [];

  for (let i = 0; i < Array.length(lines); i++){
    let line = lines[i];
    line = String.replaceAll(line, '* ', '');
    
    if (String.includes(line, 'https://cwe.mitre.org/data/definitions/')) {
      const cwe_url = String.replaceAll(line, m/\[([^\]]+)\]\(([^)]+)\)/g, '$2');
      cwe_id = String.split(cwe_url, 'https://cwe.mitre.org/data/definitions/')[1];

      // fallback cwe_id
      const raw = (cwe_url && cwe_url !== line) ? cwe_url : line;
      const id = String.replaceAll(raw, m/.*definitions\/([0-9]+).*/g, '$1');

      if (id && id =~ m/^[0-9]+$/){
        cwe_id = id;
      }
    }
  }
  return {
    cwe_id: cwe_id
  };
}
```

**Action 2 - Get Bugcrowd Target Information**

* **Method**: GET
* **URL**: <https://api.bugcrowd.com/submissions/{id}?include=target,cvss\\_vector>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.changed_type !== 'target' && data.changed_type !== 'bug_url'){
  return {
    decision: {
      status: 'next',
      message: 'Update is not on target or bug_url, proceeding to next step.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching the Bugcrowd submission with id: '+ data.bc_sub_id
  },
  request: {
    url: 'https://api.bugcrowd.com/submissions/' + data.bc_sub_id + '?include=target,cvss_vector'
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while fetching Bugcrowd submission id: ' + data.bc_sub_id,
    }
  };
}

let cvss_vector_string;
const included = response.jsonBody?.included;

if (included) {
  for (let i = 0; i < Array.length(included); i++){
    if (included?[i].type === 'target' && included[i].id === data.changed_to?.target_id){
      if (included[i].attributes?.name && included[i].attributes.category){
        data.changed_to.target_name = included[i].attributes.name;
        data.changed_to.target_category = included[i].attributes.category;
      } 
      else {
        if (secrets.logging_level){
          Logger.debug(included?[i].attributes);
        }
        return {
          decision: {
            status: 'abort',
            message: 'Error: included[i].attributes must contain name and category of the Bugcrowd target.'
          }
        };
      }
    }
  }
}

if (response.jsonBody?.data?.attributes){
  const sub_attributes = response.jsonBody.data.attributes;
  if (sub_attributes.bug_url && data?.changed_type !== 'bug_url'){
    data.changed_to.component_name = sub_attributes.bug_url;
  }
  if (sub_attributes.title){
    data.changed_to.original_title = sub_attributes.title;
  }
  if (sub_attributes.description){
    data.changed_to.description = sub_attributes.description;
  }
  if (sub_attributes.remediation_recommendation){
    data.changed_to.remediation_recommendation = sub_attributes.remediation_recommendation;
  }
  if (sub_attributes.severity){
    data.changed_to.severity = sub_attributes.severity;
  }
}

// Prepare creating new vulnerability.
function prepareInputData (input){
  let vuln = {};
  vuln.import_source = 'Bugcrowd';
  vuln.file_attachment = [];
  vuln.affected_assets = {};
  vuln.tags = [];

  if (response.jsonBody?.vulnerabilities?.vulnerability_title){
    vuln.title = response.jsonBody.vulnerabilities.vulnerability_title;
  }
  if (input?.changed_to?.description){
    vuln.description = data.changed_to.original_title + '\n' + input.changed_to.description;
  }
  if (input?.changed_to?.target_name){
    vuln.affected_assets.assetName = input.changed_to.target_name;
  }
  if (input?.changed_to?.target_category){
    vuln.affected_assets.components_tag = input.changed_to.target_category;
  }

  // Bugcrowd bug_url update
  if (input?.changed_to?.component_name){
    vuln.affected_assets.component_name = input.changed_to.component_name;
    const target_information = Array.find(included, findTargetInfo)?.attributes;
    if (target_information?.name && target_information.category){
      vuln.affected_assets.assetName = target_information.name;
      vuln.affected_assets.components_tag = target_information.category;
    } 
    else {
      if (secrets.logging_level){
        Logger.debug('target_information', target_information);
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: target_information must contain name and category of the Bugcrowd target.'
        }
      };
    }
  }

  if (response.jsonBody?.data?.attributes?.remediation_advice){
    vuln.remediation_recommendation = response.jsonBody.data.attributes.remediation_advice;
  } 
  else {
    if (secrets.logging_level){
      Logger.debug('response.jsonBody?.data?.attributes', response.jsonBody.data?.attributes);
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: response.jsonBody?.data?.attributes must contain remediation_advice.'
      }
    };
  }

  const severityMap = {
    '1': 'Critical',
    '2': 'High',
    '3': 'Medium',
    '4': 'Low',
    '5': 'Info'
  };
  
  if (input.changed_to.severity){
    vuln.severity = severityMap[input.changed_to.severity] || 'Info';
  } 
  else {
    vuln.severity = 'Info';
  }
  
  if (data.bc_sub_id && data.bugcrowd_submission_url_name) {
    vuln.custom = {
      bugcrowd_submission_id: data.bc_sub_id,
      bugcrowd_submission_url: 'https://tracker.bugcrowd.com/'+ data.bugcrowd_submission_url_name + '/submissions/' + data.bc_sub_id
    };
  }

  const cvss = Array.find(included, findCvssVectorData);
  if (cvss && cvss.attributes){
    cvss_vector_string = 'CVSS:' 
      + cvss.attributes.version
      + '/AV:' + cvss.attributes.attack_vector
      + '/AC:'+ cvss.attributes.attack_complexity
      + '/PR:'+ cvss.attributes.privileges_required
      + '/UI:'+ cvss.attributes.user_interaction
      + '/S:' + cvss.attributes.authorization_scope
      + '/C:' + cvss.attributes.confidentiality_impact
      + '/I:' + cvss.attributes.integrity_impact
      + '/A:' + cvss.attributes.availability_impact
      + ' [' + cvss.attributes.score + ']';
    Array.push(vuln.tags, cvss_vector_string);
  }
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln', vuln);
  }
  return vuln;
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully fetched Bugcrowd Submission and new target details.',
  },
  data: {
    changed_type: data?.changed_type,
    bc_sub_id: data?.bc_sub_id,
    vulnerability: prepareInputData(data)
  },
};

function findCvssVectorData (array){
  return array.type === 'cvss_vector';
}

function findTargetInfo (array){
  return array.type === 'target';
}
```

**Action 3 - Find Original Vulnerability**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerabilities?q={custom\_fields:{$elemMatch:{name:{$eq:"bugcrowd\_submission\_id"},value:{$eq:{bc\_sub\_id}}}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (!data?.bc_sub_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Missing bc_sub_id from data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Missing bugcrowd submission id, aborting.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability from AttackForge with bugcrowd_submission_id.',
  },
  request: {
    url: 'https://'+ secrets.af_tenant +'/api/ss/vulnerabilities?q={custom_fields:{$elemMatch:{name:{$eq:"bugcrowd_submission_id"},value:{$eq:"' + data.bc_sub_id + '"}}}}&limit=1',
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while fetching AttackForge Vulnerability, please check the bugcrowd_submission_id in data.',
    }
  };
}

const original_vuln = response.jsonBody?.vulnerabilities?[0];
if (!original_vuln){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response?.jsonBody?.vulnerabilities));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: original_vuln must be present under response.jsonBody?.vulnerabilities?[0].',
    }
  };
}
else if (!original_vuln.vulnerability_id){
  return {
    decision: {
      status: 'abort',
      message: 'Error while retrieving the value of vulnerability_id.',
    }
  };
}

const fileId = [];

// General Fields from Vulnerability
if (original_vuln?.vulnerability_id){
  data.vuln_id = original_vuln.vulnerability_id;
}
if (original_vuln?.vulnerability_project_id){
  data.project_id = original_vuln.vulnerability_project_id;
}
if (original_vuln?.vulnerability_description_html){
  data.vuln_description = original_vuln.vulnerability_description_html;
}
if (original_vuln?.vulnerability_remediation_recommendation_html){
  data.remediation_recommendation = original_vuln.vulnerability_remediation_recommendation_html;
}
if (original_vuln?.vulnerability_attack_scenario_html){
  data.attack_scenario = original_vuln.vulnerability_attack_scenario_html;
}

// TODO: Is this needed?
if (data.changed_type === 'remediation_advice' && original_vuln?.vulnerability_attack_scenario){
  data.attack_scenario = original_vuln?.vulnerability_attack_scenario;
}

if (original_vuln?.vulnerability_title !== data.title){
  data.title = original_vuln.vulnerability_title;
}

// Check if attachments exist
if (original_vuln?.vulnerability_evidence){
  const evidences = original_vuln.vulnerability_evidence;
  for (let i = 0; i < Array.length(evidences); i++){
    if (evidences?[i].file_id){
      Array.push(fileId, evidences[i].file_id);
    }
  }
}

if (secrets.logging_level === 'debug'){
  Logger.debug('fileId: ', fileId);
}
data.fileId = fileId || [];

return {
  decision: {
    status: 'continue',
    message: 'Successfully retrieved the original vulnerability, proceeding to next step.',
  },
  data: data
};
```

**Action 4 - Get Bugcrowd CVSS Vector**&#x20;

* **Method**: POST
* **URL**: <https://api.bugcrowd.com/submissions/{id}?include=cvss\\_vector>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.changed_type !== 'cvss' && data.changed_type !== 'vrt'){
  return {
    decision: {
      status: 'next',
      message: 'Change type not cvss or vrt. Skipping CVSS Vector String step.'
    }
  };
}

const bu_sub_id = data?.bc_sub_id;
if (!bu_sub_id){
  return {
    decision: {
      status: 'abort',
      message: 'Error: Bugcrowd submission id not provided.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Bugcrowd submission.',
  },
  request: {
    url: 'https://api.bugcrowd.com/submissions/' + bu_sub_id + '?include=cvss_vector'
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed to retrieve Bugcrowd submission data.'
    }
  };
}

if (!(response.jsonBody?.included?[0] && response.jsonBody.included[0].type === 'cvss_vector')){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response?.jsonBody?.included: ', JSON.stringify(response?.jsonBody?.included));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed to retrieve cvss_vector data from Bugcrowd submission. Missing response?.jsonBody?.included?[0] or response?.jsonBody?.included?[0].type'
    }
  };
}

let cvss_vector_string;
const cvss = response.jsonBody.included[0].attributes;
cvss_vector_string = 'CVSS:'
  + cvss.version
  + '/AV:' + cvss.attack_vector
  + '/AC:' + cvss.attack_complexity
  + '/PR:' + cvss.privileges_required
  + '/UI:' + cvss.user_interaction
  + '/S:' + cvss.authorization_scope
  + '/C:' + cvss.confidentiality_impact
  + '/I:' + cvss.integrity_impact
  + '/A:' + cvss.availability_impact
  + ' [' + cvss.score + ']';

if (secrets.logging_level === 'debug'){
  Logger.debug('cvss_vector_string:', cvss_vector_string);
}
data.cvss_vector_string = cvss_vector_string;

return {
  decision: {
    status: 'continue',
    message: 'Successfully retrieved CVSS vector string: ' + cvss_vector_string,
  },
  data: data,
};
```

**Action 5 - Convert Remediation Note to Rich-Text**

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (data.changed_type && data.changed_type !== 'remediation_advice' && data.changed_type && data.changed_type !== 'vrt'){
  return {
    decision: {
      status: 'next',
      message: 'Change type not remediation_advice or vrt, proceeding to next step.'
    },
    data: data
  };
}

if (!data.changed_to?.remediation_recommendation){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data.changed_to: ',JSON.stringify(data.changed_to));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: could not get changed detail of data.changed_to?.remediation_advice.'
    }
  };
}

const markdown_object = {
  rem_advice: String.replaceAll(data.changed_to?.remediation_recommendation, '\n', '\n\n')
};

if (data.changed_to.remediation_recommendation_old){
  markdown_object.rem_advice_old = String.replaceAll(data.changed_to?.remediation_recommendation_old, '\n', '\n\n');
}

return {
  decision: {
    status: 'continue',
    message: 'Converting markdown comment body to rich-text.'
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
    body: markdown_object
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while converting Markdown to RichText',
    }
  };
}

if (!response.jsonBody?.rem_advice){
    if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody: ',JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: converted remediation note body not found under response.jsonBody.',
    },
  };
}

data.changed_to.converted_rem_advice_body = response.jsonBody?.rem_advice;

if (response.jsonBody?.rem_advice_old){
  data.changed_to.converted_rem_advice_body_old = response.jsonBody.rem_advice_old;
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully converted Markdown remediation note body to RichText.',
  },
  data: data
};
```

**Action 6 - Get CWE information**

* **Method**: GET
* **URL**: <https://cwe-api.mitre.org/api/v1/cwe/weakness/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
// If changed vulnerability_reference has cwe fetch information.
if (data?.changed_to?.cwe_id) {
  return {
    decision: {
      status: 'continue',
      message: 'Fetching CWE [ ' + data.changed_to.cwe_id + ' ] details',
    },
    request: {
      url: 'https://cwe-api.mitre.org/api/v1/cwe/weakness/' + data.changed_to.cwe_id
    },
    data: data
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No CWE found. Skip fetching CWE details.',
    },
    data: data
  };
}
```

* **Response Script**:

```javascript
if (response.headers['Content-Type'] !== 'application/json; charset=UTF-8') {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=UTF-8'
    }
  };
}

if (response.statusCode === 200 && response?.jsonBody?.Weaknesses[0]) {
  data.changed_to.cwe = response?.jsonBody.Weaknesses[0];
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved CWE details'
    },
    data: data
  };
}

else if (response.statusCode === 404){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Failed retreiving CWE details, the provided cwe_id may not be recommended for vulnerability mapping.'
    },
    data: data
  };
}

else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving CWE details'
    },
  };
}
```

**Action 7 - Get Vuln Id for Duplicate**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerabilities?q={custom\_fields:{$elemMatch:{name:{$eq:"bugcrowd\_submission\_id"},value:{$eq:{bc\_sub\_id}}}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (data.changed_type !== 'duplicate') {
  return {
    decision: {
      status: 'next',
      message: 'Not a duplicate change, proceeding to next step.'
    },
    data: data
  };
}

if (data.changed_type === 'duplicate' && !data.changed_to?.duplicate_of_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Missing duplicate_of_id from data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Missing bugcrowd duplicate_of_id, aborting.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability from AttackForge with bugcrowd_submission_id.',
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?q={custom_fields:{$elemMatch:{name:{$eq:"bugcrowd_submission_id"},value:{$eq:"' + data.changed_to.duplicate_of_id + '"}}}}&limit=1'
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response:', JSON.stringify(response));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error: Something went wrong while retrieving the duplicated Vulnerability Id for Bugcrowd submission id: ' + data.changed_to?.duplicate_of_id,
    }
  };
}

const vuln = response.jsonBody?.vulnerabilities?[0];

if (vuln && vuln.vulnerability_id && vuln.vulnerability_title){
  data.duplicate_vuln_id = vuln.vulnerability_id;
  data.duplicate_vuln_title = vuln.vulnerability_title;
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody?.vulnerabilities:', JSON.stringify(response.jsonBody?.vulnerabilities));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error: response.jsonBody?.vulnerabilities[0] must exist and contain vulnerability_id and vulnerability_title.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully retrieved duplicate Vulnerability Id: ' + vuln.vulnerability_id,
  },
  data: data
};
```

**Action 8 - Update Vulnerability on AttackForge**

* **Method**: PUT
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (!data.vuln_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: vulnerability_id is required to update vulnerability.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability: ' + data.vuln_id,
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vuln_id,
    body: prepareUpdateBody(data)
  },
  data: data
};

// Prepare update body
function prepareUpdateBody(data){
  let body = {
    project_id : data?.project_id,
  };

  const severityMap = {
    '1': 'Critical',
    '2': 'High',
    '3': 'Medium',
    '4': 'Low',
    '5': 'Info'
  };

  if (data?.changed_type === 'target' || data?.changed_type === 'bug_url') {
    const affected_assets = data.vulnerability?.affected_assets;
    if (!affected_assets || !affected_assets?.assetName) {
      return {
        decision: {
          status: 'abort',
          message: 'Error: data?.vulnerability?.affected_assets and assetName is required to update target.',
        }
      };
    }

    body.affected_assets = [];

    let asset_obj = {
      assetName: affected_assets?.assetName,
      components: [
        {
          name: affected_assets?.component_name
        }
      ],
      tags: [
        affected_assets?.components_tag
      ]
    };

    if (secrets.logging_level === 'debug'){
      Logger.debug('asset_obj', JSON.stringify(asset_obj));
    }
    Array.push(body.affected_assets, asset_obj);
  }

  if (data.changed_type === 'remediation_advice' || data.changed_type === 'vrt'){
    if (data?.remediation_recommendation && data.changed_to?.converted_rem_advice_body && data.changed_to?.converted_rem_advice_body_old){
      // swap out
      const rem_rec_swapout = String.replace(data.remediation_recommendation, data.changed_to.converted_rem_advice_body_old, data.changed_to.converted_rem_advice_body);
      if (rem_rec_swapout === data.remediation_recommendation && data.changed_to?.remediation_recommendation_old){
        body.remediation_recommendation = formatVulnInfo(String.replace(data.remediation_recommendation, data.changed_to.remediation_recommendation_old, data.changed_to.converted_rem_advice_body));
      } else {
        body.remediation_recommendation = formatVulnInfo(rem_rec_swapout);
      }
    }
    if (data.vuln_description){
      body.description = formatReferences(data.vuln_description);
    }
    if (data.attack_scenario){
      body.attack_scenario = data.attack_scenario;
    }
  }

  if (data.changed_type === 'cvss' || data.changed_type === 'vrt'){
    if (!body.tags){
      body.tags = [];
    }
    Array.push(body.tags, data.cvss_vector_string);
  }

  if (data.changed_type === 'vrt'){
    body.title = data.changed_to.vrt_id;
  }

  if (data.changed_type === 'vulnerability_references' || data.changed_type === 'vrt') {
    const vuln_ref_split_string = '<strong>Vulnerability References</strong>\n';
    const preserved_description = String.split(data.vuln_description, vuln_ref_split_string)[0];

    if (!data.changed_to?.vulnerability_references){
      if (secrets.logging_level === 'debug'){
        Logger.debug('data.changed_to:', JSON.stringify(data.changed_to));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: data.changed_to must contain vulnerability_references.',
        }
      };
    }

    const new_description = preserved_description 
      + vuln_ref_split_string 
      + data.changed_to.vulnerability_references
      + '\n';
    body.description = formatReferences(new_description);

    if (!body.remediation_recommendation && data.remediation_recommendation){
      body.remediation_recommendation = data.remediation_recommendation;
    }
    if (data.attack_scenario){
      body.attack_scenario = data.attack_scenario;
    }
  }

  if (data.changed_type === 'duplicate'){
    body.title = '[Duplicate] ' + data.title;
    if (data.duplicate_vuln_id && data.duplicate_vuln_title){
      body.description = 'This Vulnerability was marked as a Duplicate to AttackForge Vulnerability - <a href=' 
        + 'https://'
        + secrets.af_tenant 
        + '/projects/' 
        + data.project_id 
        + '/vulnerabilities/' 
        + data.duplicate_vuln_id 
        + '>' 
        + data.duplicate_vuln_title 
        + '</a>' 
        + ' on Bugcrowd.' 
        + '\n' 
        + data.vuln_description;
    } 
    else {
      if (secrets.logging_level === 'debug'){
        Logger.debug('data:', JSON.stringify(data));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: data must contain duplicate_vuln_id and duplicate_vuln_title to populate description with duplicate details.',
        }
      };
    }
  }

  if (data.changed_type === 'title'){
    if (data.vuln_description && data.changed_to?.title && data.changed_to?.title_old){
      body.description = replaceVulnDescription(data.changed_type, data.vuln_description, data.changed_to?.title);
      if (data.remediation_recommendation){
        body.remediation_recommendation = data.remediation_recommendation;
      }
    } 
    else {
      if (secrets.logging_level === 'debug'){
        Logger.debug('data.changed_to: ', JSON.stringify(data.changed_to));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: data.changed_to must contain changed title.',
        }
      };
    }
  }

  if (secrets.logging_level === 'debug'){
    Logger.debug('body',JSON.stringify(body));
  }

  if (data.changed_to?.cwe) {
    const cwe = data.changed_to.cwe;
    if (cwe.ID) {
      const name = cwe;
      if (!body.tags){
        body.tags = [];
      }
      Array.push(body.tags, 'CWE-' + cwe.ID);
    }
    if (cwe.Name) {
      const name = cwe.Name;
      body.title = name;
    }
    if (cwe.Description) {
      let description = cwe.Description;
      if (secrets.logging_level === 'debug') {
        Logger.debug('description', description);
      }
      body.description = body.description + formatVulnInfo(description) + '\n';
    }
    if (cwe.ExtendedDescription) {
      let extendedDescription = cwe.ExtendedDescription;
      body.description = body.description + ' ' + formatVulnInfo(extendedDescription) + '\n';      
    }
    if (cwe.BackgroundDetails) {
      const backgroundDetails = cwe.BackgroundDetails;
      body.attack_scenario = "";
      if (Array.isArray(backgroundDetails)) {
        for (let x = 0; x < backgroundDetails.length; x++) {
          const attackScenario = backgroundDetails[x];
          body.attack_scenario = body.attack_scenario + formatVulnInfo(attackScenario);
        }
      }
      else {
        body.attack_scenario = body.attack_scenario + formatVulnInfo(backgroundDetails);
      }
    }
    if (cwe.PotentialMitigations) {
      const potentialMitigations = cwe.PotentialMitigations;
      body.remediation_recommendation = body.remediation_recommendation ? body.remediation_recommendation + '\n' : "";
      if (Array.isArray(potentialMitigations)) {
        for (let x = 0; x < potentialMitigations.length; x++) {
          const mitigation = potentialMitigations[x];
          if (mitigation.Description) {
            body.remediation_recommendation = body.remediation_recommendation 
              + formatVulnInfo(mitigation.Description);
          } 
        }
      }
      else {
        if (potentialMitigations.Description) {
          body.remediation_recommendation = body.remediation_recommendation 
            + formatVulnInfo(potentialMitigations.Description);
        }
      }
    }
  }

  if (data.changed_type === 'severity' || data.changed_to?.severity){
    if (data.changed_to?.severity){
      body.priority = severityMap[data.changed_to.severity] || 'Info';
    } else {
      body.priority = 'Info';
    }
  }

  if (data.changed_type === 'description'){
    if (data.changed_to?.remediation_recommendation){
      body.remediation_recommendation = data.changed_to?.remediation_recommendation;
    }
    if (data.changed_to?.description){
      body.description = replaceVulnDescription(data.changed_type, data.vuln_description, data.changed_to.description);
    }
  }

  return body;
}

function formatVulnInfo (value) {
  if (String.startsWith(value, "\n\n")) {
    value = String.substring(value, 2);
  }
  else if (String.startsWith(value, "\n")) {
    value = String.substring(value, 1);
  }
  value = String.replaceAll(value, "\n\n", "<br/>");
  value = String.replaceAll(value, "\n", "<br/>");
  
  return value;
};

function formatReferences(vuln_ref){
  return String.replaceAll(vuln_ref, m/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" rel="noopener noreferrer" target="_blank">$1</a>');
}

function replaceVulnDescription (type, vuln_description, text){

  const title = String.replaceAll(
    vuln_description,
    m/[\s\S]*Bugcrowd\sSubmission\sTitle:\s*([^\n]*)[\s\S]*/gi,
    '$1'
  );

  const description_block = String.replaceAll(
    vuln_description,
    m/[\s\S]*Bugcrowd\sDescription:\s*([\s\S]*?)(?=\n\n<strong>\s*Vulnerability\sReferences)/gi,
    '$1'
  );

  const split_desc = String.split(description_block, '<strong>Vulnerability References</strong>');

  let new_description;
  if (type === 'title'){
    new_description = 'BugCrowd Submission Title: ' + text + '\n' + '\nBugCrowd Description: ' + split_desc[0] + '\n\n<strong>Vulnerability References</strong>' + split_desc[1];
  } else if (type === 'description'){
    new_description = 'BugCrowd Submission Title: ' + title + '\n' + '\nBugCrowd Description: ' + text + '\n\n<strong>Vulnerability References</strong>' + split_desc[1];
  }
  Logger.debug('new_description', new_description);
  return formatReferences(new_description);
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while updating the Vulnerability.',
    }
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed updating Vulnerability',
    }
  };
}
```

## Create Bugcrowd Comment on New Remediation Note

<figure><img src="/files/I2NOvmt6DNaItsKGHtqH" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create a comment in Bugcrowd when a remediation note is created.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event:**
  * vulnerability-remediation-note-created
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_visibility - must be one of the following: *everyone, bugcrowd\_and\_researcher, bugcrowd\_and\_customer, customer, bugcrowd*
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get AttackForge Project ID and Bugcrowd Submission ID**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
const vuln_id = data.remediation_note_vulnerability?.vulnerability_id;

if (!vuln_id){
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.remediation_note_vulnerability?.vulnerability_id'
    }
  };
}
if (!data.remediation_note_details) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.remediation_note_details'
    }
  };
}
if (data.remediation_note_details =~ m/said on Bugcrowd\s\[https:\/\/tracker\.bugcrowd\.com\/.*\/submissions\/.*\]/) {
  return {
    decision: {
      status: 'finish',
      message: 'This remediation note is posted from Bugcrowd, no action required.'
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug(data?.remediation_note_details);
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability ID: ' + vuln_id,
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vuln_id
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Retrieving Vulnerabiliy data was not successful.'
    }
  };
}
if (!response.jsonBody?.vulnerability) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while fetching Vulnerability information. Missing response.jsonBody?.vulnerability.'
    }
  };
}

const vuln = response.jsonBody.vulnerability;
const vuln_project_id = vuln.vulnerability_projects?[0].id;

if (!vuln_project_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln.vulnerability_projects: ', JSON.stringify(vuln.vulnerability_projects));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: vuln_project_id must be present, aborting.'
    }
  };
}
if (!vuln?.vulnerability_custom_fields) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln: ', JSON.stringify(vuln));
  }
  return {
    decision: {
      status: 'abort',
      message: 'bugcrowd_submission_id not found in vuln.'
    }
  };
}
  
data.vuln_project_id = vuln_project_id;

let bugcrowd_submission_id;
const vuln_custom_fields = vuln.vulnerability_custom_fields;

for (let i = 0; i < Array.length(vuln_custom_fields); i++){
  const customField = vuln_custom_fields[i];
  if (customField.key === 'bugcrowd_submission_id' && customField.value) {
    bugcrowd_submission_id = customField.value;
    return {
      decision: {
        status: 'continue',
        message: 'Vulnerability ID is from ' + bugcrowd_submission_id,
      },
      data: {
        note: data,
        bugcrowd_submission_id: bugcrowd_submission_id
      }
    };
  }
}
```

**Action 2 - Format Remediation Note**

* **Script**:

```javascript
const rem_note = data.note?.remediation_note_details_html 
  ? data.note?.remediation_note_details_html 
  : data.note?.remediation_note_details;

if (!rem_note){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No remediation note detail found.',
    },
  };
}

// Add your custom style to be converted to Bugcrowd Markdown
const rules = [
  ['<p>', '\n'],
  ['</p>', ''],

  // quotes
  ['<blockquote>', '\n>'],
  ['</blockquote>', '\n'],

  // bold/italic
  ['<strong>', '**'],
  ['</strong>', '**'],
  ['<em>', '_'],
  ['</em>', '_'],

  // tags not directly applicable to Bugcrowd
  ['<u>', ''], ['</u>', ''],
  ['<s>', ''], ['</s>', ''],
  [m/<span[^>]*>/gi, ''], ['</span>', ''],

  // code
  [m/<code[^>]*>/gi, '`'],
  ['</code>', '`'],
];

let rem_note_reformat = applyRules(data.note?.remediation_note_details_html, rules);

if (rem_note_reformat =~ m/<a href/){
  rem_note_reformat = formatHyperlink(rem_note_reformat);
}

data.note.remediation_note_details_reformatted = rem_note_reformat;

return {
  decision: {
    status: 'continue',
    message: 'Successfully reformatted remediation note to Bugcrowd comment format.',
  },
  data: data
};

function applyRules (text, rules){
  let applied_text = text;
  for (let i = 0; i < Array.length(rules); i++){
    applied_text = String.replaceAll(applied_text, rules[i][0], rules[i][1]);
  }
  return applied_text;
}

function formatHyperlink (text){
  return String.replaceAll(
    text,
    m/<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi,
    '[$2]($1)'
  );
}
```

**Action 3 - Create Bugcrowd Submission Comment**

* **Method**: POST
* **URL**: <https://api.bugcrowd.com/comments>
* **Headers:**
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let note;
let bc_submission_id;

// Bugcrowd Visibilities: 
// "everyone" 
// "bugcrowd_and_researcher" 
// "bugcrowd_and_customer" 
// "customer" 
// "bugcrowd"
let visibility = secrets.bugcrowd_visibility;

const rem_note = data.note?.remediation_note_details_reformatted 
  ? data.note?.remediation_note_details_reformatted 
  : data.note?.remediation_note_details;

if (!data.note?.remediation_note_created) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.note?.remediation_note_created was not found.'
    }
  };
}

const author_date_time_string = data.note?.remediation_note_user?.first_name 
  + ' on ' + Date.format(data.note.remediation_note_created, 'dddd d mmmm yyyy');

note = '[' 
  + author_date_time_string 
  + ' said on AttackForge](https://andy.attackforge.dev/projects/' 
  + data.note.vuln_project_id
  + '/vulnerabilities/' 
  + data.note.remediation_note_vulnerability.vulnerability_id 
  + '#' 
  + data.note.remediation_note_created 
  + ') : ' 
  + rem_note;

if (!data.bugcrowd_submission_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: bugcrowd_submission_id was not found on data.'
    }
  };
}

bc_submission_id = data?.bugcrowd_submission_id;

const requestBody = {
  data: {
    type: "comment",
    attributes: {
        body: note,
        visibility_scope: visibility
    },
    relationships: {
      submission: {
        data: {
          type: "submission",
          id: bc_submission_id
        }
      }
    }
  }
};

return {
  decision: {
    status: 'continue',
    message: 'Creating comment on Bugcrowd',
  },
  request: {
    body: requestBody,
  }
};
```

* **Response Script**:

```javascript
if (!response.statusCode === 201){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while creating comment on Bugcrowd, error code: ' + response.statusCode
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Created comment on Bugcrowd.'
  }
};
```

## Create Bugcrowd Comment on Updated Remediation Note

<figure><img src="/files/MeT0UTwuvf6ld7rQfieo" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create a comment in Bugcrowd when a remediation note is updated.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event:**
  * vulnerability-remediation-note-updated
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_visibility - must be one of the following: *everyone, bugcrowd\_and\_researcher, bugcrowd\_and\_customer, customer, bugcrowd*
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get AttackForge Project ID and Bugcrowd Submission ID**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (data.remediation_note_is_deleted) {
  return {
    decision: 'finish',
    message: 'Deleted messages are preserved by design. Finishing the process.'
  };
}
if (!data.remediation_note_vulnerability?.vulnerability_id){
  return {
    decision: {
      status: 'abort',
      message: 'vulnerability_id not found'
    }
  };
}
if (!data.remediation_note_details) {
  return {
    decision: {
      status: 'abort',
      message: 'data.remediation_note_details not found'
    }
  };
}

if (secrets.logger_level === 'debug'){
  Logger.debug(data.remediation_note_details);
}

if (data.remediation_note_details =~ m/said on Bugcrowd\s\[https:\/\/tracker\.bugcrowd\.com\/.*\/submissions\/.*\]\n\n\>/) {
  return {
    decision: {
      status: 'finish',
      message: 'This remediation note is posted from Bugcrowd, no action required.'
    }
  };
}

const vuln_id = data.remediation_note_vulnerability.vulnerability_id;

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability ID: ' + vuln_id,
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vuln_id,
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Retrieving Vulnerabiliy data was not successful.'
    }
  };
}
if (!response.jsonBody?.vulnerability) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Missing response.jsonBody?.vulnerability.'
    }
  };
}

let bugcrowd_submission_id;
const vuln = response.jsonBody.vulnerability;

if (!vuln.vulnerability_custom_fields) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln.vulnerability_custom_fields: ', JSON.stringify(vuln.vulnerability_custom_fields));
  }
  return {
    decision: {
      status: 'abort',
      message: 'bugcrowd_submission_id not found'
    }
  };
}

const vuln_project_id = vuln.vulnerability_projects?[0].id;
data.vuln_project_id = vuln_project_id;

for (let i = 0; i < Array.length(vuln.vulnerability_custom_fields); i++){ 
  const customField = vuln.vulnerability_custom_fields[i];
  if (customField.key === 'bugcrowd_submission_id' && customField.value) {
    bugcrowd_submission_id = customField.value;
    return {
      decision: {
        status: 'continue',
        message: 'Vulnerability ID is from ' + bugcrowd_submission_id,
      },
      data: {
        note: data,
        bugcrowd_submission_id: bugcrowd_submission_id
      }
    };
  }
}

return {
  decision: {
    status: 'finish',
    message: 'Vulnerability not from BugCrowd, exiting the process.'
  }
};
```

**Action 2 - Format Remediation Note**

* **Script**:

```javascript
const rem_note = data.note?.remediation_note_details_html 
  ? data.note?.remediation_note_details_html 
  : data.note?.remediation_note_details;

if (!rem_note){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No remediation note detail found.',
    },
  };
}

// Add your custom style to be converted to Bugcrowd Markdown
const rules = [
  ['<p>', '\n'],
  ['</p>', ''],

  // quotes
  ['<blockquote>', '\n>'],
  ['</blockquote>', '\n'],

  // bold/italic
  ['<strong>', '**'],
  ['</strong>', '**'],
  ['<em>', '_'],
  ['</em>', '_'],

  // tags not directly applicable to Bugcrowd
  ['<u>', ''], ['</u>', ''],
  ['<s>', ''], ['</s>', ''],
  [m/<span[^>]*>/gi, ''], ['</span>', ''],

  // code
  [m/<code[^>]*>/gi, '`'],
  ['</code>', '`'],
];

let rem_note_reformat = applyRules(data.note?.remediation_note_details_html, rules);

if (rem_note_reformat =~ m/<a href/){
  rem_note_reformat = formatHyperlink(rem_note_reformat);
}

data.note.remediation_note_details_reformatted = rem_note_reformat;

return {
  decision: {
    status: 'continue',
    message: 'Successfully reformatted remediation note to Bugcrowd comment format.',
  },
  data: data
};

function applyRules (text, rules){
  let applied_text = text;
  for (let i = 0; i < Array.length(rules); i++){
    applied_text = String.replaceAll(applied_text, rules[i][0], rules[i][1]);
  }
  return applied_text;
}

function formatHyperlink (text){
  return String.replaceAll(
    text,
    m/<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi,
    '[$2]($1)'
  );
}
```

**Action 3 - Create Bugcrowd Submission Comment**

* **Method**: POST
* **URL**: <https://api.bugcrowd.com/comments>
* **Headers:**
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let note;
let bc_submission_id;

// Bugcrowd Visibilities: 
// "everyone" 
// "bugcrowd_and_researcher" 
// "bugcrowd_and_customer" 
// "customer" 
// "bugcrowd"
let visibility = secrets.bugcrowd_visibility;

const rem_note = data.note?.remediation_note_details_reformatted 
  ? data.note?.remediation_note_details_reformatted 
  : data.note?.remediation_note_details;

if (!data.note?.remediation_note_created) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.note?.remediation_note_created was not found.'
    }
  };
}

const author_date_time_string = data.note?.remediation_note_user?.first_name 
  + ' on ' + Date.format(data.note.remediation_note_created, 'dddd d mmmm yyyy');

note = '[' 
  + author_date_time_string 
  + ' said on AttackForge](https://andy.attackforge.dev/projects/' 
  + data.note.vuln_project_id
  + '/vulnerabilities/' 
  + data.note.remediation_note_vulnerability.vulnerability_id 
  + '#' 
  + data.note.remediation_note_created 
  + ') : ' 
  + rem_note;

if (!data.bugcrowd_submission_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: bugcrowd_submission_id was not found on data.'
    }
  };
}

bc_submission_id = data?.bugcrowd_submission_id;

const requestBody = {
  data: {
    type: "comment",
    attributes: {
        body: note,
        visibility_scope: visibility
    },
    relationships: {
      submission: {
        data: {
          type: "submission",
          id: bc_submission_id
        }
      }
    }
  }
};

return {
  decision: {
    status: 'continue',
    message: 'Creating comment on Bugcrowd',
  },
  request: {
    body: requestBody,
  }
};
```

* **Response Script**:

```javascript
if (!response.statusCode === 201){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while creating comment on Bugcrowd, error code: ' + response.statusCode
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Created comment on Bugcrowd.'
  }
};
```

## Create/Update Remediation Notes on Created/Updated Bugcrowd Comments

<figure><img src="/files/z6IESy8hU0VdENVwSA9B" alt=""><figcaption></figcaption></figure>

The purpose of this example is to schedule a flow to run each day to create or update remediation notes based on comments activity in Bugcrowd.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Schedule**
  * **Cron String:** \*/30 \* \* \* \*
  * **Frequency**: Run every 30 minutes
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_engagement\_name - your Bugcrowd engagement name
  * bugcrowd\_secret - your [Bugcrowd secret](https://docs.bugcrowd.com/api/webhooks/)
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get Bugcrowd Vulnerabilities**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerabilities?q={custom\_fields:{$elemMatch:{name:{$eq:"source"},value:{$eq:"Bugcrowd"}}}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';
let query = '{custom_fields:{$elemMatch:{name:{$eq:"source"},value:{$eq:"Bugcrowd"}}}}';
url = url + 'q=' + query;

return {
  decision: {
    status: 'continue',
    message: 'Fetching AttackForge Vulnerabilities'
  },
  request: {
    url: url
  }
};
```

* **Response Script**:

```javascript
const vulns = response.jsonBody?.vulnerabilities;

if (!Array.isArray(vulns)){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No vulnerabilities found from response',
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug('Vulnerabilities created from Bugcrowd found: ', vulns);
}

return {
  decision: {
    status: 'next',
    message: 'Bugcrowd sourced vulnerabilities retrieved.',
  },
  data: vulns
};
```

**Action 2 - Format Remediation Notes**

* **Script**:

```javascript
if (!Array.isArray(data) || Array.length(data) === 0){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No vulnerabilities present in data, please check your request.',
    },
  };
}

// Collection of bc_sub_ids and its rem_notes & bc_comments
const rem_notes = {};
const vuln_dictionary = {};
const bc_sub_ids = [];
const seen = {};

for (let i = 0; i < Array.length(data); i++){
  const vuln = data[i];

  if (!vuln?.vulnerability_custom_fields){
    continue;
  }

  const bcSubVuln = Array.find(vuln.vulnerability_custom_fields, findBugcrowdSubId);
  if (!bcSubVuln || !bcSubVuln.value){
    continue;
  }
  
  const bc_sub_id = bcSubVuln.value;
  if (!vuln_dictionary[bc_sub_id]){
    vuln_dictionary[bc_sub_id] = {
      projectId: vuln.vulnerability_project_id,
      vuln_id: vuln.vulnerability_id
    };
  }

  if (!seen[bc_sub_id]) {
    seen[bc_sub_id] = true;
    Array.push(bc_sub_ids, bc_sub_id);
  }

  const notes = vuln.vulnerability_remediation_notes;
  // Skip all the vulnerabilities with no remediation note.
  if (Array.isArray(notes) && Array.length(notes) > 0){
    for (let j = 0; j < Array.length(notes); j++){
      const current_note = notes[j];
      const html = current_note?.note_html;
      const note_body = current_note?.note;

      if (!html || !note_body) {
        continue;
      }
      // must include a bugcrowd tracker URL with #<uuid> and not #undefined
      if (!(html =~ m/<a href="https:\/\/tracker.bugcrowd.com\/.*\/submissions\/.*#(?!undefined\b)([0-9a-f-]{8}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{12}).*/i)){
        continue;
      }

      const comment_id = String.replaceAll(
        html,
        m/.*#([0-9a-f-]{8}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{12}).*/gi,
        '$1'
      );

      if (!comment_id || comment_id === html){
        if (secrets.logging_level === 'debug'){
          Logger.debug('comment_id: ', comment_id);
          Logger.debug('html: ', html);
        }
        return {
          decision: {
            status: 'abort',
            message: 'Error while retrieving comment message. comment_id should be substring of uuid from html.'
          }
        };
      }

      rem_notes[comment_id] = {
        bc_sub_id: bc_sub_id,
        af_vuln_id: vuln.vulnerability_id,
        rem_note_id: current_note.id,
        rem_note: note_body,
        rem_note_html: html,
        vuln_project_id: vuln.vulnerability_project_id
      };
    }
  }
}


return {
  decision: {
    status: 'continue',
    message: 'Processed remediation note and reformatted',
  },
  data: {
    rem_notes: rem_notes,
    bc_sub_ids: bc_sub_ids,
    vuln_dictionary: vuln_dictionary
  }
};

function findBugcrowdSubId(custom_field){
  return custom_field.key === 'bugcrowd_submission_id';
}
```

**Action 3 - Get Comments from Submission**

* **Method**: GET
* **URL**: <https://api.bugcrowd.com/submissions?fields\\[comment]=body,created\\_at,author,file\\_attachments\\&include=comments,comments.author,comments.file\\_attachments\\&page\\[limit]=25\\&page\\[offset]=0>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.bc_comments){
  data.bc_comments = [];
}

// pagination initialisation
if (!data.page_size){
  data.page_size = 25;
}
if (!data.offset){
  data.offset = 0;
}
if (!data.total_hits){
  data.total_hits = 0;
}

let url = 'fields[comment]=body,created_at,author,file_attachments&include=comments,comments.author,comments.file_attachments&page[limit]=' 
  + data.page_size + '&page[offset]=' + data.offset;

if (secrets.logging_level === 'debug'){
  Logger.debug('url', url);
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Bugcrowd submission',
  },
  request: {
    url: 'https://api.bugcrowd.com/submissions?' + url
  },
  data: {
    rem_notes: data.rem_notes,
    bc_comments: data.bc_comments,
    vuln_dictionary: data.vuln_dictionary,
    bc_sub_ids: data.bc_sub_ids,
    page_size: data.page_size,
    offset: data.offset,
    total_hits: data.total_hits
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: 'abort',
    message: 'Error while fetching comments from submissions.'
  };
}

if (!response.jsonBody?.data){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: 'abort',
    message: 'No response.jsonBody?.data was found from response returned.'
  };
}

// Initialise bc_comments array
if (!data?.bc_comments){
  data.bc_comments = [];
}

// Initialise author object to collect author information over repeated loops
if (!data.author){
  data.author = {};
}

const meta_data = response.jsonBody?.meta;
if (!meta_data){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody', JSON.stringify(response.jsonBody));
  }
  return {
    decision: 'abort',
    message: 'Error: meta_data is required to process pagination.'
  };
}

if (!(meta_data.total_hits && meta_data.count)){
  if (secrets.logging_level === 'debug'){
    Logger.debug('meta_data', JSON.stringify(meta_data));
  }
  return {
    decision: 'abort',
    message: 'Error: meta_data must contain total_hits and count to process pagination.'
  };
}

data.total_hits = meta_data.total_hits;
data.offset = data.offset + meta_data.count;

const bugcrowd_submissions = response.jsonBody.data;

// comment_ids format: {comment_id : submission_id}
const comment_ids = {};

for (let i = 0; i < Array.length(bugcrowd_submissions); i++){
  const submission = bugcrowd_submissions[i];
  if (!submission.relationships?.comments || Array.length(submission.relationships?.comments?.data) === 0){
    continue;
  }
  const comments = submission.relationships.comments?.data;
  for (let j = 0; j < Array.length(comments); j++){
    comment_ids[comments[j].id] = submission.id;
  }
}

const included = response.jsonBody?.included;

if (!included){
  return {
    decision: 'abort',
    message: 'No response.jsonBody?.included was found from response returned.'
  };
}

for (let i = 0; i < Array.length(included); i++){
  if (included[i].type === 'identity'){
    if (!data.author[included[i].id]){
      data.author[included[i].id] = {
        name: included[i]?.attributes?.name,
        email: included[i]?.attributes?.email,
      };
    } 
    else {
      continue;
    }
  }
}

for (let i = 0; i < Array.length(included); i++){
  if (included[i].type === 'comment'){
    const comment_body = included[i]?.attributes.body;
    if (!comment_body || comment_body === '[DELETED]') {
      continue;
    }
    if (comment_body =~ m/^\[.*said on AttackForge\]/) {
      continue;
    }
    if (included[i]?.relationships?.author?.data?.id){
      const comment = {
        id : included[i].id,
        bc_sub_id : comment_ids[included[i].id],
        body : included[i].attributes?.body,
        created_at : included[i].attributes?.created_at,
        author : data.author[included[i].relationships.author.data.id].name,
        author_email : data.author[included[i].relationships.author.data.id].email
      };
      Array.push(data.bc_comments, comment);
    }
  }
}

if (data.offset < data.total_hits){
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching more submissions from Bugcrowd',
    },
    data: {
      rem_notes: data.rem_notes,
      bc_comments: data.bc_comments,
      vuln_dictionary: data.vuln_dictionary,
      offset : data.offset,
      page_size: data.page_size,
      total_hits : data.total_hits
    }
  };
} 
else {
  return {
    decision: {
      status: 'continue',
      message: 'Fetched all comments from Bugcrowd submissions.',
    },
    data: {
      rem_notes: data.rem_notes,
      bc_comments: data.bc_comments,
      vuln_dictionary: data.vuln_dictionary
    }
  };
}
```

**Action 4 - Convert Markdown to Rich Text**&#x20;

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
const bc_object = {};

if (data.bc_comments) {
  for (let i = 0; i < Array.length(data.bc_comments); i++){
    if (data.bc_comments[i].body && data.bc_comments[i].id) {
      const new_line_converted_body = String.replaceAll(String.replaceAll(data.bc_comments[i].body, '\n\n\n', '\n\n'), '\n', '\n\n');
      bc_object[data.bc_comments[i].id] = new_line_converted_body;
    }
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Converting markdown comment body to richtext.'
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
    body:  bc_object
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while converting Markdown to RichText',
    },
    data: data
  };
}

if (!data) {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: missing data',
    }
  };
}

const converted_comment_body = response.jsonBody;
if (!converted_comment_body){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: converted comment body not found',
    }
  };
}

const comment_body_full = {};
const ids = Object.keys(converted_comment_body);

if (data.bc_comments){
  for (let i = 0; i < Array.length(data.bc_comments); i++){
    let current_id = data.bc_comments[i].id;
    comment_body_full[current_id] = {
      bc_sub_id: data.bc_comments[i].bc_sub_id,
      id: data.bc_comments[i].id,
      body: data.bc_comments[i].body,
      rich_text_body: converted_comment_body[current_id],
      created_at: data.bc_comments[i].created_at,
      author: data.bc_comments[i].author,
      author_email: data.bc_comments[i].author_email,
    };
    if (secrets.logging_level === 'debug'){
      Logger.debug('data.bc_comments[i]', JSON.stringify(data.bc_comments[i]));
    }
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully converted Markdown comment body to RichText.',
  },
  data: {
    rem_notes: data.rem_notes,
    bc_comments: comment_body_full,
    vuln_dictionary: data.vuln_dictionary
  }
};
```

**Action 5 - Create or Update Comments**

* **Script**:

```javascript
const create_required = [];
const update_required = [];

const vuln_dictionary = data.vuln_dictionary || {};
const bc_comments = data.bc_comments || {};
const rem_notes = data.rem_notes || {};

const bc_comment_ids = Object.keys(bc_comments);

for (let i = 0; i < Array.length(bc_comment_ids); i++){
  const bc_comment_id = bc_comment_ids[i];
  const current_comment = bc_comments[bc_comment_id];
  if (!current_comment) {
    continue;
  }

  const vuln = vuln_dictionary[current_comment.bc_sub_id];
  if (!vuln) {
    continue;
  }

  const existing_rem_note = rem_notes[bc_comment_id];

  // build payload copy
  const payload = {
    bc_comment_id: bc_comment_id,
    bc_sub_id: current_comment.bc_sub_id,
    body: current_comment.body,
    rich_text_body: current_comment.rich_text_body,
    created_at: current_comment.created_at,
    author: current_comment.author,
    author_email: current_comment.author_email,
    vuln_id: vuln.vuln_id,
    vuln_project_id: vuln.projectId
  };

  // if no existing remediation note on bc_comment_id 
  if (!existing_rem_note){
    Array.push(create_required, payload);
    continue;
  }

  // update decision
  const existing_html = existing_rem_note.rem_note_html;
  const bc_comment_rich_text = payload.rich_text_body;

  if (!existing_html || !bc_comment_rich_text) {
    payload.rem_note_id = existing_rem_note.rem_note_id;
    Array.push(update_required, payload);
    continue;
  }

  if (!existing_rem_note.rem_note_id) {
    if (secrets.logging_level === 'debug'){
      Logger.debug('No existing_rem_note.rem_note_id found. This means this remediation note was deleted. Adding to create queue.');
    }
    Array.push(create_required, payload);
    continue;
  }

  const inner = extractCommentHtml(existing_html);
  if (!inner){
    // if inner extraction fails, treat it as an update
    if (secrets.logging_level === 'debug'){
      Logger.debug('Failed extracting Bugcrowd comment rich-text substring from existing_html: ', existing_html);
    }
    payload.rem_note_id = existing_rem_note.rem_note_id;
    Array.push(update_required, payload);
    continue;
  }

  if (inner === bc_comment_rich_text) {
    continue;
  }

  if (inner !== existing_html){
    payload.rem_note_id = existing_rem_note.rem_note_id;
    Array.push(update_required, payload);
  }

  if (secrets.logging_level === 'debug'){
    Logger.debug('Should not reach here.');
  }
}

if (Array.length(create_required) === 0 && Array.length(update_required) === 0){
  return {
    decision: 'finish',
    message: 'All comments from Bugcrowd are in sync. Exiting process.'
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Determined Create/Update, proceeding to Create step.'
  },
  data: { 
    create: create_required, 
    update: update_required 
  }
};

function extractCommentHtml(html){
  if (!html) {
    return null;
  }
  const inner = String.replaceAll(
    html,
    m/[\s\S]*said on Bugcrowd\s*<\/a>\s*<\/em>\s*<\/p>\s*<blockquote>([\s\S]*?)\s*<\/blockquote>\s*$/gi,
    '$1'
  );
  if (inner === html) {
    return null;
  }
  return inner;
}
```

**Action 6 - Create Remediation Notes**

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}/remediationNote
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (!data.create || Array.length(data.create) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No bc_comments create left to process. Continuing to next step.',
    },
    data: {
      update: data.update
    }
  };
};

let bc_comment;

if (Array.length(data.create) > 0){
  bc_comment = data.create[0];
}

if (!bc_comment.bc_comment_id){
  Logger.error('Error: Following bc_comment does not have bc_comment_id: ', JSON.stringify(bc_comment));
  return {
    decision: {
      status: 'abort',
      message: 'No bc_comment_id found to create new remediation note. Aborting the process.',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' 
  + bc_comment.vuln_id + '/remediationNote';

const author_name = bc_comment.author === 'a Crowdcontrol user' 
  ? bc_comment.author_email 
  : bc_comment.author;

const author_date_time_string = author_name 
  + ' on ' + Date.format(bc_comment.created_at, 'dddd d mmmm yyyy');

const note_body = '<p><em><a href="https://tracker.bugcrowd.com/' 
  + secrets.bugcrowd_engagement_name 
  + '/submissions/' 
  + bc_comment.bc_sub_id 
  + '#' 
  + bc_comment.bc_comment_id 
  + '" rel="noopener noreferrer" target="_blank">' 
  + author_date_time_string 
  + ' said on Bugcrowd </a> </em> </p><blockquote>' 
  + bc_comment.rich_text_body 
  + ' </blockquote>';

const projectId = bc_comment.vuln_project_id;

Array.shift(data.create);

return {
  decision: {
    status: 'continue',
    message: 'Creating comment on Vulnerability ID ' + bc_comment.vuln_id
  },
  request: {
    url: url,
    body: {
      note: note_body,
      note_type: 'RICHTEXT',
      projectId: bc_comment.vuln_project_id
    }
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  Logger.error(JSON.stringify(response));
  return {
    decision: {
      status: 'abort',
      message: 'Error while creating remediation note on AttackForge.'
    }
  };
}

if (data && !data.error_collection){
  data.error_collection = {
    create: [],
    update: []
  };
}

if (response.jsonBody?.status === 'error' && response.jsonBody.error){
  if (secrets.logging_level === 'debug'){
    Logger.debug(response?.jsonBody?.error);
  }
  Array.push(data.error_collection.create, response.jsonBody.error);
}

if (data?.create && Array.length(data.create) === 0) {
  if (data.update && Array.length(data.update) > 0){
    return {
      decision: {
        status: 'continue',
        message: 'No more create to repeat. Proceeding to update.'
      },
      data: {
        update: data.update
      }
    };
  } 
  else {
    return {
      decision: {
        status: 'finish',
        message: 'No more create or update to repeat. Exiting the process.'
      }
    };
  }
}

if (data?.create && Array.length(data.create) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Processing next create request on queue.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'abort',
    message: 'Error: should never get here'
  }
};
```

**Action 7 - Update Remediation Notes**

* **Method**: PUT
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{vulnerabilityId}/remediationNote/{remediationNoteId}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (!data.update || Array.length(data.update) === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No update request, finishing the process'
    }
  };
}

if (!(data.update?[0].vuln_id && data.update?[0].rem_note_id)) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Update data does not contain necessary information to process. Requires af_vuln_id and rem_note_id'
    }
  };
}

const current_update = data.update[0];
if (!current_update){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data.update: ', data.update);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed retrieving first element from data.update array.'
    }
  };
}

const vulnerabilityId = current_update.vuln_id;
const remediationNoteId = current_update.rem_note_id;

const author_name = current_update?.author === 'a Crowdcontrol user' 
  ? current_update?.author_email 
  : current_update.author;

const author_date_time_string = author_name 
  + ' on ' + Date.format(current_update.created_at, 'dddd d mmmm yyyy');

const url = 'https://andy.attackforge.dev/api/ss/vulnerability/' 
  + vulnerabilityId 
  + '/remediationNote/'
  + remediationNoteId;

let note_body = '<p><em><a href="https://tracker.bugcrowd.com/' + 
  + secrets.bugcrowd_engagement_name
  + '/submissions/'
  + current_update.bc_sub_id 
  + '#' 
  + current_update.bc_comment_id;

note_body = note_body 
  + '" rel="noopener noreferrer" target="_blank">' 
  + author_date_time_string 
  + ' said on Bugcrowd </a> </em> </p> <blockquote>'
  + current_update.rich_text_body 
  + ' </blockquote>';

Array.shift(data.update);

return {
  decision: {
    status: 'continue',
    message: 'Sending update remediation note request.',
  },
  request: {
    url: url,
    body: {
      note: note_body,
      note_type: 'RICHTEXT'
    }
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while updating remediation note, statusCode: ' + response.statusCode 
    }
  };
}

if (data && !data.error_collection){
  data.error_collection = {};
  if (!data.error_collection.update){
    data.error_collection.update = [];
  }
}

if (response.jsonBody?.status === "error" && response.jsonBody?.error) {
  Logger.info('There was an error while updating Remediation Note. Please refer to the data.error_collection.');
  Array.push(data.error_collection.update, response.jsonBody?.error);
}

if (data?.update && Array.length(data.update) === 0){
  return {
    decision: {
      status: 'finish',
      message: 'Update queue empty, finishing the process.'
    },
    data: {
      error_collection: data.error_collection
    }
  };
}

if (data?.update && Array.length(data.update) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Processing next update request in queue.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'abort',
    message: 'Error: should never get here'
  }
};
```


# Tenable

<figure><img src="/files/7IUWEjfZM7AgOGM72yXu" alt=""><figcaption></figcaption></figure>

## Tenable WAS - Initiate Project Scope Scan \[Step 1 of 5]

<figure><img src="/files/m78H7BW5ByfOFP9HBLM3" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a Web Application scan of the assets on the project scope in Tenable when a user clicks on an [Action](https://support.attackforge.com/attackforge-enterprise/actions) in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Action**:&#x20;
  * Entities: Projects and Project
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * was\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable WAS - Launch Scan \[Step 2 of 5\] Flow](#tenable-was-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Determine Action Entity**&#x20;

* **Script**:

```javascript
if (data?.project){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on project'
    },
    data: {
      project: data.project
    }
  };
}
else if (data?.projects){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on projects'
    },
    data: {
      projects: data.projects
    }
  };
}
else {
  return {
    decision:{
      status: 'abort',
      message: 'Flow called on an unsupported entity'
    }
  };
}
```

**Action 2 - Get Project(s)**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/project/{{project\_id}}
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.project) {
  return {
    decision:{
      status: 'next',
      message: 'Only one project, skipping to next action.'
    },
    data: {
      project: data.project
    }
  };
}
else if (data.projects){
  const projects = data.projects;

  // Initialise counter
  if (!data.counter){
    data.counter = 0;
  }

  if (Array.isArray(projects) 
    && Array.length(projects) > 0 
    && projects?[data.counter]?.project_id
  ){
    const project_id = projects[data.counter].project_id;
    return {
      decision: {
        status: 'continue',
        message: 'Fetching project: ' + project_id
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/project/' + project_id
      },
      data: {
        projects: projects,
        counter: data.counter
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('projects: ', JSON.stringify(projects));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error while validating "projects" object. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.projects and/or data.project'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (!data?.projects && !data?.project) {
  return {
    decision:{
      status: 'abort',
      message: 'data.projects and/or data.project missing'
    }
  };
}
if (data?.counter === undefined) {
  return {
    decision:{
      status: 'abort',
      message: 'data.counter missing'
    }
  };
}

const project = response.jsonBody?.project;

if (!project) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Project not found');
    Logger.debug('jsonBody: ', JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No projects matched with ' + data.projects[data.counter].project_id
    }
  };
}

data.projects[data.counter]?.assets = [];
if (project.project_scope){
  data.projects[data.counter].assets = project.project_scope;
}

if (Array.length(data.projects) > (data.counter + 1)) {
  data.counter = data.counter + 1;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next project.'
    },
    data: {
      projects: data.projects,
      counter: data.counter
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Fetched project scope for all projects. ', 'Projects: ', JSON.stringify(data.projects));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Fetched project scope for all projects, continuing to next step.'
    },
    data:  {
      projects: data.projects
    }
  };
}
```

**Action 3 - Trigger WAS Scan Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let body;
let project_id;
let project_scope_assets;

// Project
if (data?.project?.project_id && data.project.project_scope) {
  project_scope_assets = data.project.project_scope;
  project_id = data.project.project_id;
}
// Projects
else if (data?.projects && Array.length(data.projects) > 0){
  const curr_project = data.projects[0];
  if (curr_project.assets && curr_project.project_id) {
    project_scope_assets = curr_project.assets;
    project_id = curr_project.project_id;
  }
}

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'project_id is missing'
    }
  };
}
if (!project_scope_assets) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is missing'
    }
  };
}

const valid_project_scope_assets = [];
if (Array.isArray(project_scope_assets)) {
  for (let x = 0; x < project_scope_assets.length; x++) {
    let asset = project_scope_assets[x];
    if (String.startsWith(asset, 'http://') || String.startsWith(asset, 'https://')) {
      Array.push(valid_project_scope_assets, asset);
    }
    else if (isValidHostname(asset)) {
      asset = 'https://' + asset;
      Array.push(valid_project_scope_assets, asset);
    }
  }
}
project_scope_assets = valid_project_scope_assets;

if (Array.isArray(project_scope_assets) && project_scope_assets.length === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is empty'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Project id and asset/scope sent to was_scan_flow: ' + secrets.was_scan_flow_trigger_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.was_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      assets: project_scope_assets 
    }
  }
};

function isValidHostname(str) {
  if (str.length === 0 || str.length > 253) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length === 0 || label.length > 63) return false;
      if (label[0] === '-' || label[label.length - 1] === '-') return false;
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) return false;
    }
  }
  return true;
}

function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length > 1 && label[0] === '0') return false;
      const num = Number.parseInt(label);
      return num >= 0 && num <= 255;
    }
  }
}

function isValidCIDR(str) {
  const match = String.match(str, m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/);
  if (!match) return false;
  const octets = [+match[1], +match[2], +match[3], +match[4]];
  const prefix = +match[5];
  let isValid = true;
  if (octets && Array.isArray(octets) && Array.length(octets) > 0) {
    for (let x = 0; x < octets.length; x++) {
      const o = octets[x];
      if (!(o >= 0 && o <= 255)) {
        isValid = false;
      }
    }
  }
  if (!(prefix >= 0 && prefix <= 32)) {
    isValid = false;
  }
  return isValid;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submiting project to was_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submiting project to was_scan_flow. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}
if (data?.projects) {
  Array.shift(data.projects);
  if (Array.length(data.projects) > 0) {
    return {
      decision:{
        status: 'repeat',
        message: 'Sending next request...',
        delay: 1000
      },
      data: {
        projects: data.projects
      }
    };
  }
  else {
    return {
      decision: {
        status: 'finish',
        message: 'Completed sending all projects to WAS Scan Flow.',
      }
    };
  }
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed sending project to WAS Scan Flow.',
    }
  };
}
```

## Tenable VM - Initiate Project Scope Scan \[Step 1 of 5]

<figure><img src="/files/sX6Hi8tFFeF3Nf4eQe8J" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a VM scan of the assets on the project scope in Tenable when a user clicks on an [Action](https://support.attackforge.com/attackforge-enterprise/actions) in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Action**:&#x20;
  * Entities: Projects and Project
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * vm\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM - Launch Scan \[Step 2 of 5\] Flow](#tenable-vm-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Determine Action Entity**

* **Script**:

```javascript
if (data?.project){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on project'
    },
    data: {
      project: data.project
    }
  };
}
else if (data?.projects){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on projects'
    },
    data: {
      projects: data.projects
    }
  };
}
else {
  return {
    decision:{
      status: 'abort',
      message: 'Flow called on an unsupported entity'
    }
  };
}
```

**Action 2 - Get Project(s)**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/project/{{project\_id}}
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.project) {
  return {
    decision:{
      status: 'next',
      message: 'Only one project, skipping to next action.'
    },
    data: {
      project: data.project
    }
  };
}
else if (data.projects){
  const projects = data.projects;

  // Initialise counter
  if (!data.counter){
    data.counter = 0;
  }

  if (Array.isArray(projects) 
    && Array.length(projects) > 0 
    && projects?[data.counter]?.project_id
  ){
    const project_id = projects[data.counter].project_id;
    return {
      decision: {
        status: 'continue',
        message: 'Fetching project: ' + project_id
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/project/' + project_id
      },
      data: {
        projects: projects,
        counter: data.counter
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('projects: ', JSON.stringify(projects));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error while validating "projects" object. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.projects and/or data.project'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (!data?.projects && !data?.project) {
  return {
    decision:{
      status: 'abort',
      message: 'data.projects and/or data.project missing'
    }
  };
}
if (data?.counter === undefined) {
  return {
    decision:{
      status: 'abort',
      message: 'data.counter missing'
    }
  };
}

const project = response.jsonBody?.project;

if (!project) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Project not found');
    Logger.debug('jsonBody: ', JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No projects matched with ' + data.projects[data.counter].project_id
    }
  };
}

data.projects[data.counter].assets = [];
if (project.project_scope){
  data.projects[data.counter].assets = project.project_scope;
}

if (Array.length(data.projects) > (data.counter + 1)) {
  data.counter = data.counter + 1;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next project.'
    },
    data: {
      projects: data.projects,
      counter: data.counter
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Fetched project scope for all projects. ', 'Projects: ', JSON.stringify(data.projects));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Fetched project scope for all projects, continuing to next step.'
    },
    data:  {
      projects: data.projects
    }
  };
}
```

**Action 3 - Trigger VM Scan Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let body;
let project_id;
let project_scope_assets;

// Project
if (data?.project?.project_id && data.project.project_scope) {
  project_scope_assets = data.project.project_scope;
  project_id = data.project.project_id;
}
// Projects
else if (data?.projects && Array.length(data.projects) > 0){
  const curr_project = data.projects[0];
  if (curr_project.assets && curr_project.project_id) {
    project_scope_assets = curr_project.assets;
    project_id = curr_project.project_id;
  }
}

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'project_id is missing'
    }
  };
}
if (!project_scope_assets) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is missing'
    }
  };
}

const valid_project_scope_assets = [];
if (Array.isArray(project_scope_assets)) {
  for (let x = 0; x < project_scope_assets.length; x++) {
    const asset = project_scope_assets[x];
    if (isValidCIDR(asset) || isValidIPv4(asset) || isValidHostname(asset)) {
      Array.push(valid_project_scope_assets, asset);
    }
  }
}
project_scope_assets = valid_project_scope_assets;

if (Array.isArray(project_scope_assets) && project_scope_assets.length === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is empty'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Project id and asset/scope sent to vm_scan_flow: ' + secrets.vm_scan_flow_trigger_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.vm_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      assets: project_scope_assets 
    }
  }
};

function isValidHostname(str) {
  if (str.length === 0 || str.length > 253) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length === 0 || label.length > 63) return false;
      if (label[0] === '-' || label[label.length - 1] === '-') return false;
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) return false;
    }
  }
  return true;
}

function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length > 1 && label[0] === '0') return false;
      const num = Number.parseInt(label);
      return num >= 0 && num <= 255;
    }
  }
}

function isValidCIDR(str) {
  const match = String.match(str, m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/);
  if (!match) return false;
  const octets = [+match[1], +match[2], +match[3], +match[4]];
  const prefix = +match[5];
  let isValid = true;
  if (octets && Array.isArray(octets) && Array.length(octets) > 0) {
    for (let x = 0; x < octets.length; x++) {
      const o = octets[x];
      if (!(o >= 0 && o <= 255)) {
        isValid = false;
      }
    }
  }
  if (!(prefix >= 0 && prefix <= 32)) {
    isValid = false;
  }
  return isValid;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submiting project to was_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submiting project to was_scan_flow. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}
if (data?.projects) {
  Array.shift(data.projects);
  if (Array.length(data.projects) > 0) {
    return {
      decision:{
        status: 'repeat',
        message: 'Sending next request...',
        delay: 1000
      },
      data: {
        projects: data.projects
      }
    };
  }
  else {
    return {
      decision: {
        status: 'finish',
        message: 'Completed sending all projects to VM Scan Flow.',
      }
    };
  }
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed sending project to VM Scan Flow.',
    }
  };
}
```

## Tenable WAS - Launch Scan \[Step 2 of 5]

<figure><img src="/files/CmaAjHuiI0SyUU0LJRCe" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create and launch a Web Application scan in Tenable.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * owner\_id - the Tenable ID of the owner of the scanner.
  * tenable\_template\_id - the UUID of the Tenable-provided template resource.
  * user\_template\_id - the UUID of the Tenable user-defined template.
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Create WAS Scan Configuration**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
// user custom fields
const scan_name = 'WAS Scan on Flows'; 
const description = 'WAS scan created via AttackForge Flow';

// From body
const project_asset = data.jsonBody?.assets;
const project_id = data.jsonBody?.project_id;

if (!project_id) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.project_id must exist. Please check the log.'
    }
  };
}
if (!project_asset) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must exist. Please check the log.'
    }
  };
}
if (!Array.isArray(project_asset)) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must be an array. Please check the log.'
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug('scan_name: ', scan_name);
  Logger.debug('description: ', description);
  Logger.debug('project_asset: ', project_asset);
}

return {
  decision: {
    status: 'continue',
    message: 'Creating WAS configuration.',
  },
  request: {
    body: {
      name: scan_name,
      description: description,
      targets: project_asset,
      template_id: secrets.tenable_template_id,
      user_template_id: secrets.user_template_id,
      owner_id: secrets.owner_id
    }
  },
  data: {
    project_id: project_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 201 && response.statusCode !== 202){
  if (secrets.logging_level === 'debug'){
    Logger.debug('Error: ' + JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error creating WAS configuration. Please check the log.',
    }
  };
}

const response_body = response.jsonBody;
const config_id = response_body?.config_id;

if (!config_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response_body: ' + JSON.stringify(response_body));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No config_id returned. Please check the log.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Configuration created: ' + config_id,
  },
  data: {
    project_id: data?.project_id,
    config_id: config_id
  }
};
```

**Action 2 - Launch WAS Scan**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs/{config\\_id}/scans>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.config_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.config_id must be present for WAS Scan launch. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Launching WAS scan.',
  },
  request: {
    url: 'https://cloud.tenable.com/was/v2/configs/' + data.config_id + '/scans'
  },
  data: {
    project_id: data?.project_id,
    config_id: data?.config_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 201 && response.statusCode !== 202){
  if (secrets.logging_level === 'debug'){
    Logger.debug('Error: ' + JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error launching WAS scan',
    }
  };
}

const scan_id = response.jsonBody?.scan_id;

if (!scan_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.body: ' + JSON.stringify(response.body));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No scan_id returned, please check the log.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Scan launched: ' + scan_id,
  },
  data: {
    project_id: data?.project_id,
    config_id: data?.config_id,
    scan_id: scan_id
  }
};
```

**Action 3 - Update Project with Scan Details**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!data?.project_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: project_id is missing'
    }
  };
}
if (!data?.scan_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: scan_id is required in order to update tenable_was_active_scan custom_field. Please check the log'
    }
  };
}
if (!data.config_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: config_id is required in order to update tenable_was_active_scan custom_field. Please check the log'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating project with "tenable_was_active_scan" and "tenable_was_config_id" custom_fields.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_was_active_scan",
          value: String.from(data.scan_id)
        },
        {
          key: "tenable_was_config_id",
          value: String.from(data.config_id)
        }
      ]
    }
  },
  data: {
    project_id: data?.project_id,
    config_id: data?.config_id,
    scan_id: data?.scan_id
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error while updating project with Tenable scan custom fields. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated project: ' + data?.project_id + ' with "tenable_was_active_scan" custom field: '+ data?.scan_id +' and "tenable_was_config_id custom field: "' + data?.config_id,
  }
};
```

## Tenable VM - Launch Scan \[Step 2 of 5]

<figure><img src="/files/2IK8a9eXiRcua3GnDZSG" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create and launch a VM scan in Tenable.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * scan\_name\_prefix - the prefix for the scan name in Tenable e.g. AttackForge Project Scan
  * vm\_network\_scan\_template\_uuid - the UUID for the Tenable VM scan template
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Create VM Scan Configuration**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/scans>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
const curr_time = Date.datetime('now');
// Users can customise scan name by configuring scan name pattern in secret or as a variable
const scan_name = secrets.scan_name_prefix + curr_time;
// Users can set description of scan here.
const description = 'Example description';

// From body
const project_asset = data.jsonBody?.assets;
const project_id = data.jsonBody?.project_id;

if (!project_id) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.project_id must exist. Please check the log.'
    }
  };
}
if (!project_asset) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must exist. Please check the log.'
    }
  };
}
if (!Array.isArray(project_asset)) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must be an array. Please check the log.'
    }
  };
}

// multiple targets - simply separate it with comma in target "target1, target2"
let target_list = '';
for (let x = 0; x < project_asset.length; x++) {
  const asset = project_asset[x];
  if (x === project_asset.length - 1) {
    target_list = target_list + asset;
  }
  else {
    target_list = target_list + asset + ',';
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Sending a request for create VM scan.'
  },
  request: {
    body: {
      settings: {
        name: scan_name,
        description: description,
        text_targets: target_list
      },
      uuid: secrets.vm_network_scan_template_uuid
    },
  },
  data: {
    project_id: project_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  } 
  return {
    decision: {
      status: 'abort',
      message: 'Error',
    }
  };
}

if (!response.jsonBody?.scan?.id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody: ', JSON.stringify(response.jsonBody));
  } 
  return {
    decision:{
      status: 'abort',
      message: 'Error: No id found from response.jsonBody.scan'
    }
  };
}

const scan_id = response.jsonBody.scan.id;

return {
  decision: {
    status: 'continue',
    message: 'Scan id created in Tenable',
  },
  data: {
    project_id: data?.project_id,
    scan_id: scan_id
  }
};
```

**Action 2 - Launch VM Scan**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/scans/{scan\\_id}/launch>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.scan_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.scan_id is required for launching Tenable VM Scan. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Launching Tenable VM Scan on scan_id:' + data.scan_id,
  },
  request: {
    url: 'https://cloud.tenable.com/scans/' + data.scan_id + '/launch'
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while Launching the VM Scan, please check the log.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully launched VM Scan.',
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id
  }
};
```

**Action 3 - Update Project with Scan Details**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!data?.scan_id){
  return {
    decision:{
      status: 'abort',
      message: 'Error: scan_id is required.'
    }
  };
}
if (!data?.project_id){
  return {
    decision:{
      status: 'abort',
      message: 'Error: project_id is required.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating "tenable_vm_active_scan" on project ' + data.project_id + '.',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_vm_active_scan",
          value: String.from(data.scan_id)
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error while updating project with "tenable_vm_active_scan" custom_field'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully completed updating project with "tenable_vm_active_scan" custom_field'
  }
};
```

## Tenable VM & WAS - Find Active Scans to Poll Status \[Step 3 of 5]

<figure><img src="/files/qBPeMNHtk8L913nDohNF" alt=""><figcaption></figcaption></figure>

The purpose of this example is to poll the scan status for any active Tenable scans, and for any completed scans - trigger a download of the vulnerabilities.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Schedule**:&#x20;
  * Every hour
    * CRON String: 0 0/1 \* \* \*
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * vm\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM - Poll Scan Status \[Step 4 of 5\] Flow](#tenable-vm-poll-scan-status-step-4-of-5)
  * was\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable WAS - Poll Scan Status \[Step 4 of 5\] Flow](#tenable-was-poll-scan-status-step-4-of-5)
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Tenable VM & WAS Scan Projects**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/projects?order=created:desc\&limit=500
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Getting all projects with tenable active scan ids.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/projects?order=created:desc&limit=500'
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  return {
    decision: { 
      status: 'abort', 
      message: 'Error fetching projects.'
    }
  };
}

const projects = response?.jsonBody?.projects;

if (!projects || projects && Array.length(projects) === 0) {
  return {
    decision: { 
      status: 'abort', 
      message: 'No projects found.'
    }
  };
}

const vm_projects = [];
const was_projects = [];

for (let i = 0; i < Array.length(projects); i++) {
  const project = projects[i];
  const cfs = project.project_custom_fields;

  if (!cfs || Array.length(cfs) === 0) {
    continue;
  }

  const vm_scan_id = Array.find(cfs, find_vm_scan);
  const was_scan_id = Array.find(cfs, find_was_scan);
  const was_cf_config = Array.find(cfs, find_was_config);

  if (vm_scan_id) {
    Array.push(vm_projects, {
      project_id: project.project_id,
      scan_id: vm_scan_id.value
    });
  }

  if (was_scan_id && was_cf_config) {
    Array.push(was_projects, {
      project_id: project.project_id,
      scan_id: was_scan_id.value,
      config_id: was_cf_config.value
    });
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully fetched ' + Array.length(vm_projects) + ' VM Projects, and ' + Array.length(was_projects) + ' WAS Projects.'
  },
  data: {
    vm_projects: vm_projects,
    was_projects: was_projects
  }
};

function find_vm_scan(field){
  return field.key === 'tenable_vm_active_scan';
}

function find_was_scan(field){
  return field.key === 'tenable_was_active_scan';
}

function find_was_config(field){
  return field.key === 'tenable_was_config_id';
}
```

**Action 2 - Tenable VM - Get Scan Status + Trigger Download**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{vm\_poll\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.vm_projects && Array.length(data.vm_projects) > 0) {
  return {
    decision: {
      status: 'continue',
      message: 'Requesting latest scan status and triggering download for: ' + JSON.stringify(data.vm_projects[0])
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.vm_poll_flow_trigger_id,
      body: {
        project: data.vm_projects[0]
      }
    },
    data: {
      vm_projects: data?.vm_projects,
      was_projects: data?.was_projects
    }
  };
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'No active VM scan ids found, proceeding to next action.'
    }
  };
}
```

* **Response Script**:

```javascript
if (!response?.statusCode === 202) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error while requesting latest scan status update. Please check the log.'
    }
  };
}
if (!data?.vm_projects) {
  return{
    decision:{
      status: 'abort',
      message: 'data.vm_projects missing'
    }
  };
}

Array.shift(data.vm_projects);

if (Array.length(data.vm_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Successfully requested latest scan status update. Repeating the action',
    },
    data: {
      vm_projects: data?.vm_projects,
      was_projects: data?.was_projects
    }
  };  
}
else {
  return {
    decision: {
      status: 'continue',
      message: 'Successfully requested latest scan status update. Continuing to next action.',
    },
    data: {
      vm_projects: data?.vm_projects,
      was_projects: data?.was_projects
    }
  };
}
```

**Action 3 - Tenable WAS - Get Scan Status + Trigger Download**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/flows/{{was\_poll\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.was_projects && Array.length(data.was_projects) > 0) {
  return {
    decision: {
      status: 'continue',
      message: 'Requesting scan update for project: ' + JSON.stringify(data.was_projects[0]),
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.was_poll_flow_trigger_id,
      body: {
        project: data.was_projects[0]
      }
    },
    data: {
      was_projects: data?.was_projects
    }
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed processing scans'
    }
  };
}
```

* **Response Script**:

```javascript
if (!response?.statusCode === 202) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while requesting latest scan status update. Please check the log.'
    }
  };
}
if (!data?.was_projects) {
  return{
    decision: {
      status: 'abort',
      message: 'data.was_projects missing'
    }
  };
}

Array.shift(data.was_projects);

if (Array.length(data.was_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Successfully requested latest scan status update. Repeating the action',
    },
    data: {
      was_projects: data.was_projects
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed processing scans'
    }
  };
}
```

## Tenable WAS - Poll Scan Status \[Step 4 of 5]

<figure><img src="/files/yxduxVsHFr6nlfuR5mzY" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export the results of a Web Application scan in Tenable.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * apikey - your AttackForge user API key
  * current\_flow\_id - the Id for this Flow in AttackForge once it's created (used for error reporting in email)
  * admin\_user\_id - the Id for the user in AttackForge who receives error emails (used for error reporting)
  * import\_vuln\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM & WAS - Import Vulnerabilities \[Step 5 of 5\] Flow](#tenable-vm-and-was-import-vulnerabilities-step-5-of-5)
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Poll Scan Status**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/was/v2/scans/{scan\\_id}>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data?.jsonBody?.project?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.project_id missing'
    }
  };
}
if (!data?.jsonBody?.project?.scan_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.scan_id missing'
    }
  };
}
if (!data?.jsonBody?.project?.config_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.config_id missing'
    }
  };
}

const project_id = data.jsonBody.project.project_id;
const scan_id = data.jsonBody.project.scan_id;
const config_id = data.jsonBody.project.config_id;

return {
  decision: {
    status: 'continue',
    message: 'Checking scan status for scan_id: ' + scan_id,
  },
  request: {
    url: 'https://cloud.tenable.com/was/v2/scans/' + scan_id
  },
  data: {
    project_id: project_id,
    scan_id: scan_id,
    config_id: config_id
  }
};
```

* **Response Script**:

```javascript
// Error
if (response.statusCode !== 200){
  if (response.jsonBody?.reasons?[0]?.code === 'NOT_FOUND') {
    if (secrets.logging_level === 'debug') {
      Logger.debug('code: ', response.jsonBody.reasons[0].code);
      Logger.debug(JSON.stringify(response));
    }
    if (!data.retry_counter) {
      data.retry_counter = 0;
    }
    data.retry_counter = data.retry_counter + 1;

    if (data.retry_counter > 3) {
      if (secrets.logging_level === 'debug'){
        Logger.debug('response: ', JSON.stringify(response));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Retry failed 3 times. Exiting the process. Please check the log.',
        }
      };
    }

    return {
      decision: {
        status: 'repeat',
        message: 'Scan not found with scan_id, retrying...',
      },
      data: {
        project_id: data?.project_id,
        scan_id: data?.scan_id,
        config_id: data?.config_id,
        retry_counter: data?.retry_counter,
        scan_type: data?.scan_type
      }
    };
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error getting scan status',
    }
  };
}

// Success
const response_body = response.jsonBody;
const status = response_body?.status;
const error_statuses = ['failed', 'aborted', 'cancelled'];

if (Array.includes(error_statuses, status)){
  return {
    decision: {
      status: 'abort',
      message: 'Scan ended with status: ' + status,
    }
  };
}

if (status === 'completed') {
  // Single-target
  if (response_body.target && !response_body?.target_scans){
    data.scan_type = 'single-target';
  }
  // Multi-target
  if (response_body?.target_scans?.completed > 0){
    data.scan_type = 'multi-target';
  }

  return {
    decision: {
      status: 'continue',
      message: 'Scan ' + (data.scan_type || 'unknown') + ' completed.',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type
    }
  };
} 
else {
  // If the scan returns incomplete and not failed - then exit the flow.
  // Next scheduled project scan flow will trigger this polling again.
  if (secrets.logging_level === 'debug'){
    Logger.debug('Scan Status: ', status);
  }
  return {
    decision:{
      status: 'finish',
      message: 'Polling completed. Scan status: ' + status + '. Exiting the flow.'
    }
  };
}
```

**Action 2 - Search Scans (multi-target only)**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs/{config\\_id}/scans/search>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.scan_type === 'multi-target') {
  return {
    decision: {
      status: 'continue',
      message: 'Searching children scan_ids with config_id',
    },
    request: {
      url: 'https://cloud.tenable.com/was/v2/configs/' + data.config_id + '/scans/search'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type
    }
  };
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'Not a multi-target scan, proceeding to exporting step.'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data.config_id: ', data.config_id);
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error searching scan with config_id: ' + data.config_id + '. Please check the log.',
    }
  };
}

const items = response?.jsonBody?.items;

if (!items){
  return {
    decision:{
      status: 'abort',
      message: 'Items not found in response body.'
    }
  };
}

const children_scan_ids = [];
const aborted_scan_ids = [];

for (let i = 0; i < Array.length(items); i++) {
  if (items[i]?.parent_id === data.scan_id && items[i].status === 'completed' && items[i].scan_id) {
    Array.push(children_scan_ids, items[i].scan_id);
  }
  if (items[i].status === 'aborted' && items[i].scan_id) {
    Array.push(aborted_scan_ids, {
      scan_id: items[i].scan_id, 
      status: "aborted"
    });
  }
}

// Warn the aborted scan ids.
// Treating aborted scan as failed but completed scan.
if (secrets.logging_level === 'debug'){
  Logger.debug('Children Scan Ids: ', children_scan_ids);
  Logger.warn('Aborted Scan Ids: ', aborted_scan_ids);
}

if (Array.length(children_scan_ids) > 0){
  return {
    decision: {
      status: 'continue',
      message: 'Continue to next action',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type,
      children_scan_ids: children_scan_ids,
      aborted_scan_ids: aborted_scan_ids
    }
  };
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'No children found.'
    }
  };
}
```

**Action 3 - Export Scan Report**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/was/v2/scans/{scan\\_id}/report>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
// Initialise
if (!data.final_result){
  data.final_result = [];
}
let current_scan;

// multi-target
if (data?.children_scan_ids 
  && Array.isArray(data.children_scan_ids) 
  && Array.length(data.children_scan_ids) > 0
) {
  current_scan = data.children_scan_ids[0];
}
else {
  // single-target
  current_scan = data.scan_id;
}

if (!current_scan){
  if (secrets.logging_level === 'debug'){
    if (data?.children_scan_ids){
      Logger.debug('data.children_scan_ids: ', JSON.stringify(data.children_scan_ids));
    } 
    else {
      Logger.debug('data.scan_id: ', data.scan_id);
    }
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: current_scan must be present. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Exporting scan report.'
  },
  request: {
    url: 'https://cloud.tenable.com/was/v2/scans/' + current_scan + '/report'
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  return {
    decision: {
      status: 'abort',
      message: 'Error exporting scan report',
    }
  };
}

// Single-target will also be recorded under final_result
if (data?.final_result) {
  Array.push(data.final_result, response.jsonBody);
}

if (data?.children_scan_ids){
  Array.shift(data.children_scan_ids);
  if (Array.length(data.children_scan_ids) > 0){
    return {
      decision: {
        status: 'repeat',
        message: 'Repeating process on next children scan.',
      },
      data: {
        project_id: data?.project_id,
        scan_id: data?.scan_id,
        config_id: data?.config_id,
        retry_counter: data?.retry_counter,
        scan_type: data?.scan_type,
        children_scan_ids: data?.children_scan_ids,
        aborted_scan_ids: data?.aborted_scan_ids,
        final_result: data?.final_result
      }
    };
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Scan report exported successfully.',
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

**Action 4 - Import Scan Report Result**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{import\_vuln\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.project_id) {
  return {
    decision: {
      status: 'continue',
      message: 'Import scan results'
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.import_vuln_flow_trigger_id,
      body: {
        final_result: data.final_result,
        project_id: data.project_id
      },
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type,
      children_scan_ids: data?.children_scan_ids,
      aborted_scan_ids: data?.aborted_scan_ids,
      final_result: data?.final_result
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id must be present.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode === 202){
  return {
    decision: {
      status: 'continue',
      message: 'Successfully called Importing Scan flow. Proceeding to next action.',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type,
      children_scan_ids: data?.children_scan_ids,
      aborted_scan_ids: data?.aborted_scan_ids,
      final_result: data?.final_result
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error while calling Import Scan flow. Please check the log.'
    }
  };
}
```

**Action 5 - Update Project to Remove Table Scan Details**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = apikey
* **Request Script**:

```javascript
if (!data?.project_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: data.project_id must exist to update project. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Removing tenable_active_scan from project: ' + data.project_id
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_was_active_scan",
          value: null
        },
        {
          key: "tenable_was_config_id",
          value: null
        }
      ]
    }
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to update project: ' + data?.project_id + '. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully removed tenable_was_active_scan from project: ' + data?.project_id
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

**Action 6 - Send Email Report on Aborted Scan**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/email
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = apikey
* **Request Script**:

```javascript
if (data?.aborted_scan_ids 
  && Array.isArray(data.aborted_scan_ids) 
  && Array.length(data.aborted_scan_ids) > 0
) {
  let aborted_scan_id_table = "";
  for (let i = 0; i < Array.length(data.aborted_scan_ids); i++) {
    if (data.aborted_scan_ids[i]?.scan_id && data.aborted_scan_ids[i]?.status) {
      aborted_scan_id_table = aborted_scan_id_table + '<tr><td>' + data.aborted_scan_ids[i].scan_id + '</td><td>' + data.aborted_scan_ids[i].status + '</td></tr>';
    }
  }

  const curr_year = Date.format(Date.datetime('now'), 'yyyy');
  const emailHeader = "<!doctype html> <html lang='en' xmlns:v='urn:schemas-microsoft-com:vml' style='color-scheme: light dark'> <head> <meta charset='utf-8'> <meta name='x-apple-disable-message-reformatting'> <meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='format-detection' content='telephone=no, date=no, address=no, email=no, url=no'> <meta name='color-scheme' content='light dark'> <meta name='supported-color-schemes' content='light dark'> <link rel='preconnect' href='https://fonts.googleapis.com'> <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin> <link href='https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;700&display=swap' rel='stylesheet' media='screen'> <style> .body-sub { margin-top: 25px; border-top-width: 1px; padding-top: 25px; border-top-color: #eaeaec; border-top-style: solid; } body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } .sm-w-full { width: 100% !important; } /*@media (prefers-color-scheme: dark) { body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } } :root { color-scheme: light dark; }*/ /*@media (max-width: 600px) { .sm-w-full { width: 100% !important; } }*/ h1 { margin-top: 0; text-align: left; font-size: 24px; font-weight: 700; color: #333333 } p { margin-bottom: 5px; margin-top: 6px; font-size: 16px; line-height: 24px; color: #51545e } .styled-table { width: 600px; border-collapse: collapse; } .styled-table th { border-collapse: collapse; border: 1px solid white; } .styled-table td { border-collapse: collapse; border: 1px solid white; padding-left: 20px; } .styled-button { border: none; border-collapse: collapse; } .styled-button td { border: 1px solid; border-radius: 5px; border-color: transparent; background-color: #469cf0 !important; padding: 15px 30px; } .styled-button a { background-color: #469cf0 !important; display: inline-block; font-size: 17px; color: #ffffff; text-decoration: none; font-family: sans-serif; } </style> <!--[if mso]> <style> .styled-button td { border: 1px solid; border-radius: 5px; border-color: #469cf0; background-color: #469cf0 !important; mso-padding-alt: 15px 30px; } </style> <![endif]--> </head> <body style='margin: 0; width: 100%; background-color: #f2f4f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word'> <div role='article' aria-roledescription='email' aria-label lang='en'> <table class='email-wrapper' style='width: 100%; background-color: #1e293b; font-family: &quot;Inter&quot;, ui-sans-serif, system-ui, -apple-system, &quot;Segoe UI&quot;, sans-serif;' cellpadding='0' cellspacing='0' role='none' > <tr> <td align='center'> <table class='email-content' style='width: 100%' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' class='email-masthead' style='display: flex; justify-content: center; gap: 8px; padding-top: 25px; padding-bottom: 25px; text-align: center; font-size: 16px'> <img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAABnCAYAAADc1TyNAAAEDmlDQ1BrQ0dDb2xvclNwYWNlR2VuZXJpY1JHQgAAOI2NVV1oHFUUPpu5syskzoPUpqaSDv41lLRsUtGE2uj+ZbNt3CyTbLRBkMns3Z1pJjPj/KRpKT4UQRDBqOCT4P9bwSchaqvtiy2itFCiBIMo+ND6R6HSFwnruTOzu5O4a73L3PnmnO9+595z7t4LkLgsW5beJQIsGq4t5dPis8fmxMQ6dMF90A190C0rjpUqlSYBG+PCv9rt7yDG3tf2t/f/Z+uuUEcBiN2F2Kw4yiLiZQD+FcWyXYAEQfvICddi+AnEO2ycIOISw7UAVxieD/Cyz5mRMohfRSwoqoz+xNuIB+cj9loEB3Pw2448NaitKSLLRck2q5pOI9O9g/t/tkXda8Tbg0+PszB9FN8DuPaXKnKW4YcQn1Xk3HSIry5ps8UQ/2W5aQnxIwBdu7yFcgrxPsRjVXu8HOh0qao30cArp9SZZxDfg3h1wTzKxu5E/LUxX5wKdX5SnAzmDx4A4OIqLbB69yMesE1pKojLjVdoNsfyiPi45hZmAn3uLWdpOtfQOaVmikEs7ovj8hFWpz7EV6mel0L9Xy23FMYlPYZenAx0yDB1/PX6dledmQjikjkXCxqMJS9WtfFCyH9XtSekEF+2dH+P4tzITduTygGfv58a5VCTH5PtXD7EFZiNyUDBhHnsFTBgE0SQIA9pfFtgo6cKGuhooeilaKH41eDs38Ip+f4At1Rq/sjr6NEwQqb/I/DQqsLvaFUjvAx+eWirddAJZnAj1DFJL0mSg/gcIpPkMBkhoyCSJ8lTZIxk0TpKDjXHliJzZPO50dR5ASNSnzeLvIvod0HG/mdkmOC0z8VKnzcQ2M/Yz2vKldduXjp9bleLu0ZWn7vWc+l0JGcaai10yNrUnXLP/8Jf59ewX+c3Wgz+B34Df+vbVrc16zTMVgp9um9bxEfzPU5kPqUtVWxhs6OiWTVW+gIfywB9uXi7CGcGW/zk98k/kmvJ95IfJn/j3uQ+4c5zn3Kfcd+AyF3gLnJfcl9xH3OfR2rUee80a+6vo7EK5mmXUdyfQlrYLTwoZIU9wsPCZEtP6BWGhAlhL3p2N6sTjRdduwbHsG9kq32sgBepc+xurLPW4T9URpYGJ3ym4+8zA05u44QjST8ZIoVtu3qE7fWmdn5LPdqvgcZz8Ww8BWJ8X3w0PhQ/wnCDGd+LvlHs8dRy6bLLDuKMaZ20tZrqisPJ5ONiCq8yKhYM5cCgKOu66Lsc0aYOtZdo5QCwezI4wm9J/v0X23mlZXOfBjj8Jzv3WrY5D+CsA9D7aMs2gGfjve8ArD6mePZSeCfEYt8CONWDw8FXTxrPqx/r9Vt4biXeANh8vV7/+/16ffMD1N8AuKD/A/8leAvFY9bLAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAJYoAMABAAAAAEAAABnAAAAAOLtLv4AADQ/SURBVHgB7Z0HvBXF9cfvAykC9oqiYgNjj738Y9eoqAkW1GgUC2JDjGKsMcbYS2woYAmKLfZeExVr7CViF0GsKFZEEIX7//6e78pl3+zu7N699Z3z+fze3p05c+bMb2d3z8zO7svlaljy+XwXsE4SF9GfH6yWpIzpGgPGgDFgDBgDxoAx0CYYIEjaBjwPbkrSYPR7ga/AhaBHkrKmawwYA8aAMWAMGAPGQEMyQFC0CrgRzASSK5M0FP2eYIoKIh+Bw0HXJDZM1xgwBowBY8AYMAaMgYZggCBoUXAW+BYUy7AkDaTgYuDLYgP8fhH0TWLHdI0BY8AYMAaMAWPAGKhbBgh8OoODwHjgkrOSNA4DWoP1icsQaXeCtZLYM11jwBgwBowBY8AYMAbqigGCnW3B0yBKjknSKAxpYfy7EQa/J0/rs5ZIYtd0jQFjwBgwBowBY8AYqGkGCG5WAzcBH9kvSWMw2AS0OD5OPkbhCGDrs5IQbLrGgDFgDBgDxoAxUFsMEMxondU5YDLwlT5JW4Hh+3yNo/cS2DFpHaZvDBgDxoAxYAwYA8ZAVRkggNE6q0PA+yCJzEB5zaTOU+ayJJW06N7Fdu2kdZm+MWAMGAPGgDFgDBgDFWeAoKUPeLYliEm6+ZoCiyd1mjLHJ62oRX8q26FgyaR1mr4xYAwYA8aAMWAMGANlZ4AgZXVwCyhF3qJwp6TOUqZfKZVSVm8hDgHdktZt+saAMWAMGAPGgDFgDGTOAEFJd3Au+A6UKvencZBK1wB6vFiqvIyBndL4YGWMAWPAGDAGjAFjwBgomQECkTnBoWACyErOSeMYlS8AJmblBHbuAeum8cXKGAPGgDFgDBgDxoAxkIoBgo/twXMga9kjjUM4oU81PJWxM9OwdwlYKo1PVsYYMAaMAWPAGDAGjAEvBgg2fg1uBeWQ6RhdxcsRhxJltVi9HKL1WUeBuRzVWpIxYAwYA8aAMWAMGAPpGCC40P/7+wfIYp0VZpwyltQu6TzM5Si7p9NqdomvYGqXtP5ZOWPAGDAGjAFjwBgwBpoZIKDQv6E5DHwAyi03lEI7zvUG+uxCueVeKlivFF+trDFgDBgDxoAxYAy0UQYIInYAPv+CJquA5qBSqMaJDkBvAVZCtD5rGOhZis9W1hgwBowBY8AYMAbaCAMEDfrkwe2gkvIDla1UKsXYuKCSTlPXp+BoYOuzSj14Vt4YMAaMAWPAGGhEBggSFgfngymg0vICFc5RKq/Y2LrSjrfU9ypbfey0qdQ2WHljwBgwBowBY8AYaAAGCAq0zupw8CGolpyaBZU4Py+oZjvup/71s2iL2TAGjAFjwBgwBoyBOmWAYOD3QLNH1ZSfqHydrCjE1shqNoa69bhzBFg6qzaZHWPAGDAGjAFjwBioAwa4+a8J7gC1IC/iRIesaMPWtrXQKHzQl+WPAfNk1TazYwwYA8aAMWAMGAM1ygA3/APB96BW5JgsqaJRXcE7tdI4/HgGWJCV5UE2W8aAMWAMGAPGQI0x0A5/VgNz1ohf3+HHjVn60tTUNCVrmyX615vytcJ3iU2x4saAMWAMGAPGgDHgYkAB1o+ujCql3UdA9F4Z6r4am1PLYDeNyZ8olE9T0MoYA8aAMWAMGAPGQH0woACrVkRBx6XlcIag7U3s3lcO22bTGDAGjAFjwBgwBoyBIAMlf2sqaLCE/WcoO7qE8nFFL0ahL7BvU8UxZfnGQJ0xwLrGbrjcFejxuwaOGrBp1vo7BlhaemDShhigP+g6z6FvmtmGml1zTeU4KMboDAqTOXpiNo3j0iae4tRSgHUBpOvxWbnkUQw/AX5Trgrakl1OHL3puYhnm3/i2H7qqetUa7lgqr5a6rNOX8uYqJvFp6XeNOByYex09PTzc+r7wVO3Imr4Px8Vae3oumB1oE+gqG/oPyh0AoUAazq/v0H/Y7avgafBU7RHM9qpBXsLUlg3jTj5lrq+jVNy5VOH/tH9/K68kLT2pH/g6hvYUrCxKJBONWQ6fn1Wjopp20LYXRGsCnqBJYD6h/hrT/40tgqwJ4JxQP3gVfAuPql/lE2oW0H/vGWoYAY2p6TtW2Xwp9kk7dV1RcdhDaD/xLIU0LnSfCzYSsT51+h+yPYN8AJ4nra8zzYzwb5eJKvmf1SZTJu+acKRC3FkUGYtS2foFYqth0M6GcomtHV7jN9Ztgr8DH+B2kq0VSd83QpcHo3zx3s2YCp6m9FmXdxSCfXphqYb5DKpDDRGoUk0Yx141DaVwKNuQE8C3wv/cOr7c6rKMiyE3woItwC7gk2B2pFG1BefA9eDW2jb50mN4Mu/KLOtR7nTsH+Gh95sKtjXjeEOsNZsGeE7GnTcDPajvlZrarGnWb1nQE9QDXkav7bKqmLaoxu5+O8L1gEKHpOIBgxvgf8A8Sb/Mp9Rwc8DsX0WyFo0EaGg8RMwBjwGHqENE9hWVGijglkdi52BPqq9CEgq31JA56SOxe2049OkBoL6+HUiaUOC6RXc/wftOCmHIxeCasvulWg4jZwDPFblxk6i/jSdsBIUedWB//OA8Ql5HOplPESJujqDsQnrbDR19R2N2FML5U9KSMrn6HdPXWGJBam7I+gPyvEB5AnYPQEskMRN9O8EPnJSErvSxagGvVf6GC/SeYnfof2CPP13jnGgWvJUUh5c+ji/FDgTZPnfOWZg7xGwI9DMZ2aCvcGgUvIlFV0N1s6sARGGqGdhoHNnHMhSPsHYWUCzX6mF8qdl6VQKW80Dq0w7VEo2NLK6xbfs8kPz6y4/Ij98+WH5x3oNz98Hjuh5Xt5rNE5Eqcj/78Cey/sS7tbrR3LSE2B3OmkPtznv1LZ+3EpqP/xrtLmvN9s/Ky7Ipn/CMpmo4+/GGHoEjAR67JC1aBZM14P/UtduCYznPXV99YrNHc3O3sUJMb8/IL8f17a4mbiS+k6MD3HZJdXNsdHgSrOozwJtF4+rMEG+7oGbAN2D9K/N1mKblaQ5/mnr1rm9J3icNpwP5k5rKKocdjUA2B8dHQudOz1BlqLZyKOAvhc5BOiRfxqpJPcu/5rrr3aApRPvr1wcvJ6FLzc0v39Tx9zodnPkBja1Zy1V+9zW7J/bsXPuwV7D8r4n3X+os9qPCV0HpC7SWjr8wSmcnZ8y+6QoZ0WyY0AzxWkerQ3guJflgu1qGnV1AH8j7wGwgUsn47TlsXc9dY4E6qdVEer+PRWfnKDyyej+gevnOwnK1JUqnKyIw+oHZ4KFy+z8lth/hDqPBE1lrqtc5hWQDAYP0oZls6wEe7rHKhC9DCyVpW2HLT3lORs8QL0rO/JrPam5/+jZfTXlNi4OOnlipfcl+VVntstdwLtBnWf+MEs9/yMrWjvn1p45jbyfnwPPynT8oj6OV/4vZOlk6upQsaRoBrYme/VoldDc/eH+Io7B16EallEWBuC9M4YPSml8acrtDP6Zsrx3MfzUbPQVYEfvQtkp9sfUqviwJ330jezMxluiTi0O1o2rQ7x2s8YM/g7Ezyc89auppvVziQVOtqWQ+lzckgo9mXgXvAbeAR8BBZ+aRZgTKDBTsKFgrTeIGix0I/8csDL1Hwy/U/mdtXyFQa9JBUfFmhTRuax1elGyLpl304ZtaMP4KEWfPOysid61QPzFyUQU/gfGgPfAZ0B3bQUd8luDvBWA7iO9QNQs1cbkP0z96uu38btUIWIo+3co1U71yaq+kaUTQIFOvDCamDksd2rTHLkueUe3JLjKkbfj8pfk+7xzcNM9cQY5UGM4YBejp+lmE08G4EwdZ5CnukttSRJ3BSNcmZZWVgb6YL2UkeAhHP9rOHccZ2A2fmN/ASxphKyLarVkDSrWBf23tFU3ibILdS1EJdcAPY71lePx73pf5RA9BSXHheRllaxrRtzjy1Z1wUk/EkeCLq0yZyW8wU9xcDd4Az64E4QLNuXLUmBT8AewGQh7itOfPK01/SN2p/A7SxmAsdEpDaoNmhjoATYAOwEFUy5REHMtbVBf/s6l4JNG+Q3R03m5SIS+OLoL/AvoTd3YY45dDSYUsP0W6L6wNnCJzo+b0O+PXZ0naUVB3u/B+2kNeJbTMZok3WrOYF0IWTpBYqXXcEhpn9vOFVwVFVajTltsRP6Rjwc2fV+UHvbzbDJ0UHXCmfgxoBN6Ez/VUK2DOVGu5NgXzUOG6lpGBgzAt24ipQTG8kKBx1ZAN7PMBR9109DNsprBVaFd4/jxZWGnnFvarZvM5WCVBPUM4/zRI7NSZRJ2birVSNbl4WRrbI4EYcHVWPLOAP/Cf+/AAd08ZcYD2R5JPRuxPQFsCVzSl8TL0NuLsj+5FFKmfY29L1KWVTHdvBUkPIlv57FVkCU+eoKg6Jp9LDg+mOGzj331S/WRsOBqJnlXg7Np02tsvQV9zSaNEajnQrZbgCFAgW9QNOun416K6Pi/Sb3jSzGSpGxY9J7ERhpdEXWuT8GVLs53y+dzpzRPLkYUyNP923XMrdrtJ7/HIJCsTvq3CJOW1ZqBQ0lq3zo5UYoehWybqMQs5WoOCGZ5Ub1fadv/G1wWSpXDuBBqIFMO0Y0i7EZXjvrCbD5Kxg5cHz4MU8g4/VTVl8Dmvej+KYF+lGq1rv+hPtG/fkXmVSAsuLqSvA05PpcD7+CKMq2E8o+RqGBO17XJrRR+TtidjYKwLCUz3mmDvjF4A85tCp4LcVJvM64QkheaTJkFyLwOdA9RGke6zhXNLCUKroL2KP8juI90XQP2BsXn31T29dj+v2xLlbTX0FT1VrSyIg+1sF0Raaz82JQbROC04szpsao5BVmM1Y9e5oL8Te8N9vomyDVY3QdkcfOJd7CONVoufEluBFGtHYS9O+gDGv34ikYf41uU9dtX1McXB3EXNdn8CGhUlVYUfKquuCBUdX0MPHo1Wj9LExuNeme07CfZDEI5rv0+9nQRXxc87aPsq0Nf2AvdAb76AT1xORa8Ct4BnwI9KtJxnx/0BLppC3ODKHmYTL2RJ57LLrRb1x6N2H3lJRT3xr+GnP2Fjzlp32VgYQchulYcS9vPcuSlTmq5Bl1M3eo/mkFdzGHsOPIfR/chR15NJOHbeHzcGWceBT0DTml2+GBwWCA9bleTICuHKD1F+h6qNyQ/VXLL8RhFWxT8qi9sAQ4h/YFUBqtcSBehSosuYoq4Y6X3RfmlCZiGzPScnM1zChKMLcRt+0SM7x9XAQftRw7kseg9AjrE6bfx/ANpf9ioMik1G1FAU9dP+BbkWP3AseqDfpJAQTdfvfb7PNDi6ShRsLMdeBc0RSlG5GmtgEaRGvlFyY9kai3AGyBJXWqPz+Nv1H4WONMFUrxlIbpeHAoyC7Dwrwf2zkzh3JeU0Q1ReJn+MSXMBnWI46XA5uCPYGMQFI2ed8fON8GMcuzj0/9hV49FfI//h+juhn+TyuFPjdjUQGDDEF+G0HbNcpZFsK3vI+qcvAssEqhE94bzyN8AvZJmzQJ2M93FN33X7QiM3gKC/aoveSei87VPpehqML1XiK6u232xVba+iG0FjDtSzyb81jGpS0lys8qigRp5HQdhXiHTjPa5v7F4ff4kX62aqVtX+9xeyw5rXpgX6zO+PInS1bGKbViBjq5R3R4eFIj9f3voaYZHF9NEwrH6HjT/bznPrW66k4ECEx+Rbf0LiiR1/KJLBeWuS775tqXQXo1cOxd2IrYKDD+KyC9k/Z7+0Luwk8H2BGwoCE4io1DWF+0PBU+C0OBKRsUZGA+uYHdToJvHK6Ag9/BDwUulgqslqU9t6FZwIGarm7oekbwdo1e32fQpBcBHhzRA63XLFlwV6qQOnQP9gWuGcBXSDwC1LnfgoO5pQdFAZu1gomufY6GB9CkgGKRJ/V1QkUCf46F/N1O3wZXIqnSANRLCnlHFcbLcsPwm7drldm8OmOKUi/N1+2mf69Aunzt9zRHNC0iLc8N+/42MskXjYZXWUXp/fF3Aw98x6AwAn3nobs+JvKKHXqkqSfp4El2XX0nKJ9F11RWbBr9LoLRbrOLPCprJvc5Dtys6Az30YlVajn/YKNlVfiqJB3AN0WOysS6FuDTKKdjSRXtjcA1QcKWZq2/Zll1os/gbBZb2rGwGegfi36Oe+vWqpsdX8zucf4E09c2KCDzfT0Vnh1R2OMdvvpC8mkjG/5k4on7tknVciY60XUlTQBkUBZ4DqMNnIBYs2yb3y36RL2J1Ir8VFcfKSjfmO9JNTiNQmsN77qHIqr6N1dQh95tvf/KaddEIdwLF0zymKKq1MX9yQZmblu3v2ToF0O+je5OH/pzoHOShZyrpGdiXoj43hFfRGw00k+savZM8m+xJv0g66zSbgZYdHX/1Ax+ZhpICq8t8lON0sKPZKgV3O/J7cpx+hvnnY0vBna/osc61vsr1qNfSl1yBtp50HEX7Ez0Wz4CDM7HxusOOBiz9HOm1lvQUDulpQlB6BROC+xyLDqRp1tslIzgWo10ZluZmoJIBlv7xqVfk++OkXP+mjrn1FSilFuL4pqbcSSsM9f4/Y8Oo63+p62vcgrvQNJ/R9kT0bmih4XK2WtMUJ3/ghF48TsnykzMAr/NSSgGWj+iNrBlAgdZDHgUWQmdvD71QFfxbkEz1LV85Bv98AndfexpYaTbLp59624xSpM1HkO87WJGpS/HvtCibDZLXl3aoPwTlHtr/SDCx3PvUqUeyZ4TUsxfHUUscalk+wTnXWiudt3GyHgprOJT0VCKME4e6JYmBSgVYL1KX18iTtVMLsyT1L0nWXbkOZZ6J9aZOuaVYx/VnV34wjZNqCmnHg3wwr63ucyHpSNsP8Wz/9XDY/GiQ7cuUedijnB4J7OOhZyrJGdiNIkt6FNMx+1eR3vCi31E/9e9z5opSiMnbkvxFYnQK2ffy46LCTj1u4aoPfp+ewPf70R2cQL8uVeGlCcd3djiv6/BQR3qlkm6honccla1F2q8c6bWUpFnoaQ6HOjnSgkk7keCKC67kuq7AzSQBAy4iExT3UmUuqfn1Wq2fiBXWTh3Lm4A9FCCVKvq0A7NYhxC0rexp6x707vDUbQtqW9PIX3s0VCfzFQE93xu1/n3OPIGytlsCA/DZmeJh0/xBy78Exi0Z/2Y7Jqjk2F+WNF2M08r2ngV13dCLMbqO1KVwPFbE8X8CDVh85BWU9DjUdZP0KV9POkvg7NoOh/WI7nFHekWS4P57KirMyBfXqWO4eXFCDf5WIKVrQFAiH//TT/V4cLNgIfZVbpQj3ZJiGKhEgHUznfXBGD+as5e/NL86sfNAn29e+dhrnotqn+tK0HZqzuMDifipUZNmsb7zst/ASpxsGlkO8mzig3AXvCnrmOsiGSdLodAvTsnyEzGwLdqreJRoFRhzHFulRdg5tOWiHKHSOosyXUhdt3WOM0WPiRRw1KP8RFvnwvFrwcKeDfgYPb2l9Zmnfr2rrUkDxFFQ7oeDyIAgWKAM+3dj0zXU36gMdWVpUjPDWiIQlLgXuZahQK9gIfafBz7XckfRtp1U7gDrK+hVwBIvJ+Xb8aHQ05ras+g1w4d0zQve58htv9yw3PbxTmjGq0kd6R8+ug2usx7t28SzjcOCevComYfgrFZQrbCvf5/jM31d0LdtCAPwqHN6UEh2MPnfHKdXg4ns65Hh5470YJJujlsFEz32Nfu1pIeeVEZ56tWimmY7hoLVPZ3TwE4fb3zTU78R1NSHXDLalVjhNA0aP3DUqX8EXcvXK80IajYqKGODCYF9Pelxtesh+mSGd+VArZXdrWg7yh1g/Z0Do+9mxMryi+T6tpsjt01JC9vDa9FszKmrjmp+RTpca1bOOfx03XhmaTT+L92k5/Bo5kvoPByidz3pcaMmFdUNSI8jTUpnYENM/MbTTKvAWOU4Zz9lc6OnjTT/Pqc3tn36lvx40tOPWlQ7AKf28nRsJnoHw/1oT/1GUVvR0ZDvSav6jAnHQutyX3P41500zRLVquwa4phmoqIkbG3Z01GF6iyvHcFxUzlRzIfPRa5YP8nv0Shf7FOg9xX5uXgseIqPbhod/Qud9h1zK0+d3Lxg+6w4G5xYkzkAWmD6AHCNBOJM1HU+bV+BBvzOsxGXwZfzTSzSP8GW3vw6yMOWbtR3UUY3GpP0DCgwbu9RXC8iPBShpzdBBwDNwkTJZmSuA56JUgrkLRPYD9t9lf7wZVhmHaQvmsDHv9LWqxPoZ6Fa0dF80GHOdw18lwimsz8RKLiuBXkLJ/SCQrF0Y0fHdkJxYi38htMt8EMIimaknw0mBvaXDuxrdxrwmiRxlK21JF3LbgXlfPSs4O38wrlcrgDraxqhryw7b7xB1mdOzQ1u1zm3QmZrr4IVsK9/t9PULvfnpYfmbxh3aPO3mhxas5Lw/RGIOo+UP89KbTO/BtLSLh6t/QSdG2P0dKPeD8TdqDdGZ31QzzMWuF89ob+uRO3beXoQGhirPP3/ZexpZnLrGHu6hhwKkgRY3WNsFrLfKPxo8K3OkVOr0MaOHOOFy1ivAqhp9KVvQuqYk/T5HHmfUeZ7R3o1klyPCNWuBavhTFSdHMvFyL8IuO7rd8OpgqwocfUFDXDiykXZZPlz/hgUVotU8s98inaojWlET+xWTlMwYRkdh2ZxHYhCXilbfbPGNbXayuZyI/LL8nT3CN//N9jKgG8C8yJ8W2uBOWbmTqLIPp7FTkZPN/51PfXrXo2TQTe/PT0bci3H+YsY3ZfIHw22itHTrItu1BZgxRAVkX0QebppxYkC4xvilMgfDuICLJnR/znrRV94WzseMo+HjlQ+8tSrZ7XHcV6PBqsxm7QKdXtdp0sg+C7K7htSXoMuV3/VAL1W5KsQR7qFpFclmfOvNxVfA/T0ISiasfEJSuYOFmT/W1BqsPtbbGwCshD1GZ+2ZFFXWhszCgUV0WUtV2PwUl+jTT/mTub/Dc5X6nevfOrTv92hrj16XZL3WqPCRW8KdvcH9fyYwoeaYp292fEZnU1F74rigq7fLTcO3ah9ZAcuFL/yUTSd2RmAtx6k7D57aujedRyXuMBYhR8APjfgrugNVAFP6eCpp/Ov0UWPyHRzrIZogK1zvZyYN6Jh6ge6YQblp2BCFfcVnLhEA8K0Mj1twWA5zvtFwZGkPwbWCua37OsL7BroxonrvFT7fwkY4gyE5IdxGKIemZwZd5G1ZJSpEyxL0bqOwS031Vi7fJ9qMz7LsGvi/zcYazlEQWPEdrkOM2c0/5/CTV8YSHgXI7RlDB1YMysaHZQjII3xoHLZtFMjmAGeNd4LN2966t6Pnh73xAVPeix5IBgMTJIxoFlZfbg1TrwCYxnh+E6jT/yTn+fGGSVf/z7nbMp86qHrO1tTyk3Mw42aUOmJFzfD3VZwN6EmPMrWiag1lQqkXMFULR33TiF0uPwOUW2VvAHHu3OrVP8EzZ4tA9YBmixYBITJM2ScGJYZSHedlw19zwu0P6vdXwLVLAMsPafdi4tE2JTqbM4vd2G+E5Pip/Mh0PZ512GdTTu7Hb2l2K5DbsNvpje/3RM7A6OaadP1nBCaTj82O09q0tLOeKUTN050xHxnpcTfVPjTjfrsOMPk74HumZT52EPXVGAAvjRLsL8nGffBrYJdX7keRfV7zXJEidZv7AXOilJqyfN95LCAh61GUNEM1g0cxz4cm7Y0W67ZCAX8QXE9qgrqVGo/bAbuuxIcOK2EskmKataqH30qbA1c0JbrWOgRrmYZXXnB8mH7rlnKMN1ypmtCRdyXtKYsxkGtz3uyoJNVgKUpwH05kK8WDMduO+T2JdBZp5wL20N9YEzFgvcTl/tn/s53941d+Fcwo1HAcmCXQkIjbbm46yQ4xLNNz6P3qKduQe06fhwD4m6ayu8PdCKY+DHQD7UlPVQTBcayxzmd5E1Q/fucYZSZHOOLz+NJmVg6xk6tZ2uWw/caux66V8HfLvA3rdYblsC/qBkQ3bRdN/+F4aFzjfDQw9FWnUeTHOm1lHQbzmhtn8+McsFvV3CvlxDmAqUEWCdT/rJCJZ5bcazrWl9PfR81PerUyz0f+ShnoeN78sfVdThO3x2nVMjvNSK/YH5m7rgs/h1OwWaSrept1ym35IxpuT9R7jifsrRPX2XW47Pu4P98ytSZzlb46/onn65m6J/Qxj5eLS6I/sfwdzNpA4vTQ37rRj2UMlpgaRLBADzpEcbBESrFWS+wM7o4wfO3Lo77gY4x+hqA7AiuitHzvcCtSPva0Q8YEtWlaIZ8dbCup/fboXcxbR5QoTa/SX0HefqWRk2j+c/CCqqNtFV9QRwVy6LsaEZ0QnFilX5rdjEomr2aGEyskX1NcpwLRsGvgpQk4jovNYO3OAg9jnEV4MfoOB1XPn1jedKzDLBUjWbkKiZZBFj6fsvwJB6zBmrT5v83WMXlavo2FkOrnXqOzJ88fh+/ESPt/IaDvhttvQ+skqTNtaxLm3QhHOTp44fo3eKpG1TTjXpf0CGYEdjvyb5GL5cH0m23NQPbkLRa62RnSuLAuMXKy2wfAb91Wp098RD6kxbRRwXg781eJHRvBXJ6Al/9UENVyhhPvWeAx4FrJoTkVqLzQzdvr4Ffq9LJEr5Oe/NLVk2k9lvk9glodGN/RVDVAIt+3AUfVgr4pt1PQK0EWHp69AH4L9Cs1QMc0+/ZppG3HYXak6bry0uOvHInxd0nyl1/yfaJMUoSLWo9OakFHs8txtqrqorWfYH5un7l9b2nX3ylvYryFVW7OuMvenX2QyPszTx9vgYOvvLUDaq9SMLoYGLIvv59TtyMSUjRtpEMPzp/D/NsrQJjzSAmFo63RsIjPAuujd4WMbrvku/zyKEregog61U6wd14nN8dfJOgEcdybAcn0E+rWur1P229xeV0TXDJJq7ECqcpuFrKUecYjqsCm7SimcOnwTMxkE7YzNEX5GkQuipYDX+0/vk2kDa4wkzoP3nfVJkmyRko5QQ7l+qOTl4lgc3M3DhQVSHIY4EJo5Aeyf+xM514LKV/B96paiOyq/xQTPnMZuq1+X+mrRbedKP2ne38Nbpbp62rjZTbgHZu5NlWfbMsbWCsKu4Hb3jWFffvcyZgZ7ynrX0INup6JAvvT9BWPWKNmtUL0nEO7dZseaPLczRwmqOR29D+ag+w+uCXZnCC8ngwIeH+YfSJ9cF6MVgfu7oGumbLtFZVA/13sVFKUIWJX+R1frnq2pxjoUeFJgkZSBtgncVBHQJ0w0wsM9vnHuVtvreaqnjZ5HtYklGv9fP72nywkbT9TdK0ZsL3phM0URP7nDi9ceT3ns7oa8ClBpW6UYs7HxmEf1We6/Rxs2o6eqzrugEEHSopMJYxjrtmnHyD683R1UyWU7A1nYynnJmtE9ckaZfWyfWVQpv1WP3IBF7rCnUZ/V9cNrJosKp1Q0FZmYQNg4mV2of3OalrV0d9CpIfdqQnSfK+b9JvXsLwnkDncFA0Mzo8q2skdWlWTAFvUBYnYftgou3HM5AmwPoLB+LoeNPhGu8NbPqGu+YAZrEmtuvMxZtLScVAUMcC91x+Wu6mqdOa/9N9uKMxOfCgx4TbAFenjCldM9kD8aSrhzcz0RnuoRepAmcabY2MVJqVqanp9Wbt2q8CA1xUtUbF96J3T0tfLRRPu72OgroIx4mGTpoVjZJ7ojIDeafS3u6BtLrb5RhchNNnJnC8G7rX0fbVE5SpK1U44ZWj3B0Op3Vv8n387ShecpKeUKzgsKKA5zVHepKkRINGOPoPxg8E4iooA0g4NZhYwv7tIWU12NV5bZKAgZ/ncfwKTENNHxG91E89Wuvtg5seX2ZofqNcPncQvW1tFr6zXoG9Mkpzr26X+3zGjNyt87TPjXr7iMiFuF6ewMf7dDxNJV8JtvUqVCNK+L0ormh05CPPovSEj6KHzrXoKEiP+zCmZmcGgf8Ck9kZOIhdjbLjJJPAWJXQ1/Um6E381MU+TvTvc5anTNiM5yMY+BD0iDNEfk9wOfZ2wp6uQ5kJNnthTIu9w9a6ZFZXi6Hj2S4G/uhpeGH0bsLPLfFxvGeZelO7EYePBcGB3na0exPaPbqSDaLOLi3+uKrVGlRXoOPSzSyNOq/BL80kneEwqjV7n6NzniMvaZIGPpNA8Lt3a5OmPus7i42qiT5SeCGIE30LZ1ujK5wB+JkTDIsjkvxJYJFwS5XLwY+jPfwtqOydpWcYHVEwHLOdQr5rJOnlDmX1ryS+jKlD2fpq+fJeRkOUKK/v93wO4uQHFFYMMRObTNnFwRdxlbTkP802yUAqsn5srQmmt9iO25wdZYzC58YZCOQr0MhsLQi2dAP/FDwMNFvkJejeAXzkry6DFOwCHvQxUKTzLL8XcNmLS6Oc6htbZKvw85m4spXKxyHN1LnkeRKDgVdZ3aK+E1yOkKb74EK+laOrtYgu2dLXRlAPY2H3a31GyDdoD5qdbR87F7mcJk0DrKVmUy7jDnWd7PDjep8qKadZ76Dog9fL+ZTPSsfnEeFzVLYF0fG9WVXaiHbgZyrQrMJgMLXW20hHmwsfD/D08330wqaOPU20UruMlB9bpbZO0GjSZ8akdcnGTdmHpsXN/hVar/9D9lNhJ4Ot3voa7WlnL/pZ1GBiOHYme9qS2s5AgckGCcq0UtVFFmgmXn1a/m0KriCtI9uyC8dDj8k1c/xygso0g6BZDJ0PjSjn0KgfHA1bk7TTHellSYJf9YXjQoxfwrH7PCSvUslHUtHNjso026/1WNs48pImXUgB13mpx/Q6T3xmzpPW6dLPuxLrKS0uwLqCxmhq+rV6alQ1fYUrdc7twDvV9MOj7p3QWcZDTyr6aF2S18x9zL6A0qM+iujo3+fU/Rocz7ZGqsHDPCjsF6k0K3MCP2+dtVv6L/qBLnoKjHxEj7dCR9XY0jlyuY+hIh0FGppxGgnWB16zc+h1Br8BIyivQeMAoJtSQfrx4wLymwoJ5dzS9s+wvyvQMfKVrVHULHmx375la1oPPhS4jwxxUut/hoTkZZZMHStj7GrgCiDGkj40s8pSGoInDUr3B084TCj4VhC+niPPO6nlvAxr6+YY0ssXnbwNpleMi0/SW65USYhyTTnqMcc+lfKhEeuBv+5A/8MwKFV/RIhDHYCm3n1kMkq+gViiQ4ndnX0caNE5NpHxFmXKNtQjQtqjr9z7yt/TcBZXhsr1OPwNTyfeRq9bmE3yFgLjQBqZQaGXgB5p7As2A6uDlcFqLfsHsNUN4XXgI6eE+VpIx0hJjwgLdrTF1gbgKx/HinTOKrYR95tyNf+IsIWLRfB1XFE7i3/OZCdsZimOgth8bK8LxgGXqG59EiGRUCbzR4QFB7DdA4SdgxPI+1VBN82W8vOAMSBMbiXD+3FpUh+wrc90qB1BqatHhK41WPfRopIOTlIyG1kfLvcDWudRkFoIsPoUnPHYnl+u40PduvC/6eGDVN4Dcyf1hTINE2DRlk5AAYWPaL1E2Wb9sH2UjxMtOntFHTd0tgNaQ5KFKOj6EZRi7/AYfzMLsFQPvvYFWpeXRPSoyEswWhcBlhqDr7qxTo8g4mryMu3X2FNwHrVOM2w2J5J/bJYtwGrhalXqKL63sPuLKDhaPNLBmEzKa4b4u18stv6hOraIMZMoG3vzgzNBWB9wPR5tVQfla2INljp0YUGbFvAdDBpu+rkV+xVOgNNlwQ1A8i2IWpdSVu+ouwncD3xEJ1dJC7/jGoP9o30cadHZN85eMJ9yjRRg/S4BV663jYL0pN7Hj8WA70L7Z9DtEFUZ+X8BtSIKzvYGzseFpGcaYIkXbOram0Tko9ZxxQp6dRNgqTH4e2QMEePJPwgkHnAVk0V5BRB3gij5N5mp1r1RrqwBVgtX+ghoWBD0JHnzFbc56W/K7w00aAkTDWZGgTWT2i7Wp/y8QLPNb4EwUV2Rg7WCTfTCAqySgs6Cfd/tHCjqme514ASevY7zLWh6/gzAq57f78pBv4mtFsFXM4hdh/r1HN1H9P2kd3wUS9C5lrJHA58LwSFwqNekp5dQX10Wpd1aj6BPVviIFlGHrWfxKR+rwzHQDJlGkz4vSmjdlPrc/RGG9WhOA49DInQqlaXzU6+8PwQ+rESl8HkJfC5GXcd71icfR1BGr+c/4FmmLtRoj94u1UscYY8ElyLvEvAn9G5kezd4lXJT2IYKugqYlwAbg93AliAq8H+K/D2wq/OpJgXfHqJdA3HuKqA+USwbsKPgpx96U4szfH9T7irKz4v+P4BrTZRiiD8C3d9Gs70VPAbGUXYa21BBX9f8VcH2QI9glwFh8gMZB2NzVJiCR7r42Z16J7J1Dp48bCRSETmn4fSkRKVMORUD8HwzB/ceCmf5VldSXw6lgI57nOj7SSPilErNh5MP4eQW7OzvYWsNdLYCuqC2NVmfBm/i2WgFxm956paidhmF9wFRNynZ18XsMBAaYOEv3aD5/+/pRjAEVFN0PRwAPq6wEydS3+Kgv2e9hUXNW8OfXhppGKE9x9MfvqNBp4B2IQ1bnnQFpArExqH/Btt3gI6b3oLLg45gQdAT6HMvvYFeFImTB1HYEz+q/dZgnJ/6Pt21tF395kyH8nakDSN/P/RmOPJjkyh3AeW/QfEi0C2kgHjeqgUKhsZR5l22E4A4LARb6rPdwbKgF5DfcaKA6AD8uDNOMSZf16mzY3QyzZ4Dp3UxqZpwEPQ2wtxAnV4RrX7PBeYEyksrChB0UHWS6mT7ugXqKJNpt06+igv1phpJZOEoXOuCpJGCjzyH0mM+ihnoXIaN/sAn8NO0uwKIqhy/DNqa1sShFNQILE7Ey4g4pYzydVN/FGzhYW8LjttaHLfnw3TJ0w1Aa7t0k9TNQiPnSsszVDgQX16pdMXUqcXUB1OvbkC/9axfwYO+D6a3vcd6lqkLNdpzOu3STfp8sFiE0wrgl2lBhJpXls4fvQl+LPVX7Vrt5WmREr6eBVcKVjSQCcreJCjIOSqY4buP/Suxr/NSM4erxpTTfVvBrFCq6PqimavXSzVUjfI+N7RM/OLgdMZQD6CodUUg8pcGOnEWAIqMpaOTpRyiWSNN9SrA+gx/PmCrDqNRj/AeB/Ezto0sA2lcV88GXgof4qwSUgjmNvOobFN01gVPe+g2hAp99Vc0ZAfPxohLXZTKLvQPXGv+7IFPgKXRo4LE/nGOYfdS7CrQOR1sE6efUf632NGN/Gzq16CsKkLdU2m7Hrlotk8ztj6i6+iNlNNMlm6kDSO0R8GjAvlTwG6gXPcHcfYq0FKZUmdKZKsaMoRKdT/d2VH5EHicRNvOdOR5JVFWa7o2RlmBmgYC5RwAqR+fAy6k3sLsF7smzQxwILqCtcFg8C/wBpgCalUUdD0Bzge7gJ6NdChpT3egNxh9RK/HlvPkaUUt9fXzcaxF5/pWBkIS0Nci9689bGsBpWb4Ugvl9SV3n4XfWqCsQYaXoFt4EYWfsbKfl9GMlPCmC4hamFrssM5/DbC8BF1FcDuCx0C5RC+dXAGSHI+7PJ05yauhDiXsLwfGedZTUNN6nFbnLWk6RnoLNyjPOqqu2SSc1yzoPWB6sCEl7uvepPuUnpxkJi02Xa7pUVpZhMrmBlHny4AsKqaO3kD3Sr0cl6VMxNh5QIOG1EL507J0Kq2tTGewcEJT2xuBrcH/AU3btgP1IAvhpLAh0EL0ybTnf2z/Ax4ALxJJ/8C2XkWPBvWI6UuPBlxOW7/20MtS5R6M6fGR+kycbMixWRYffR6J6FHxF0CPn6JExzZOJ6q88gp1xelNR8GrLtq5CLq6IPsct4/RuxVUTDgG3+PjcCo8waNSXQv6Ac1GxAq29bjmVuzfzlbXFc1gbAl8+ghqoaKZ2TFAdq+nnrdDNd0ZmgX3OR6aMU8l+PQu7VZ7NZjwWTOketYGB4IztBOQqewH/VFa3Qic6Fr8H3hZk636ke4zK4COIKnoXHkc3AQewHY5Zi3Fr6uf6Pwvi9AODRh2x/gdIBikaPbvb+SPRe/hUhyg/FuUPxxb6ms6DjuAdYAeUyYV3Ws0S3kbuB3bHyU14NCfQpqLe4dq+ZJKnm6FYD3e0yMCTUtuAhYEjSa60L8G7ga3gufpBEqrG+E4zY+zvheiL2jfj5VuHD7qRjKnR726UX+Fj7E3CGxKV31S2yjR8dQUulfg4zKUsC5xrBt9pGCzEwo6xxS8xck0bFY6MNZr9Rqo+Z73P+HjpLiGhOVTl2YZVgHrg18DzTouCgp9R77oWOt4it9pQAGRbqhvAM3a/BeMwY9UfRwf5qV8ZxAn31HHd3FKUfkJ6iqY+YE6vyrsaIsNXeeXAB20XyTqL1nczIpMVu4n7dL1rBdQP1gVLAvUF3R8Ctc6nc+62arPvQ9eBy8CHX8NvMom+NcF43M7KviSussWZKk+6u7KRudKUHRuqI9k3nbq1D1GAa/OTx2XJYGuC/JDg3uJzrnC+aiBzSsC/nzANjPBl24YE6oqqQMsGqBo9Y/gd0Anb1sRXbS1PuQacBsdY2Jbabi10xioRQa4FukmJuiCqsBHN1edp5qVnAy+4TzVRd2kDTBAf9Dx1w1d9zcNTKZz/H0GKKiaZM0Ax0PHoTDAncmxqKvJiVL4SBRgQZQuXtuBA8CmQKPFtiyf0PgbgB6paYbLxBgwBowBY8AYMAaMAb83MgisNNW5KxgENB1rMjsDmoK+FVxEoPXc7Fm2ZwwYA8aAMWAMGANtjYHIGSwCK63/0GLCIWDVtkZOivbqkcSN4BwCLS2QNzEGjAFjwBgwBoyBNshAaIBFcLUwfFwHNm+DvJTaZK3/GEyQdUmphqy8MWAMGAPGgDFgDNQfA1FrqL6kOScBvYXTB5j4MTAJtQvBbX7qpmUMGAPGgDFgDBgDjcZA6AxWcUOZzdqR/b+A1YvT7fdsDOj101HgdGauxs6WYzvGgDFgDBgDxoAx0KYY8AqwxAhBlr6rMRBoPVZ3YDKLgX/z82QCqydmJdkvY8AYMAaMAWPAGGirDHgHWAWCCLT0zas/g/2Az0chC0Ubcfs6jToF3EBwZd9ZacQjbG0yBowBY8AYMAZSMJA4wCrUQaC1Fr9PBNsX0trQ9nPaeh64hMDKPmDYhg68NdUYMAaMAWPAGPBhIHWAVTBOoNWX31qf1Ra+j6V/b3AVOIPA6j22JsaAMWAMGAPGgDFgDJSHAa3PAn8CH4FGlQdo2AblYdCsGgPGgDFgDBgDxoAxEMIAAUgPcAH4HjSKjKEhu4GSZ/tCaLNkY8AYMAaMAWPAGDAG4hkgGFkT3AHqWSbi/DFA/0TWxBgwBowBY8AYMAaMgdpggODkd+AFUE8yDWeHg6Vrg0XzwhgwBowBY8AYMAaMgQADBCpdwGDwIah1uR8H1w80wXaNAWPAGDAGjAFjwBioTQYIXBYH54MpoNbkfzjUD9g6q9rsPuaVMWAMGAPGgDFgDEQxQBCzBrgd1IJ8ihNHg7mifLY8Y8AYMAaMAWPAGDAG6oIBgpodwPOgGqJ1VsNAz7ogy5w0BowBY8AYMAaMAWPAlwECnDnBIPABqJTcS0Xr+fpoesaAMWAMGAPGgDFgDNQlAwQ8i4F/gO9AueQVDO8CbJ1VXfYSc9oYMAaMAWPAGDAGUjFA8PNrcCvIUj7B2FHA1lmlOipWyBgwBowBY8AYMAYaggGCoe3As6AUmUrhi8GSDUGKNcIYMAaMAWPAGDAGjIFSGSAw0vqsQ8EEkFTuocC6pfpg5Y0BY8AYMAaMAWPAGGhIBgiUuoNzgc/6rJfR26khibBGGQPGgDFgDBgDxoAxkDUDBE6rg1uASz4m8UjQLet6zZ4xYAwYA8aAMWAMGAMNzwBBVB/wDJB8D4aCJRq+4dZAY8AYMAaMAWPAGDAGyskAAVVnoC+wb1zOesy2MWAMGAPGgDFgDBgDaRj4fwiA0POb8QTHAAAAAElFTkSuQmCC' alt='AttackForge Logo' style='width: 200px'> </td> </tr> <tr> <td class='email-body' style='width: 100%; background-color: #fff'> <table align='center' class='email-body_inner sm-w-full' style='margin-left: auto; margin-right: auto; width: 800px; background-color: #fff' cellpadding='0' cellspacing='0' role='none'> <tr> <td style='padding: 45px'>";
  const emailFooter = "</td> </tr> </table> </td> </tr> <tr> <td> <table align='center' class='email-footer sm-w-full' style='margin-left: auto; margin-right: auto; width: 570px; text-align: center' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' style='padding: 45px; font-size: 16px'> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'> This is a system generated email and reply is not monitored. <br> For any queries please reach out to <span style='text-decoration-line: underline'>support@attackforge.com</span> </p> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'>&copy; 2018-"+ curr_year +" AttackForge&reg;</p> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </div> </body> </html>";
  
  // Modify the email script based on requirements
  const content = '<p><strong>Attention: Flow has detected aborted scan ids.</strong></p>'
  + '<p>Please check on the log history of the flow run.</p>'
  + '<br>'
  + '<table class="styled-table" align="center">'
  + '<tr><th width="300">Scan Id</th><th width="300">Status</th></tr>'
  + aborted_scan_id_table
  + '</table>'
  + '<br>'
  + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.af_hostname+'/flows/'+secrets.current_flow_id+'/home">View Flow Run</a></td></tr></table>'  
  + '<br>';
  
  return {
    decision: {
      status: 'continue',
      message: 'Send error email',
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/ss/email',
      body: {
        to: secrets.admin_user_id,
        subject: '[Flow Notification] Aborted Scan Ids Discovered from Tenable Web App Scanning',
        html: emailHeader + content + emailFooter
      },
    }
  };
} 
else {
  return {
    decision:{
      status: 'finish',
      message: 'No aborted scan_ids found. Exiting the flow.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode === 200){
  return{
    decision: {
      status: 'finish',
      message: 'Successfully reported aborted scan ids to the admin user. Exiting the flow.'
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error while reporting aborted scan ids to the admin user. Please check the log.'
    }
  };
}
```

## Tenable VM - Poll Scan Status \[Step 4 of 5]

<figure><img src="/files/wPDYRvvOM64dWZt3zMw8" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export the results of a VM scan in Tenable.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * import\_vuln\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM & WAS - Import Vulnerabilities \[Step 5 of 5\] Flow](#tenable-vm-and-was-import-vulnerabilities-step-5-of-5)
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Latest Scan Status**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/scans/{scan\\_id/latest-status>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data?.jsonBody?.project?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.project_id missing'
    }
  };
}
if (!data?.jsonBody?.project?.scan_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.scan_id missing'
    }
  };
}

const project_id = data.jsonBody.project.project_id;
const scan_id = data.jsonBody.project.scan_id;

return {
  decision: {
    status: 'continue',
    message: 'Getting latest VM Scan status for scan_id: ' + scan_id,
  },
  request: {
    url: 'https://cloud.tenable.com/scans/' + scan_id + '/latest-status',
  },
  data: {
    project_id: project_id,
    scan_id: scan_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  return {
    decision: {
      status: 'abort',
      message: 'Error'
    }
  };
}

const error_status = ['aborted', 'canceled', 'paused', 'pausing', 'stopped', 'stopping'];

if (response.jsonBody?.status !== 'completed') {
  if (Array.includes(error_status, response.jsonBody?.status)) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Error: Current project :' , data.project_id , ', Current scan_id :' , data.scan_id, ' has status: ', response.jsonBody?.status);
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error'
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('Scan pending, exiting process.');
    }
    return {
      decision: {
        status: 'finish',
        message: 'Scan pending, exiting.'
      }
    };
  }
}

if (response.jsonBody?.status === 'completed') {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Completed scan for project :' , data.project_id, ', scan_id :' , data.scan_id, ', scan status :' , response.jsonBody?.status);
  }
  return {
    decision: {
      status: 'continue',
      message: 'Scan ' + data?.scan_id + ' is complete.',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };
}
```

**Action 2 - Export Scan**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs/{config\\_id}/scans/search>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.scan_id){
  return {
    decision: {
      status: 'continue',
      message: 'Requesting export of scan_id: ' + data.scan_id,
    },
    request: {
      url: 'https://cloud.tenable.com/scans/' + data.scan_id + '/export',
      body: {
        format: "csv"
      }
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };  
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.scan_id is required for exporting scan request. Please check the log.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  return {
    decision: {
      status: 'abort',
      message: 'Error'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('File generated for scan_id: ', data.scan_id, 'file name: ', data.file);
}

// Give reasonable delay for Tenable File System to register file - guide: delay 1000+
return {
  decision: {
    status: 'continue',
    message: 'File generated',
    delay: 2000
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    file: response.jsonBody?.file
  }
};
```

**Action 3 - Download Exported Scan**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/scans/{scan\\_id}/export/{file\\_id}/download>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/octet-stream
* **Request Script**:

```javascript
if (data?.file){
  return {
    decision: {
      status: 'continue',
      message: 'Requesting download of the exported scan file: ' + data?.file,
    },
    request: {
      url: 'https://cloud.tenable.com/scans/' + data?.scan_id + '/export/' + data?.file + '/download'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      file: data?.file
    }
  };
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.file must be present.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode === 200){
  const base64 = String.encode(response.body, 'base64');
  return {
    decision: {
      status: 'continue',
      message: 'Successfully downloaded scan report ' + data?.file
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      file: data?.file,
      base64: base64
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while requesting download of exported scan on file: '+ data?.file + '. Please check the log.'
    }
  };
}
```

**Action 4 - CSV to JSON**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/utils/parse-csv
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (data?.base64){
  return {
    decision: {
      status: 'continue',
      message: 'Convert CSV data to JSON',
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/ss/utils/parse-csv',
      body: {
        base64: data.base64,
        options: {
          columns: true
        }
      }
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      file: data?.file
    }
  };
} 
else {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.base64 must be present.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode === 200){
  return {
    decision: {
      status: 'continue',
      message: 'Successfully converted the encoded csv to json type. Proceeding to next action.'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      records: response?.jsonBody?.records
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error while converting the base64 encoded csv to json type. Please check the log.'
    }
  };  
}
```

**Action 5 - Import Scan Results**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{import\_vuln\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.records && data?.project_id){
  return {
    decision: {
      status: 'continue',
      message: 'Triggering flow to import vulnerabilities.',
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.import_vuln_trigger_flow_id,
      body: {
        records: data.records,
        project_id: data.project_id
      }
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('project_id: ', data?.project_id);
    Logger.debug('records: ', data?.records);
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.records and data.project_id must be present in order to import vulnerabilities.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode === 202) {
  return {
    decision: {
      status: 'continue',
      message: 'Successfully sent request to import scan results.'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while sending request to import scan results. Please check the log.'
    }
  };
}
```

**Action 6 - Update Project to Remove Tenable Details**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = apikey
* **Request Script**:

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Removing tenable_vm_active_scan from project: ' + data?.project_id
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data?.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_vm_active_scan",
          value: null
        }
      ]
    }
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  return {
    decision: {
      status: 'abort',
      message: 'Failed to update project: ' + data?.project_id
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully removed tenable_vm_active_scan from project: ' + data?.project_id
  }
};
```

## Tenable VM & WAS - Import Vulnerabilities \[Step 5 of 5]

<figure><img src="/files/rpcMmq1ilvwtxBEhScAJ" alt=""><figcaption></figcaption></figure>

The purpose of this example is to import vulnerabilities from a Tenable Web Application or VM scan.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * logging\_level - set to "debug" for additional logging

**Action 1 - Validate, Detect, Transform & Group Findings**

* **Script**:

```javascript
if (!secrets?.af_hostname) {
  return {
    decision: { 
      status: 'abort', 
      message: 'secrets.af_hostname is required.' 
    }
  };
}
if (!data?.jsonBody?.project_id) {
  return {
    decision: { 
      status: 'abort', 
      message: 'data.jsonBody.project_id is required.' 
    }
  };
}

// Detecting format VM or WAS
const isVM  = data.jsonBody?.records && Array.isArray(data.jsonBody.records);
const isWAS = !isVM && data.jsonBody?.final_result && Array.isArray(data.jsonBody.final_result);

if (!isVM && !isWAS) {
  return {
    decision: {
      status: 'abort',
      message: 'Unable to detect source format. Expected data.jsonBody.records (VM) or data.jsonBody.final_result (WAS).'
    }
  };
}

const sourceType = isVM ? 'VM' : 'WAS';

// Normalise into flat work items
const items = [];

if (isVM) {
  for (let i = 0; i < Array.length(data.jsonBody.records); i++) {
    Array.push(items, { 
      type: 'VM', 
      record: data.jsonBody.records[i] 
    });
  }
} 
else {
  // WAS: each entry in final_result is a HTTP response wrapper from the export step
  // The WAS report JSON, once parsed, has scan + findings at the root of the object

  for (let r = 0; r < Array.length(data.jsonBody.final_result); r++) {
    const raw = data.jsonBody.final_result[r];
    let entry;
    entry = raw?.scan ? raw : (raw?.data || raw);
    const scanId = entry?.scan?.scan_id || '';
    const target = entry?.scan?.target || '';
    const scanStartedAt = entry?.scan?.started_at || '';
    
    if (entry?.findings && Array.isArray(entry.findings)) {
      for (let i = 0; i < Array.length(entry.findings); i++) {
        Array.push(items, { 
          type: 'WAS', 
          finding: entry.findings[i], 
          scanId: scanId, 
          target: target, 
          scanStartedAt: scanStartedAt 
        });
      }
    }
  }
}

if (Array.length(items) === 0) {
  return {
    decision: { 
      status: 'abort', 
      message: 'No ' + sourceType + ' items found in input data.' 
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug(sourceType + ' item count: ' + Array.length(items));
}

// Group by pluginId + '|' + priority
const groupKeys  = [];
const groupVulns = [];

for (let i = 0; i < Array.length(items); i++) {
  const item = items[i];
  let pluginId;
  let priority;
  let assetName;

  if (item.type === 'VM') {
    pluginId  = item.record['Plugin ID'] || '0';
    priority  = mapPriorityVM(item.record['Risk']);
    assetName = item.record['FQDN'] || item.record['Host'] || item.record['IP Address'] || 'Unknown';
  }
  else {
    pluginId  = item.finding.plugin_id != null ? item.finding.plugin_id + '' : '0';
    priority  = mapPriorityWAS(item.finding.risk_factor);
    assetName = item.target || item.finding.uri || 'Unknown';
  }

  const key = pluginId + '|' + priority;

  // Find existing group
  let idx = -1;
  for (let g = 0; g < Array.length(groupKeys); g++) {
    if (groupKeys[g] === key) { 
      idx = g; 
      break; 
    }
  }

  if (idx === -1) {
    // New group — build base vuln from this item
    const vuln = item.type === 'VM'
      ? buildVMVuln(item.record, pluginId, priority, assetName)
      : buildWASVuln(item.finding, item.scanId, item.target, item.scanStartedAt, pluginId, priority, assetName);
    const note = item.type === 'VM'
      ? buildVMNote(item.record, assetName)
      : buildWASNote(item.finding);
    // User can modify where note can be placed. Currently it gets pushed into Steps to Reproduce
    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }
    Array.push(groupKeys, key);
    Array.push(groupVulns, vuln);
  } 
  else {
    // Existing group — add asset + evidence
    const vuln = groupVulns[idx];

    let assetExists = false;
    for (let a = 0; a < Array.length(vuln.affected_assets); a++) {
      if (vuln.affected_assets[a].assetName === assetName) { 
        assetExists = true; 
        break; 
      }
    }
    if (!assetExists) {
      Array.push(vuln.affected_assets, { 
        assetName: assetName 
      });
    }

    const note = item.type === 'VM'
      ? buildVMNote(item.record, assetName)
      : buildWASNote(item.finding);

    // User can modify where note can be placed. Currently it gets pushed into Steps to Reproduce
    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }
  }
}

if (Array.length(groupVulns) === 0) {
  return {
    decision: { 
      status: 'finish', 
      message: 'No vulnerabilities to import after grouping.' 
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Grouped ' + Array.length(items) + ' ' + sourceType + ' items into ' + Array.length(groupVulns) + ' vulnerabilities.');
}

return {
  decision: {
    status: 'continue',
    message: 'Grouped ' + Array.length(items) + ' ' + sourceType + ' items into ' + Array.length(groupVulns) + ' vulnerabilities.'
  },
  data: {
    vulns_to_import: groupVulns,
    total_count: Array.length(groupVulns),
    imported_count: 0,
    source_type: sourceType
  }
};

// Functions below
// VM vuln builder
function buildVMVuln(record, pluginId, priority, assetName) {
  const title = record['Name'] || 'Untitled VM Finding';

  const tags = [];
  if (record['CVE'] && String.length(record['CVE']) > 0) {
    const cves = String.split(record['CVE'], ',');
    for (let j = 0; j < Array.length(cves); j++) {
      const cve = String.trim(cves[j]);
      if (String.length(cve) > 0) {
        Array.push(tags, cve);
      }
    }
  }

  const custom_fields = [
    { key: 'tenable_plugin_id', value: pluginId },
    { key: 'tenable_scan_type', value: 'VM' },
    { key: 'vuln_state',        value: record['Vulnerability State'] || '' },
    { key: 'first_found',       value: record['First Found']         || '' },
    { key: 'last_found',        value: record['Last Found']          || '' },
    { key: 'plugin_family',     value: record['Plugin Family']       || '' },
    { key: 'exploit_available', value: record['Exploit Available']   || 'false' },
    { key: 'patch_available',   value: record['Patch Available']     || 'false' }
  ];

  if (record['CVSS3 Vector'] && String.length(record['CVSS3 Vector']) > 0) {
    Array.push(custom_fields, { key: 'cvss3_vector', value: record['CVSS3 Vector'] });
  }
  if (record['CVSS3 Base Score'] && record['CVSS3 Base Score'] !== '0.0') {
    Array.push(custom_fields, { key: 'cvss3_base_score', value: record['CVSS3 Base Score'] });
  }
  if (record['CVSS Base Score'] && record['CVSS Base Score'] !== '0.0') {
    Array.push(custom_fields, { key: 'cvss2_base_score', value: record['CVSS Base Score'] });
  }
  const vpr = record['Vulnerability Priority Rating (VPR)'];
  if (vpr && vpr !== 'null' && String.length(vpr) > 0) {
    Array.push(custom_fields, { key: 'vpr_score', value: vpr });
  }
  if (record['See Also'] && String.length(record['See Also']) > 0) {
    Array.push(custom_fields, { key: 'reference', value: record['See Also'] });
  }
  const frameworks = [];
  if (record['Metasploit'] === 'true') {
    Array.push(frameworks, 'Metasploit');
  }
  if (record['CANVAS'] === 'true') {
    Array.push(frameworks, 'CANVAS');
  }
  if (record['Core Exploits'] === 'true') {
    Array.push(frameworks, 'Core Exploits');
  }
  if (record['D2 Elliot'] === 'true') {
    Array.push(frameworks, 'D2 Elliot');
  }
  if (record['ExploitHub'] === 'true') {
    Array.push(frameworks, 'ExploitHub');
  }
  if (Array.length(frameworks) > 0) {
    Array.push(custom_fields, { 
      key: 'exploit_frameworks', 
      value: Array.join(frameworks, ', ') 
    });
  }

  let likelihood = 1;
  if (vpr && vpr !== 'null') {
    const vprScore = Number.parseFloat(vpr);
    if (!Number.isNaN(vprScore)) {
      likelihood = Math.round(vprScore);
      if (likelihood < 1) {
        likelihood = 1;
      }
      if (likelihood > 10) {
        likelihood = 10;
      }
    }
  }

  let solution = record['Solution'] || '';
  if (solution === 'N/A' || solution === 'n/a') {
    solution = '';
  }

  const is_zeroday = record['Exploit Available'] === 'true' && record['Patch Available'] !== 'true';

  const vuln = {
    projectId:                  data.jsonBody?.project_id,
    title:                      title,
    priority:                   priority,
    description:                record['Description'] || record['Synopsis'] || '',
    attack_scenario:            '',
    steps_to_reproduce:         buildVMSteps(record),
    remediation_recommendation: solution,
    likelihood_of_exploitation: likelihood,
    affected_assets:            [{ assetName: assetName }],
    tags:                       tags,
    custom_fields:              custom_fields,
    is_zeroday:                 is_zeroday,
    import_source:              'Tenable VM',
    import_source_id:           'VM-' + pluginId,
    created:                    record['First Found'] || record['Last Found'] || '',
    is_visible:                 true
  };
  if (secrets?.import_to_library) {
    vuln.import_to_library = secrets.import_to_library;
  }
  return vuln;
}

// Per-item VM evidence note
// prefixed with asset + port
function buildVMNote(record, assetName) {
  if (record['Plugin Output'] && String.length(record['Plugin Output']) > 0) {
    let prefix = '[' + assetName;
    if (record['Port'] && record['Port'] !== '0') {
      prefix = prefix + ':' + record['Port'];
      if (record['Protocol']) {
        prefix = prefix + '/' + record['Protocol'];
      }
    }
    prefix = prefix + ']';
    return { 
      note: prefix + '\n' + record['Plugin Output'], 
      type: 'PLAINTEXT' 
    };
  }
  return null;
}

// WAS vuln builder
function buildWASVuln(finding, scanId, target, scanStartedAt, pluginId, priority, assetName) {
  const title = finding.name || 'Untitled WAS Finding';
  let remediation = finding.solution || '';
  if (remediation === 'N/A' || remediation === 'n/a') {
    remediation = '';
  }

  const tags = [];
  if (finding.cves && Array.isArray(finding.cves)) {
    for (let j = 0; j < Array.length(finding.cves); j++) {
      const cve = String.trim(finding.cves[j]);
      if (String.length(cve) > 0) {
        Array.push(tags, cve);
      }
    }
  }
  if (finding.cwe && Array.isArray(finding.cwe)) {
    for (let j = 0; j < Array.length(finding.cwe); j++) {
      Array.push(tags, 'CWE-' + finding.cwe[j]);
    }
  }
  if (finding.owasp && Array.isArray(finding.owasp)) {
    for (let j = 0; j < Array.length(finding.owasp); j++) {
      const o = finding.owasp[j];
      if (o && o.year && o.category) {
        Array.push(tags, 'OWASP-' + o.year + '-' + o.category);
      }
    }
  }

  const custom_fields = [
    { key: 'tenable_plugin_id',       value: pluginId },
    { key: 'tenable_scan_type',       value: 'WAS' },
    { key: 'scan_id',                 value: scanId },
    { key: 'target_url',              value: target },
    { key: 'plugin_family',           value: finding.family || '' },
    { key: 'plugin_publication_date', value: finding.plugin_publication_date || '' }
  ];

  if (finding.cvssv4 !== null && finding.cvssv4 !== undefined) {
    Array.push(custom_fields, { 
      key: 'cvssv4_score',  
      value: finding.cvssv4 + '' 
    });
  }
  if (finding.cvssv4_vector && String.length(finding.cvssv4_vector) > 0) {
    Array.push(custom_fields, { 
      key: 'cvssv4_vector', 
      value: finding.cvssv4_vector 
    });
  }
  if (finding.cvssv3 !== null && finding.cvssv3 !== undefined) {
    Array.push(custom_fields, { 
      key: 'cvssv3_score',  
      value: finding.cvssv3 + '' 
    });
  }
  if (finding.cvssv3_vector && String.length(finding.cvssv3_vector) > 0) {
    Array.push(custom_fields, { 
      key: 'cvssv3_vector', 
      value: finding.cvssv3_vector 
    });
  }
  if (finding.cvss !== null && finding.cvss !== undefined) {
    Array.push(custom_fields, { 
      key: 'cvss2_score',
      value: finding.cvss + '' 
    });
  }
  if (finding.cvss_vector && String.length(finding.cvss_vector) > 0) {
    Array.push(custom_fields, { 
      key: 'cvss2_vector',
      value: finding.cvss_vector 
    });
  }
  if (finding.see_also && Array.isArray(finding.see_also) && Array.length(finding.see_also) > 0) {
    Array.push(custom_fields, { 
      key: 'reference', 
      value: Array.join(finding.see_also, '\n') 
    });
  }
  if (finding.wasc && Array.isArray(finding.wasc) && Array.length(finding.wasc) > 0) {
    Array.push(custom_fields, { 
      key: 'wasc', 
      value: Array.join(finding.wasc, ', ') 
    });
  }
  if (finding.input_name && String.length(finding.input_name) > 0) {
    Array.push(custom_fields, { 
      key: 'input_name', 
      value: finding.input_name 
    });
  }
  if (finding.input_type && String.length(finding.input_type) > 0) {
    Array.push(custom_fields, { 
      key: 'input_type', 
      value: finding.input_type 
    });
  }

  let likelihood = 1;
  const cvssScore = finding.cvssv4 !== null && finding.cvssv4 !== undefined ? finding.cvssv4
                  : finding.cvssv3 !== null && finding.cvssv3 !== undefined ? finding.cvssv3
                  : finding.cvss   !== null && finding.cvss   !== undefined ? finding.cvss
                  : null;
  if (cvssScore !== null) {
    likelihood = Math.round(cvssScore);
    if (likelihood < 1) {
      likelihood = 1;
    }
    if (likelihood > 10) {
      likelihood = 10;
    }
  }

  const vuln = {
    projectId:                  data.jsonBody?.project_id,
    title:                      title,
    priority:                   priority,
    description:                finding.description || finding.synopsis || '',
    attack_scenario:            '',
    steps_to_reproduce:         buildWASSteps(finding),
    remediation_recommendation: remediation,
    likelihood_of_exploitation: likelihood,
    affected_assets:            [{ assetName: assetName }],
    tags:                       tags,
    custom_fields:              custom_fields,
    is_zeroday:                 false,
    import_source:              'Tenable WAS',
    import_source_id:           'WAS-' + pluginId,
    created:                    scanStartedAt,
    is_visible:                 true
  };

  if (secrets?.import_to_library) {
    vuln.import_to_library = secrets.import_to_library;
  }
  return vuln;
}

// WAS evidence note
function buildWASNote(finding) {
  const parts = [];
  const uri = finding.uri || '';
  if (String.length(uri) > 0) {
    Array.push(parts, '[' + uri + ']');
  }
  if (finding.output && String.length(finding.output) > 0) {
    Array.push(parts, finding.output);
  }
  if (finding.proof && String.length(finding.proof) > 0) {
    Array.push(parts, 'Proof: ' + finding.proof);
  }
  if (finding.request_headers && String.length(finding.request_headers) > 0) {
    Array.push(parts, 'Request Headers:\n' + finding.request_headers);
  }
  if (finding.response_headers && String.length(finding.response_headers) > 0) {
    Array.push(parts, 'Response Headers:\n' + finding.response_headers);
  }
  if (finding.payload && String.length(finding.payload) > 0) {
    Array.push(parts, 'Payload: ' + finding.payload);
  }
  if (Array.length(parts) > 0) {
    return { 
      note: Array.join(parts, '\n'), 
      type: 'PLAINTEXT' 
    };
  }
  return null;
}

// Steps-to-reproduce
function buildVMSteps(record) {
  const parts = [];
  if (record['OS'] && String.length(record['OS']) > 0) {
    Array.push(parts, 'OS: ' + record['OS']);
  }
  if (record['Port'] && record['Port'] !== '0') {
    let portLine = 'Port: ' + record['Port'];
    if (record['Protocol'] && String.length(record['Protocol']) > 0) {
      portLine = portLine + '/' + record['Protocol'];
    }
    Array.push(parts, portLine);
  }
  return Array.join(parts, '\n');
}

function buildWASSteps(finding) {
  const parts = [];
  if (finding.uri && String.length(finding.uri) > 0) {
    Array.push(parts, 'URL: ' + finding.uri);
  }
  if (finding.input_name && String.length(finding.input_name) > 0) {
    let inputLine = 'Input: ' + finding.input_name;
    if (finding.input_type && String.length(finding.input_type) > 0) {
      inputLine = inputLine + ' (' + finding.input_type + ')';
    }
    Array.push(parts, inputLine);
  }
  return Array.join(parts, '\n');
}

// ── Priority helpers ──
function mapPriorityVM(risk) {
  if (risk === 'Critical') {
    return 'Critical';
  }
  else if (risk === 'High') {
    return 'High';
  }
  else if (risk === 'Medium') {
    return 'Medium';
  }
  else if (risk === 'Low') {
    return 'Low';
  }
  return 'Info';
}

function mapPriorityWAS(risk_factor) {
  if (risk_factor === 'critical') {
    return 'Critical';
  }
  else if (risk_factor === 'high') {
    return 'High';
  }
  else if (risk_factor === 'medium') {
    return 'Medium';
  }
  else if (risk_factor === 'low') {
    return 'Low';
  }
  return 'Info';
}
```

**Action 2 - Import Vulnerabilities**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!data?.vulns_to_import || data.vulns_to_import && Array.length(data.vulns_to_import) === 0) {
  return {
    decision: { 
      status: 'next', 
      message: 'No vulnerabilities to import. Skipping.' 
    },
    data: data
  };
}

const vuln = data.vulns_to_import[0];
const remaining = Array.length(data.vulns_to_import) - 1;

if (secrets.logging_level === 'debug') {
  Logger.debug('Importing grouped vulnerability: ' + (vuln.title || 'Untitled') + ' (' + Array.length(vuln.affected_assets) + ' asset(s)). ' + remaining + ' remaining.');
}

return {
  decision: {
    status: 'continue',
    message: 'Importing vulnerability (' + (data.imported_count + 1) + '/' + data.total_count + '): ' + (vuln.title || 'Untitled')
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability',
    body: vuln
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Import error response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error importing vulnerability. Status code: ' + (response?.statusCode || 'unknown') + '. Imported so far: ' + (data?.imported_count || 0) + '/' + (data?.total_count || 0)
    }
  };
}

if (data?.vulns_to_import) {
  Array.splice(data.vulns_to_import, 0, 1);
}
if (data?.imported_count) {
  data.imported_count = (data.imported_count || 0) + 1;
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability imported successfully. Progress: ' + data?.imported_count + '/' + data?.total_count);
}

if (data?.vulns_to_import && Array.length(data.vulns_to_import) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Imported (' + data?.imported_count + '/' + data?.total_count + '). Processing next vulnerability.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'finish',
    message: 'All grouped vulnerabilities imported successfully. Total: ' + data?.imported_count
  },
  data: data
};
```


# Qualys

<figure><img src="/files/qqbeVvuQCT1s7e18Nbed" alt=""><figcaption></figcaption></figure>

## Qualys WAS - Initiate Scan \[Step 1 of 5]

<figure><img src="/files/0udbibnIN8gVNdHFQrLC" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a Web Application scan of the assets on the project scope in Qualys when a user clicks on an [Action](https://support.attackforge.com/attackforge-enterprise/actions) in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Action**:&#x20;
  * Entities: Projects and Project
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * fallback\_protocol - when an asset name doesn't have a protocol, set a default protocol e.g. https\://
  * qualys\_vm\_launch\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys WAS - Create Web Object and Launch Scan \[Step 2 of 5\]](#qualys-was-create-web-object-and-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Project(s)**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/project/{{project\_id}}
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.project) {
  return {
    decision:{
      status: 'next',
      message: 'Entity: project, skipping to next action.'
    },
    data: {
      projects: [
        {
          project_id: data.project.project_id,
          project_name: data.project.project_name,
          assets: data.project.project_scope
        }
      ]
    }
  };
}
else if (data?.projects){
  const projects = data.projects;

  // Initialise counter
  if (!data.counter) {
    data.counter = 0;
  }

  if (Array.isArray(projects)
    && Array.length(projects) > 0
    && projects?[data.counter]?.project_id
  ){
    const project_id = projects[data.counter].project_id;

    if (!project_id){
      if (secrets.logging_level === 'debug'){
        Logger.debug('Current project: ', projects[data.counter]);
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: project_id not found in current project. Please check the log.'
        }
      };
    }

    return {
      decision: {
        status: 'continue',
        message: 'Fetching project: ' + project_id
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/project/' + project_id
      },
      data: {
        projects: projects,
        counter: data.counter
      }
    };
  }
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('projects: ', JSON.stringify(projects));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error while validating "projects" object. Please check the log.'
      }
    };
  }
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.projects and/or data.project'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (data?.counter === undefined) {
  return {
    decision:{
      status: 'abort',
      message: 'data.counter missing'
    }
  };
}

const project = response.jsonBody?.project;

if (!project) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Project not found');
    Logger.debug('jsonBody: ', JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No projects matched with ' + data.projects[data.counter].project_id
    }
  };
}

// Initialise the array
data.projects[data.counter].assets = [];

if (project?.project_scope) {
  data.projects[data.counter].assets = project.project_scope;
  data.projects[data.counter].project_name = project.project_name;
}

if (Array.length(data.projects) > (data.counter + 1)) {
  data.counter = data.counter + 1;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next project.'
    },
    data: {
      projects: data.projects,
      counter: data.counter
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Fetched project scope for all projects. ', 'Projects: ', JSON.stringify(data.projects));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Fetched project scope for all projects, continuing to next step.'
    },
    data: {
      projects: data.projects
    }
  };
}
```

**Action 2 - Create Web App and Launch WAS Scan**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_vm\_launch\_scan\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let project_id;
let project_name;
let project_scope_assets;

// Project
if (data?.project) {
  if (data.project.project_id) {
    project_id = data.project.project_id;
  }
  if (data.project.project_scope) {
    project_scope_assets = data.project.project_scope;
  }
  if (data.project.project_name) {
    project_name = data.project.project_name;
  }
}

// Projects
if (data?.projects && Array.length(data.projects) > 0) {
  const curr_project = data.projects[0];
  if (curr_project?.assets && curr_project?.project_id && curr_project?.project_name) {
    project_scope_assets = curr_project.assets;
    project_id = curr_project.project_id;
    project_name = curr_project.project_name;
  }
}

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id is missing'
    }
  };
}
if (!project_scope_assets) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_scope_assets is missing'
    }
  };
}
if (!project_name){
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_name is missing'
    }
  };
}

// Normalizes assets to canonical WAS URLs. Assets without a protocol default to https://.
const valid_project_scope_assets = [];

if (Array.isArray(project_scope_assets)) {
  for (let x = 0; x < Array.length(project_scope_assets); x++) {
    const url = parseAssetToURL(project_scope_assets[x]);
    if (url) {
      if (secrets.logging_level === 'debug') {
        Logger.debug('valid_asset: ', url);
      }
      Array.push(valid_project_scope_assets, url);
    } 
    else {
      if (secrets.logging_level === 'debug') {
        Logger.debug('invalid_asset: ', project_scope_assets[x]);
      }
    }
  }
}

if (Array.length(valid_project_scope_assets) === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'No valid IP or hostname assets found in project scope after filtering.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Assets: ', JSON.stringify(valid_project_scope_assets));
}

return {
  decision: {
    status: 'continue',
    message: 'Sending project and assets to Qualys WAS Scan Flow: ' + secrets.qualys_vm_launch_scan_flow_trigger_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_vm_launch_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      project_name: project_name,
      assets: valid_project_scope_assets
    }
  }
};

function isValidHostname(str) {
  if (str.length === 0 || str.length > 253) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length === 0 || label.length > 63) return false;
      if (label[0] === '-' || label[label.length - 1] === '-') return false;
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) return false;
    }
  }
  return true;
}

function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length > 1 && label[0] === '0') return false;
      const num = Number.parseInt(label);
      if (!(num >= 0 && num <= 255)) return false;
    }
  }
  return true;
}

// READ:
// Accepts hostname, IPv4, host:port, host/path, or full http(s):// URL.
// Assets without an explicit protocol default to https://. Please update this as needed in "secret.fallback_protocol"
// Returns null if the host portion is not a valid hostname or IPv4.
function parseAssetToURL(raw) {
  const str = String.trim(raw);
  if (str.length === 0) return null;

  let protocol = secrets.fallback_protocol || 'https';
  let rest = str;

  if (String.includes(str, '://')) {
    const parts = String.split(str, '://');
    const proto = parts[0];
    if (proto !== 'http' && proto !== 'https') return null;
    protocol = proto;
    rest = parts[1];
  }

  const slashParts = String.split(rest, '/');
  let hostPort = slashParts[0];
  let path = '';
  for (let i = 1; i < Array.length(slashParts); i++) {
    path = path + '/' + slashParts[i];
  }

  let host = hostPort;
  let portStr = '';
  const portMatch = String.match(hostPort, m/^(.+):(\d+)$/);
  if (portMatch) {
    host = portMatch[1];
    portStr = portMatch[2];
    const portNum = Number.parseInt(portStr);
    if (!(portNum >= 1 && portNum <= 65535)) return null;
  }

  if (!isValidHostname(host) && !isValidIPv4(host)) return null;

  return protocol + '://' + host + (portStr ? ':' + portStr : '') + path;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submitting project to secrets.qualys_was_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submitting project to Qualys WAS Scan Flow. Status: ' + (response?.statusCode || 'unknown') + '. Please check the log.'
    }
  };
}
if (data?.projects) {
  Array.shift(data.projects);
  if (Array.length(data.projects) > 0) {
    return {
      decision: {
        status: 'repeat',
        message: 'Sending next project...',
        delay: 1000
      },
      data: {
        projects: data.projects
      }
    };
  }
  else {
    return {
      decision: {
        status: 'finish',
        message: 'Completed sending all projects to Qualys WAS Scan Flow.'
      }
    };
  }
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed sending project to Qualys WAS Scan Flow.'
    }
  };
}
```

## Qualys VM - Initiate Project Scope Scan \[Step 1 of 5]

<figure><img src="/files/8kBWloQFsu9AE5peZ3ER" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a VM scan of the assets on the project scope in Qualys when a user clicks on an [Action](https://support.attackforge.com/attackforge-enterprise/actions) in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Action**:&#x20;
  * Entities: Projects and Project
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * qualys\_vm\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM - Launch Scan \[Step 2 of 5\]](#qualys-vm-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Project(s)**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/project/{{project\_id}}
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.project) {
  return {
    decision:{
      status: 'next',
      message: 'Entity: project, skipping to next action.'
    },
    data: {
      projects: [
        {
          project_id: data.project.project_id,
          project_scope: data.project.project_scope,
          project_name: data.project.project_name
        }
      ]
    }
  };
}
else if (data?.projects){
  const projects = data.projects;

  // Initialise counter
  if (!data.counter){
    data.counter = 0;
  }

  if (Array.isArray(projects) 
    && Array.length(projects) > 0 
    && projects?[data.counter]?.project_id
  ){
    const project_id = projects[data.counter].project_id;

    if (!project_id) {
      if (secrets.logging_level === 'debug'){
        Logger.debug('Current project: ', projects[data.counter]);
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: project_id not found in current project. Please check the log.'
        }
      };
    }

    return {
      decision: {
        status: 'continue',
        message: 'Fetching project: ' + project_id
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/project/' + project_id
      },
      data: {
        projects: projects,
        counter: data.counter
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('projects: ', JSON.stringify(projects));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error while validating "projects" object. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.projects and/or data.project'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (data?.counter === undefined) {
  return {
    decision: {
      status: 'abort',
      message: 'data.counter missing'
    }
  };
}

const project = response.jsonBody?.project;

if (!project) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Project not found');
    Logger.debug('jsonBody: ', JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No projects matched with ' + data.projects[data.counter].project_id
    }
  };
}

data.projects[data.counter].project_scope = [];

if (project?.project_scope) {
  data.projects[data.counter].project_scope = project.project_scope;
  data.projects[data.counter].project_name = project.project_name;
}

if (Array.length(data.projects) > (data.counter + 1)) {
  data.counter = data.counter + 1;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next project.'
    },
    data: {
      projects: data.projects,
      counter: data.counter
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Fetched project scope for all projects. ', 'Projects: ', JSON.stringify(data.projects));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Fetched project scope for all projects, continuing to next step.'
    },
    data: {
      projects: data.projects
    }
  };
}
```

**Action 2 - Trigger Qualys VM Scan Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_vm\_scan\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
const projects = data?.projects;

if (!Array.isArray(projects) || Array.length(projects) === 0) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('projects: ', JSON.stringify(projects));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.projects is missing or empty. Please check the log.'
    }
  };
}

const project = projects?[0];
const project_id = project?.project_id;
const project_name = project?.project_name;

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: the next project is missing project_id.'
    }
  };
}

const valid_project_scope_assets = buildValidAssets(project?.project_scope);

if (secrets.logging_level === 'debug') {
  Logger.debug('Triggering Qualys VM scan for project ' + project_id, JSON.stringify(valid_project_scope_assets));
}

return {
  decision: {
    status: 'continue',
    message: 'Triggering Qualys VM scan for project: ' + project_id
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_vm_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      project_name: project_name,
      assets: valid_project_scope_assets
    }
  },
  data: {
    projects: projects
  }
};

// Validates a DNS hostname: 1-253 chars, dot-separated labels of 1-63 chars,
// alphanumeric/hyphen only, no leading/trailing hyphen on a label.
function isValidHostname(str) {
  if (String.length(str) === 0 || String.length(str) > 253) { return false; }
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < Array.length(labels); x++) {
      const label = labels[x];
      if (String.length(label) === 0 || String.length(label) > 63) { return false; }
      if (label[0] === '-' || label[String.length(label) - 1] === '-') { return false; }
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) { return false; }
    }
  }
  return true;
}

// Validates a dotted-quad IPv4 address with each octet in 0-255 and no leading zeros.
function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { return false; }
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < Array.length(labels); x++) {
      const label = labels[x];
      if (String.length(label) > 1 && label[0] === '0') { return false; }
      const num = Number.parseInt(label);
      if (!(num >= 0 && num <= 255)) { return false; }
    }
  }
  return true;
}

// Extracts the bare host from a raw scope asset for a VM/VMDR scan.
// Qualys VM targets are host assets addressed by IP or FQDN — NOT URLs — so any
// protocol prefix, path or port is stripped and only the host is kept.
// Accepts hostname, IPv4, host:port, host/path, or a full http(s):// URL.
// Returns null if the host portion is not a valid hostname or IPv4.
function parseAssetToHost(raw) {
  const str = String.trim(raw);
  if (String.length(str) === 0) { return null; }

  let rest = str;

  // Drop any protocol prefix — VM targets carry no http(s):// scheme.
  if (String.includes(rest, '://')) {
    const parts = String.split(rest, '://');
    rest = parts[1];
  }

  // Drop any path component — keep only the host[:port] portion.
  const slashParts = String.split(rest, '/');
  let hostPort = slashParts[0];

  // Drop any port — host assets are addressed by IP/FQDN only.
  let host = hostPort;
  const portMatch = String.match(hostPort, m/^(.+):(\d+)$/);
  if (portMatch && portMatch[1] && portMatch[2]) {
    host = portMatch[1];
  }

  if (!isValidHostname(host) && !isValidIPv4(host)) { return null; }

  return host;
}

// Normalises a project_scope array into the list of valid VM host assets
// (bare IP / FQDN, no protocol). Order is preserved; duplicates are kept.
function buildValidAssets(scope) {
  const assets = [];
  if (!Array.isArray(scope)) {
    return assets;
  }
  for (let i = 0; i < Array.length(scope); i++) {
    const host = parseAssetToHost(scope[i]);
    if (host !== null && host !== undefined) {
      Array.push(assets, host);
    }
  }
  return assets;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submitting project to qualys_vm_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submitting project to Qualys VM Scan Flow. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}
if (data?.projects) {
  Array.shift(data.projects);
  if (Array.length(data.projects) > 0) {
    return {
      decision: {
        status: 'repeat',
        message: 'Sending next request...',
        delay: 1000
      },
      data: {
        projects: data.projects
      }
    };
  }
}

return {
  decision: {
    status: 'finish',
    message: 'Completed sending project to Qualys VM Scan Flow.',
  }
};
```

## Qualys WAS - Create Web Object and Launch Scan \[Step 2 of 5]

<figure><img src="/files/REoxNXow6VfIbHQxE5RG" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create and launch a Web Application scan in Qualys.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_profile\_id - the Qualys scan option profile ID to use.
  * logging\_level - set to "debug" for additional logging

**Action 1 - Search Existing Web Apps**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/search/was/webapp
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/html
* **Request Script**:

```javascript
if (!data?.jsonBody && !data.assets){
  const err_msg = "Error while receiving request. No data.jsonBody or data.assets exists.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const project_id = data.jsonBody?.project_id ?? data.project_id;
const project_name = data.jsonBody?.project_name ?? data.project_name;
const assets = data?.jsonBody?.assets ?? data.assets;

if (!project_id || !project_name || !assets) {
  const err_msg = "Error while receiving request. project_id, project_name, assets must all exist in jsonBody.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

// Search every asset and compare the list of returned assets [repeat]
if (Array.isArray(assets) && assets[0]) {
  const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');
  const xml_body = "<ServiceRequest>"
  + "<filters>"
  + "<Criteria field='url' operator='EQUALS'>" + assets[0] + "</Criteria>"
  + "</filters>"
  + "</ServiceRequest>";

  return {
    decision: {
      status: 'continue',
      message: 'Searching web app: ' + assets[0] + ' from the Qualys environment.',
    },
    request: {
      url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/search/was/webapp',
      body: xml_body,
      headers: {
        Authorization: qualys_auth_base64
      }
    },
    data: {
      project_id: project_id,
      project_name: project_name,
      assets: assets,
      webapp_ids: data?.webapp_ids ?? [],
      assets_to_be_created: data?.assets_to_be_created ?? []
    }
  };
} 
else {
  const err_msg = "Error when attempting to create web app request. assets[0] does not exist.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  const err_msg = "HTTP error returned from Qualys while searching Web App. HTTP Status: " + response?.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let status;
if (xml_response) {
  status = (Array.find(xml_response, findStatus))?.responseCode?[0]?['#text'];
}
if (status !== 'SUCCESS'){
  const err_msg = "Error on creating web app on Qualys." + " status :" + status;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

const count = (Array.find(xml_response, getCount)).count[0]['#text'];
if (count === undefined){
  const err_msg = "Error: failed to retrieve count from the response. Count: " + count;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const webapp_ids = data?.webapp_ids ?? [];
const assets_to_be_created = data?.assets_to_be_created ?? [];
if (Number.parseInt(count) === 0) {
  Array.push(assets_to_be_created, data.assets[0]);
}
if (Number.parseInt(count) > 0) {
  const webapp_id = Array.find(xml_response, findWebAppId)?.data?[0]?.WebApp?[0]?.id?[0]?['#text'];
  Array.push(webapp_ids, webapp_id);
}
Array.shift(data.assets);
if (Array.length(data.assets) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Retreiving next assset id',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      assets: data.assets,
      assets_to_be_created: assets_to_be_created,
      webapp_ids: webapp_ids
    }
  };
}
return {
  decision: {
    status: 'continue',
    message: 'Successfully collected webapp_ids of Web Apps. Continuing to next action.',
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    assets_to_be_created: assets_to_be_created,
    webapp_ids: webapp_ids
  }
};

function findStatus (obj){
  if (Object.keys(obj)[0] === 'responseCode'){
    return obj;
  }
}
function getCount(obj){
  if (Object.keys(obj)[0] === 'count'){
    return obj;
  }
}
function findWebAppId (obj){
  if (Object.keys(obj)[0] === 'data'){
    return obj?.data?[0]?.WebApp?[0]?.id?[0]?['#text'];
  }
}
```

**Action 2 - Create Web Object**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/create/was/webapp
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/html
* **Request Script**:

```javascript
if (!data?.assets_to_be_created){
  data.assets_to_be_created = [];
}

if (data.assets_to_be_created && Array.length(data.assets_to_be_created) === 0) {
  if (data?.webapp_ids && Array.length(data?.webapp_ids) === 0) {
    return {
      decision: {
        status: 'finish',
        message: 'No assets were processed and no assets webapp_ids array: ' + data?.webapp_ids + '. Exiting the flow.'
      }
    };
  }
  return {
    decision:{
      status: 'next',
      message: 'No web app to be created on Qualys. Continuing to Launching Scan.'
    }
  };
}

const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');
let xml_webapp = "";
let assets_to_be_created;

if (Array.isArray(data.assets_to_be_created)){
  assets_to_be_created = data.assets_to_be_created;
}
if (assets_to_be_created[0]){
  xml_webapp = xml_webapp + "<name><![CDATA[" + assets_to_be_created[0] + "]]></name>" + "<url><![CDATA[" + assets_to_be_created[0] + "]]></url>";
} 
else {
  const err_msg = "Error processing data object. assets_to_be_created[0] is: " + assets_to_be_created?[0];
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Create Web Object- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_body = "<ServiceRequest>"
+ "    <data>"
+ "        <WebApp>"
+ xml_webapp
+ "        </WebApp>"
+ "    </data>"
+ "</ServiceRequest>";

return {
  decision: {
    status: 'continue',
    message: 'Requesting Create Web App on Qualys WAS.',
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/create/was/webapp',
    body: xml_body,
    headers: {
      Authorization: qualys_auth_base64
    }
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    assets_to_be_created: assets_to_be_created,
    webapp_ids: data?.webapp_ids
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  const err_msg = "Error while creating web app on Qualys." + " Response :" + response.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      flow_status: {
        step: 'Create Web Object- Request Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let status;
if (xml_response) {
  status = (Array.find(xml_response, findStatus))?.responseCode?[0]?['#text'];
}
if (status !== 'SUCCESS'){
  const err_msg = "Error on creating web app on Qualys." + " status :" + status;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      flow_status: {
        step: 'Create Web Object- Request Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

const webapp_id = Array.find(xml_response, findWebAppId)?.data?[0]?.WebApp?[0]?.id?[0]?['#text'];
if (webapp_id){
  Array.push(data.webapp_ids, webapp_id);
}

Array.shift(data.assets_to_be_created);
if (Array.length(data.assets_to_be_created) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Repeating',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      assets_to_be_created: data.assets_to_be_created,
      webapp_ids: data.webapp_ids || []
    }
  };
} 
else {
  if (Array.length(data.webapp_ids) === 0) {
    const err_msg = "Error: No assets were created for this project. webapp_ids: " + JSON.stringify(data.webapp_ids);
    return {
      decision: {
        status: 'next',
        message: err_msg,
      },
      data: {
        project_id: data?.project_id,
        project_name: data?.project_name,
        flow_status: {
          step: 'Create Web Object [repeat]- Response Script',
          status: 'VALIDATION_ERROR',
          message: err_msg
        }
      }
    };
  }
  return {
    decision: {
      status: 'continue',
      message: 'No more asset to create, continuing to next action',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      webapp_ids: data?.webapp_ids
    }
  };
}

function findStatus (obj){
  if (Object.keys(obj)[0] === 'responseCode'){
    return obj;
  }
}
function findWebAppId (obj){
  if (Object.keys(obj)[0] === 'data'){
    return obj;
  }
}
```

**Action 3 - Launch Scan**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/launch/was/wasscan
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/html
* **Request Script**:

```javascript
if (!data?.webapp_ids){
  const err_msg = "Data is missing webapp_ids. webapp_ids:" + data?.webapp_ids;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const date = Date.format(Date.datetime('now'), 'default');
const scan_name =  "Discover Scan on: " + data.project_name + " " + date;

function scanXmlBodyGenerator (arr){
  let multiscan = false;
  if (!Array.isArray(arr) || Array.length(arr) === 0) return;
  const xml_upper = "<ServiceRequest><data><WasScan><name>" + scan_name + "</name><type>DISCOVERY</type>";
  const xml_lower = "<profile><id>" + secrets.qualys_profile_id + "</id></profile></WasScan></data></ServiceRequest>";

  let xml_middle = "";

  if (Array.length(arr) === 1) {
    // Single Target
    xml_middle = "<target>"
      + "<webApp><id>" + arr[0] + "</id></webApp>"
      + "<webAppAuthRecord><isDefault>true</isDefault></webAppAuthRecord>"
      + "<scannerAppliance><type>EXTERNAL</type></scannerAppliance>"
      + "</target>";
  }

  if (Array.length(arr) > 1) {
    // Multi Target - build <set> by looping
    multiscan = true;
    let set_xml = "";
    for (let i = 0; i < Array.length(arr); i++) {
      set_xml = set_xml + "<WebApp><id>" + arr[i] + "</id></WebApp>";
    }
    xml_middle = "<target>"
      + "<scannerAppliance><type>EXTERNAL</type></scannerAppliance>"
      + "<webApps><set>" + set_xml + "</set></webApps>"
      + "<profileOption>DEFAULT</profileOption>"
      + "</target>";
  }

  const xml_body = xml_upper + xml_middle + xml_lower;
  return {xml_body: xml_body, multiscan: multiscan};
}

const scanResult = scanXmlBodyGenerator(data.webapp_ids);
const xml_body = scanResult ? scanResult.xml_body : undefined;

if (!xml_body) {
  const err_msg = "xml_body generation has been unsuccessful. xml_body:" + xml_body;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');

return {
  decision: {
    status: 'continue',
    message: 'Proceeding to launch scan ' + scan_name,
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/launch/was/wasscan',
    body: xml_body,
    headers: {
      Authorization: qualys_auth_base64
    }
  },
  data: {
    project_name: data.project_name,
    project_id: data.project_id,
    multiscan: scanResult.multiscan,
    scan_time: date
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  const err_msg = "HTTP error returned from Qualys while Launching Web App Scan. HTTP Status: " + response?.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let status;
if (xml_response) {
  status = (Array.find(xml_response, findStatus))?.responseCode?[0]?['#text'];
}
if (status !== 'SUCCESS') {
  const err_msg = "Error on Launching web app scan on Qualys." + " status :" + status;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const scanIdObj = Array.find(xml_response, findScanId);
const scan_id = scanIdObj ? scanIdObj.data[0].WasScan[0].id[0]['#text'] : undefined;
if (secrets.logging_level === 'debug') {
  Logger.debug('scan_id: ', JSON.stringify(scan_id));
}

return {
  decision: {
    status: 'continue',
    message: 'Qualys WAS Scan launched',
  },
  data: {
    project_name: data.project_name,
    project_id: data.project_id,
    scan_id: scan_id,
    multiscan: data.multiscan,
    scan_time: data.scan_time
  },
};

function findStatus(obj) {
  if (Object.keys(obj)[0] === 'responseCode') {
    return obj;
  }
}
function findScanId(obj) {
  if (Object.keys(obj)[0] === 'data') {
    return obj;
  }
}
```

**Action 4 - Update Project with Scan Id**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!data?.project_id || !data.scan_id || data.multiscan === undefined) {
  const err_msg = "multiscan, project_id and scan_id must be present in data.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Update Project with Scan Id- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Update project with new was scan_id',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: 'qualys_was_active_scan',
          value: String.from(data.scan_id)
        },
        {
          key: 'qualys_was_multiscan',
          value: String.from(data.multiscan)
        },
        {
          key: 'qualys_was_scan_time',
          value: String.from(data.scan_time)
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug') {
    Logger.debug("Error while updating project with new Qualys WAS scan id");
  }
  const err_msg = "Error while updating project with new Qualys WAS scan id. HTTP Status: " + response?.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Update Project with Scan Id- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
} 
else {
  return {
    decision:{
      status: 'finish',
      message: 'Successfully updated project with new qualys WAS scan id.'
    }
  };
}
```

## Qualys VM - Launch Scan \[Step 2 of 5]

<figure><img src="/files/qOYdYEi1Ax4Ky8GQ4vRM" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create and launch a VM scan in Qualys.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_option\_id - the Qualys scan option profile ID to use.
  * scan\_name\_prefix - optional prefix for the scan title (defaults to "AttackForge Qualys VM Scan").
  * logging\_level - set to "debug" for additional logging

**Action 1 - Launch Qualys VM Scan**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/api/2.0/fo/scan/
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
const curr_time = Date.datetime('now');
const safe_time = String.split(String.replaceAll(String.replaceAll(curr_time, ':', '-'), ' ', '+'), '.')[0];
const scan_name_prefix = secrets.scan_name_prefix || 'AttackForge Qualys VM Scan';

const project_id = data.jsonBody?.project_id;
const project_name = data?.jsonBody?.project_name;
const project_scope_assets = data.jsonBody?.assets;

if (!project_id) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('project_id: ', project_id);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id is missing. Please check the log.'
    }
  };
}
if (!project_name) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('project_name: ', project_name);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_name is missing. Please check the log.'
    }
  };
}
if (!project_scope_assets || !Array.isArray(project_scope_assets)) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('project_scope_assets: ', JSON.stringify(project_scope_assets));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_scope_assets must be a non-empty array. Please check the log.'
    }
  };
}
if (!secrets.qualys_auth || secrets.qualys_auth === undefined) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('qualys_auth: ', secrets.qualys_auth);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: qualys_auth must be registered in secrets. Please check the log.'
    }
  };
}

// Qualys uses ip= for IPs/ranges/CIDR and fqdn= for hostnames
const ip_targets = [];
const fqdn_targets = [];

for (let x = 0; x < Array.length(project_scope_assets); x++) {
  const asset = String.trim(project_scope_assets[x]);
  if (!asset) continue;
  if (isValidCIDR(asset) || isValidIPv4(asset)) {
    Array.push(ip_targets, asset);
  } 
  else if (isValidHostname(asset)) {
    Array.push(fqdn_targets, asset);
  } 
  else {
    if (secrets.logging_level === 'debug') {
      Logger.debug('invalid_asset: ', asset);
    }
  }
}

if (Array.length(ip_targets) === 0 && Array.length(fqdn_targets) === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'No valid targets found in assets array.'
    }
  };
}

const scan_title = String.replaceAll(scan_name_prefix + ' ' + project_name + ' ' + safe_time, ' ', '+');

// Build Qualys scan launch URL with correct parameter for each asset type
let qualys_url = 'https://' + secrets.qualys_api_url
  + '/api/3.0/fo/scan/?action=launch'
  + '&scan_title=' + scan_title
  + '&option_id=' + secrets.qualys_option_id;

const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');

if (Array.length(ip_targets) > 0) {
  qualys_url = qualys_url + '&ip=' + Array.join(ip_targets, ',');
}
if (Array.length(fqdn_targets) > 0) {
  qualys_url = qualys_url + '&fqdn=' + Array.join(fqdn_targets, ',') + "&iscanner_name=External";
}

if (secrets.logging_level === 'debug') {
  Logger.debug('ip_targets: ', JSON.stringify(ip_targets));
  Logger.debug('fqdn_targets: ', JSON.stringify(fqdn_targets));
  Logger.debug('qualys_url: ', qualys_url);
}

return {
  decision: {
    status: 'continue',
    message: 'Launching Qualys VM scan for project: ' + project_id,
  },
  request: {
    url: qualys_url,
    method: 'POST',
    headers: {
      Authorization: qualys_auth_base64
    }
  },
  data: {
    project_id: project_id
  }
};

function isValidHostname(str) {
  if (str.length === 0 || str.length > 253) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length === 0 || label.length > 63) return false;
      if (label[0] === '-' || label[label.length - 1] === '-') return false;
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) return false;
    }
  }
  return true;
}

function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length > 1 && label[0] === '0') return false;
      const num = Number.parseInt(label);
      if (!(num >= 0 && num <= 255)) return false;
    }
  }
  return true;
}

function isValidCIDR(str) {
  const match = String.match(str, m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/);
  if (!match) return false;
  const octets = [+match[1], +match[2], +match[3], +match[4]];
  const prefix = +match[5];
  let isValid = true;
  if (octets && Array.isArray(octets) && Array.length(octets) > 0) {
    for (let x = 0; x < octets.length; x++) {
      const o = octets[x];
      if (!(o >= 0 && o <= 255)) {
        isValid = false;
      }
    }
  }
  if (!(prefix >= 0 && prefix <= 32)) {
    isValid = false;
  }
  return isValid;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Qualys scan launch HTTP error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error launching Qualys VM scan. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

// Qualys returns HTTP 200 even for errors — check XML body for error codes.
// response.body contains the raw XML text.
const xml = response.body;

if (!xml) {
  return {
    decision: {
      status: 'abort',
      message: 'Empty response body from Qualys scan launch.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Qualys launch response: ', xml);
}

const res_parsed = XML.parse(xml);
let res_response;
if (res_parsed && res_parsed[0] && res_parsed[0]['SIMPLE_RETURN'][0]['RESPONSE']) {
  res_response = res_parsed[0]['SIMPLE_RETURN'][0]['RESPONSE'];
}

// Find the ITEM_LIST block (avoids fragile index like [2])
let item_list_entry;
if (res_response) {
  item_list_entry = Array.find(res_response, findItemList);
}

if (!item_list_entry) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('res_response: ', JSON.stringify(res_response));
  }
  return {
    decision: { status: 'abort', message: 'Error: ITEM_LIST not found in response. Please check the log.' }
  };
}

const items = item_list_entry['ITEM_LIST'];
const scan_meta = {};
for (let i = 0; i < Array.length(items); i++) {
  const fields = items[i]['ITEM'];
  const key = fields[0]['KEY'][0]['#text'];
  const value = fields[1]['VALUE'][0]['#text'];
  scan_meta[key] = value;
}

const scan_ref = String.split(scan_meta['REFERENCE'], 'scan/')[1];

if (!scan_ref) {
  return {
    decision: { 
      status: 'abort', 
      message: 'Error: scan_ref is missing from ITEM_LIST. Please check the log.' 
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Qualys VM scan launched. scan_ref: ' + scan_ref,
  },
  data: {
    project_id: data?.project_id,
    scan_ref: scan_ref
  }
};

function findItemList(obj){
  return Object.keys(obj)[0] === 'ITEM_LIST';
}
```

**Action 2 - Update Project with Scan Details**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!data?.scan_ref){
  return {
    decision:{
      status: 'abort',
      message: 'Error: scan_ref is required.'
    }
  };
}
if (!data?.project_id){
  return {
    decision:{
      status: 'abort',
      message: 'Error: project_id is required.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating "qualys_vm_active_scan" on project ' + data.project_id + '.',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: 'qualys_vm_active_scan',
          value: String.from(data.scan_ref)
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error while updating project with "qualys_vm_active_scan" custom_field'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated project with "qualys_vm_active_scan" custom_field'
  }
};
```

## Qualys VM & WAS - Find Active Scans to Poll Status \[Step 3 of 5]

<figure><img src="/files/eRK1sk1t1Dw3omti11Py" alt=""><figcaption></figcaption></figure>

The purpose of this example is to poll the scan status for any active Qualys scans, and for any completed scans - trigger a download of the vulnerabilities.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Schedule**:&#x20;
  * Every hour
    * CRON String: 0 0/1 \* \* \*
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * qualys\_vm\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM - Poll and Fetch Findings \[Step 4 of 5\]](#qualys-vm-poll-and-fetch-findings-step-4-of-5)
  * qualys\_was\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys WAS - Poll and Fetch Findings \[Step 4 of 5\]](#qualys-was-poll-and-fetch-findings-step-4-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Qualys Active Scan Projects**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/projects?order=created:desc\&limit=500
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!secrets.af_hostname){
  return {
    decision: {
      status: 'abort',
      message: 'af_hostname is required to request AttackForge projects.'
    }
  };
}

const limit = 50;
const skip = data?.skip ?? 0;

return {
  decision: {
    status: 'continue',
    message: 'Getting all projects with an active Qualys scan.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/projects?order=created:desc&skip='+skip+'&limit='+limit
  },
  data:{
    limit: limit,
    skip: skip,
    active_vm_scan_projects: data?.active_vm_scan_projects || [],
    active_was_scan_projects: data?.active_was_scan_projects || [],
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Please check the log.'
    }
  };
}

if (data && (!data?.active_vm_scan_projects || !data?.active_was_scan_projects)) {
  data.active_vm_scan_projects = data.active_vm_scan_projects || [];
  data.active_was_scan_projects = data.active_was_scan_projects || [];
}

const projects = response?.jsonBody?.projects;
const count = projects ? Array.length(projects) : 0;
let repeat = false;

if (count === data?.limit) {
  repeat = true;
}

if (!projects || Array.length(projects) === 0) {
  if (data?.active_vm_scan_projects 
    && Array.length(data.active_vm_scan_projects) === 0 
    && Array.length(data.active_was_scan_projects) === 0
  ) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('active_vm_scan_projects: ', JSON.stringify(data.active_vm_scan_projects));
      Logger.debug('active_was_scan_projects: ', JSON.stringify(data.active_was_scan_projects));
    }
    return {
      decision: {
        status: 'finish',
        message: 'No projects with an active Qualys VM or WAS scan found.'
      }
    };
  }
  return {
    decision: {
      status: 'continue',
      message: 'No new projects found - continuing to next action.'
    },
    data: {
      active_vm_scan_projects: data?.active_vm_scan_projects,
      active_was_scan_projects: data?.active_was_scan_projects,
    }
  };
}

for (let i = 0; i < Array.length(projects); i++) {
  const project = projects[i];
  const cfs = project.project_custom_fields;

  if (!cfs || Array.length(cfs) === 0) {
    continue;
  }

  const active_vm_scan_cf = Array.find(cfs, find_active_vm_scan);
  const active_was_scan_cf = Array.find(cfs, find_active_was_scan);

  if (active_vm_scan_cf?.value) {
    Array.push(data.active_vm_scan_projects, {
      project_id: project?.project_id,
      project_name: project?.project_name,
      scan_ref: active_vm_scan_cf?.value
    });
  }
  if (active_was_scan_cf?.value) {
    const was_multiscan = Array.find(cfs, find_was_multiscan);
    const was_scan_time = Array.find(cfs, find_was_scan_time);
    Array.push(data.active_was_scan_projects, {
      project_id: project?.project_id,
      project_name: project?.project_name,
      scan_ref: active_was_scan_cf?.value,
      multiscan: was_multiscan?.value,
      scan_time: was_scan_time?.value
    });
  }
}

if (repeat) {
  return {
    decision: {
      status: 'repeat',
      message: 'Requesting more projects. Currently fetched ' + data?.skip + ' projects.'
    },
    data: {
      limit: data.limit,
      skip: data.limit + data.skip,
      active_vm_scan_projects: data.active_vm_scan_projects,
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
}

if (Array.length(data.active_vm_scan_projects) === 0 
  && Array.length(data.active_was_scan_projects) === 0
) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('active_vm_scan_projects: ', JSON.stringify(data.active_vm_scan_projects));
    Logger.debug('active_was_scan_projects: ', JSON.stringify(data.active_was_scan_projects));
  }
  return {
    decision: {
      status: 'finish',
      message: 'No projects with an active Qualys VM or WAS scan found.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Found ' + Array.length(data.active_vm_scan_projects) + ' active VM scan project(s) and Found ' + Array.length(data.active_was_scan_projects) + ' active WAS scan project(s).'
  },
  data: {
    active_vm_scan_projects: data.active_vm_scan_projects,
    active_was_scan_projects: data.active_was_scan_projects,
  }
};

function find_active_vm_scan(field) {
  return field.key === 'qualys_vm_active_scan';
}
function find_active_was_scan(field) {
  return field.key === 'qualys_was_active_scan';
}
function find_was_multiscan(field) {
  return field.key === 'qualys_was_multiscan';
}
function find_was_scan_time(field) {
  return field.key === 'qualys_was_scan_time';
}
```

**Action 2 - Trigger Qualys VM Poll Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_vm\_poll\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.active_vm_scan_projects && Array.length(data.active_vm_scan_projects) > 0) {
  const curr_project = data.active_vm_scan_projects[0];

  if (!curr_project?.project_id || !curr_project?.project_name) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_id or project_name missing in current project. Please check the log.'
      }
    };
  }

  if (curr_project?.scan_ref === undefined) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: scan_ref missing in current project. Please check the log.'
      }
    };
  }

  return {
    decision: {
      status: 'continue',
      message: 'Triggering Qualys VM Poll Flow for project: ' + curr_project.project_id + '.'
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_vm_poll_flow_trigger_id,
      body: {
        project: {
          project_id: curr_project.project_id,
          project_name: curr_project.project_name,
          scan_ref: curr_project.scan_ref
        }
      }
    },
    data: {
      active_vm_scan_projects: data.active_vm_scan_projects,
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No VM Scans to poll, continuing to next action.'
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering poll flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error triggering Qualys VM Poll Flow. Please check the log.'
    }
  };
}

if (!data?.active_vm_scan_projects) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: Should not reach here.');
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.active_vm_scan_projects missing. Please check the log.'
    }
  };
}

Array.shift(data.active_vm_scan_projects);
if (Array.length(data.active_vm_scan_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Triggering next Qualys VM Poll Flow.',
      delay: 1000
    },
    data: {
      active_vm_scan_projects: data.active_vm_scan_projects,
      active_was_scan_projects: data.active_was_scan_projects
    }
  };
}
else {
  return {
    decision: {
      status: 'continue',
      message: 'Completed triggering Qualys VM Poll Flow for all active scan projects. Continuing to next action.'
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects
    }
  };
}
```

**Action 3 - Trigger Qualys WAS Poll Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_vm\_poll\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.active_was_scan_projects && Array.length(data.active_was_scan_projects) > 0) {
  const curr_project = data.active_was_scan_projects[0];

  if (!curr_project?.project_id 
    || !curr_project?.project_name 
    || !curr_project?.multiscan 
    || !curr_project?.scan_time
  ) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_id, project_name, multiscan, and scan_time must all be present in current project. Please check the log.'
      }
    };
  }

  if (curr_project?.scan_ref === undefined) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: scan_ref missing in current project. Please check the log.'
      }
    };
  }

  if (curr_project.scan_ref === ""){
    return {
      decision: {
        status: 'next',
        message: 'No WAS Scan to poll for project: ' + curr_project.project_id
      }
    };
  }

  return {
    decision: {
      status: 'continue',
      message: 'Triggering Qualys WAS Poll Flow for project: ' + curr_project.project_id + '.'
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_was_poll_flow_trigger_id,
      body: {
        project: {
          project_id: curr_project.project_id,
          project_name: curr_project.project_name,
          scan_ref: curr_project.scan_ref,
          multiscan: curr_project.multiscan,
          scan_time: curr_project.scan_time
        }
      }
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No WAS scans to poll, exiting the process.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering poll flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error triggering Qualys WAS Poll Flow. Please check the log.'
    }
  };
}

if (!data?.active_was_scan_projects) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: Should not reach here.');
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.active_was_scan_projects missing. Please check the log.'
    }
  };
}

Array.shift(data.active_was_scan_projects);
if (Array.length(data.active_was_scan_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Triggering next Qualys WAS Poll Flow.',
      delay: 1000
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed triggering Qualys WAS Poll Flow for all active scan projects.'
    }
  };
}
```

## Qualys WAS - Poll and Fetch Findings \[Step 4 of 5]

<figure><img src="/files/6RzWr7fn3bYVzypXE46i" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export the results of a Web Application scan in Qualys.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * current\_flow\_id - the Id for this Flow in AttackForge once it's created (used for error reporting in email)
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_import\_vuln\_trigger\_flow\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM & WAS - Import Vulnerabilities \[Step 5 of 5\]](#qualys-vm-and-was-import-vulnerabilities-step-5-of-5)
  * qualys\_web\_base\_url - your Qualys web console base URL e.g. *<https://qualysguard.qualys.com>*
  * admin\_email - the email address to send error and alert notifications
  * logging\_level - set to "debug" for additional logging

**Action 1 - Poll WAS Scan status**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/search/was/wasscan
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/xml
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
if (!secrets.qualys_auth){
  const err_msg = 'Error: qualys_auth missing from secrets.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: "N/A",
      project_name: "N/A",
      scan_ref: "N/A",
      flow_status: {
        step: 'Poll WAS Scan status - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const auth_header  = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

const project = data?.jsonBody?.project;
if (!project?.scan_ref || !project?.project_id || !project?.project_name) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('project: ', JSON.stringify(project));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: scan_ref, project_id, project_name must all be present in the project. Please check the log.'
    }
  };
}

let field;
let criteria_value;
let operator;
if (project?.multiscan === "true"){
  field = "name";
  criteria_value = project.project_name + ' ' + project.scan_time;
  operator = 'CONTAINS';
} 
else {
  field = "id";
  criteria_value = project.scan_ref;
  operator = 'EQUALS';
}

const xml = "<ServiceRequest><filters><Criteria field='" + field + "' operator='" + operator + "'>" + criteria_value + "</Criteria></filters></ServiceRequest>";

return {
  decision: {
    status: 'continue',
    message: 'Polling Qualys WAS scan status',
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/search/was/wasscan',
    body: xml,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: project.project_id,
    scan_ref: project.scan_ref,
    project_name: project.project_name,
    multiscan: project.multiscan
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  const err_msg = 'HTTP error returned from Qualys while polling for WAS scan status. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Poll WAS Scan Status - Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let responseCode;
if (xml_response) {
  responseCode = (Array.find(xml_response, findResponseCode))?.responseCode?[0]?['#text'];
}
if (responseCode !== 'SUCCESS'){
  const err_msg = 'Qualys WAS returned a non-SUCCESS responseCode while polling scan status: ' + responseCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Poll WAS Scan Status - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

// Get All WasScan — multiscan supported
const wasScans = (Array.find(xml_response, findData)).data;
const successful_was_scans   = [];
const unsuccessful_was_scans = [];

// WasScanStatus: skip still-in-progress scans
const processing_status = ['SUBMITTED', 'RUNNING'];

// WasScanResultStatus (consolidatedStatus) classification for FINISHED scans
const successful_consol_status = ['SUCCESSFUL', 'FINISHED'];
const partial_consol_status    = ['CANCELED_WITH_RESULTS', 'TIME_LIMIT_REACHED', 'TIME_LIMIT_EXCEEDED', 'MAX_LINKS_CRAWLED'];
const hard_fail_consol_status  = ['NO_HOST_ALIVE', 'NO_WEB_SERVICE', 'SERVICE_ERROR', 'SCAN_INTERNAL_ERROR', 'SCAN_RESULTS_INVALID', 'SCAN_NOT_LAUNCHED', 'ERROR'];
const soft_fail_consol_status  = ['SCANNER_NOT_AVAILABLE', 'CANCELED', 'CANCELING', 'DELETED'];

for (let i = 0; i < Array.length(wasScans); i++) {
  const status        = (Array.find(wasScans[i].WasScan, findstatus)).status[0]['#text'];
  const consol_status = (Array.find(wasScans[i].WasScan, findConsolStatus)).consolidatedStatus[0]['#text'];
  const launched_date = (Array.find(wasScans[i].WasScan, findLaunchedDate)).launchedDate[0]['#text'];
  const web_app_id    = (Array.find(wasScans[i].WasScan, findTarget)).target[0].webApp[0].id[0]['#text'];
  const scan_id       = wasScans[i].WasScan[0].id[0]['#text'];

  if (Array.includes(processing_status, status)) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Scan ' + scan_id + ' with status ' + status + ' is still in progress. Skipping.');
    }
    continue;
  }

  // ERROR and CANCELED are terminal by WasScanStatus alone — no consol_status needed
  if (status === 'ERROR') {
    Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'hard' });
    continue;
  }

  if (status === 'CANCELED') {
    Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'soft' });
    continue;
  }

  if (status === 'FINISHED') {
    // Success
    if (Array.includes(successful_consol_status, consol_status)) {
      Array.push(successful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, launched_date: launched_date, web_app_id: web_app_id, is_partial: false });
    }
    // Partial Success
    else if (Array.includes(partial_consol_status, consol_status)) {
      Array.push(successful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, launched_date: launched_date, web_app_id: web_app_id, is_partial: true });
    }
    // Hard Fail
    else if (Array.includes(hard_fail_consol_status, consol_status)) {
      Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'hard' });
    }
    // Soft Fail
    else if (Array.includes(soft_fail_consol_status, consol_status)) {
      Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'soft' });
    }
    // Unrecognised status
    else {
      Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'hard' });
    }
    continue;
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('successful_was_scans: ' + JSON.stringify(successful_was_scans));
  Logger.debug('unsuccessful_was_scans: ' + JSON.stringify(unsuccessful_was_scans));
}

if (Array.length(successful_was_scans) === 0 && Array.length(unsuccessful_was_scans) === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No scans are yet finished or failed. Exiting — will be re-polled.'
    }
  };
}

// All scans failed — build collective message listing every failure
if (Array.length(successful_was_scans) === 0 && Array.length(unsuccessful_was_scans) > 0) {
  const fail_summaries = [];
  for (let i = 0; i < Array.length(unsuccessful_was_scans); i++) {
    const f = unsuccessful_was_scans[i];
    Array.push(fail_summaries, 'Scan ' + f.scan_id + ': status=' + f.status + ' (consolidatedStatus=' + f.consolidated_status + ') aborted status: ' + f.consolidated_status + '.');
  }
  const err_msg = 'All WAS scans ended unsuccessfully. ' + Array.join(fail_summaries, ' ');
  return {
    decision: {
      status: 'next',
      message: 'All WAS scans ended unsuccessfully. Proceeding to email report action.'
    },
    data: {
      project_id:   data?.project_id,
      project_name: data?.project_name,
      scan_ref:     data?.scan_ref,
      unsuccessful_was_scans: unsuccessful_was_scans,
      flow_status: {
        step:    'Poll WAS Scan Status - Response Script',
        status:  unsuccessful_was_scans[0].status,
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully collected ' + Array.length(successful_was_scans) + ' finished scan(s). Continuing to find findings.'
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    successful_was_scans:   successful_was_scans,
    unsuccessful_was_scans: unsuccessful_was_scans
  }
};

function findResponseCode (obj){
  if (Object.keys(obj)[0] === 'responseCode'){
    return obj;
  }
}
function findData (obj){
  if (Object.keys(obj)[0] === 'data'){
    return obj;
  }
}
function findstatus (wasScan){
  if (Object.keys(wasScan)[0] === 'status'){
    return wasScan;
  }
}
function findConsolStatus (wasScan){
  if (Object.keys(wasScan)[0] === 'consolidatedStatus'){
    return wasScan;
  }
}
function findLaunchedDate (wasScan){
  if (Object.keys(wasScan)[0] === 'launchedDate'){
    return wasScan;
  }
}
function findTarget (wasScan){
  if (Object.keys(wasScan)[0] === 'target'){
    return wasScan;
  }
}
```

**Action 2 - Search Findings**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/search/was/finding
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/xml
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
if (!secrets.qualys_auth){
  const err_msg = 'Error: qualys_auth missing from secrets.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Search Findings - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const auth_header  = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status?.step + '. Proceeding to report action.'
    },
    data: data
  };
}

const curr_scan = data?.successful_was_scans?[0];

if (curr_scan) {
  const xml_body = "<ServiceRequest> <filters> <Criteria field='webApp.id' operator='EQUALS'>" + curr_scan.web_app_id + "</Criteria> <Criteria field='lastDetectedDate' operator='EQUALS'>" + curr_scan.launched_date + "</Criteria> <Criteria field='status' operator='NOT EQUALS'>FIXED</Criteria> </filters> </ServiceRequest>";

  return {
    decision: {
      status: 'continue',
      message: 'Requesting Qualys search findings on scan: ' + data.scan_ref,
    },
    request: {
      url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/search/was/finding',
      body: xml_body,
      headers: {
        Authorization: auth_header
      }
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      successful_was_scans:   data.successful_was_scans,
      unsuccessful_was_scans: data.unsuccessful_was_scans,
      final_result:           data.final_result || []
    },
  };
} 
else {
  const err_msg = 'Error: curr_scan not found. curr_scan: ' + data?.successful_was_scans?[0];
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Search Findings - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  const err_msg = 'HTTP error fetching WAS findings. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { status: 'next', message: err_msg },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: { step: 'Search Findings - Response Script', status: 'HTTP_ERROR', message: err_msg }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let responseCode;
if (xml_response) {
  responseCode = (Array.find(xml_response, findResponseCode))?.responseCode?[0]?['#text'];
}
if (!xml_response){
  const err_msg = 'Error: ServiceResponse does not exist in parsed XML body.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Search Findings - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
if (responseCode !== 'SUCCESS') {
  const err_msg = 'Qualys WAS findings search returned non-SUCCESS responseCode: ' + responseCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: { 
        step: 'Search Findings - Response Script', 
        status: 'VALIDATION_ERROR', 
        message: err_msg 
      }
    }
  };
}

const rawData     = Array.find(xml_response, findData);
const rawFindings = rawData ? rawData.data : null;

// Pre-populate webApp_groups from findings accumulated in previous repeats
const webApp_groups = {};
const accumulated = data.final_result || [];
for (let a = 0; a < Array.length(accumulated); a++) {
  const grp = accumulated[a];
  webApp_groups[grp.scan.scan_id] = grp;
}

if (rawFindings && Array.length(rawFindings) > 0) {
  for (let i = 0; i < Array.length(rawFindings); i++) {
    const f = rawFindings[i].Finding;

    const qid            = String.from(extractField(f, 'qid'));
    const name           = extractField(f, 'name');
    const severity       = extractField(f, 'severity');
    const type           = extractField(f, 'type');
    const firstDetected  = extractField(f, 'firstDetectedDate');
    const detectionScore = extractField(f, 'detectionScore');

    const webAppArr = extractFieldRaw(f, 'webApp');
    const webAppId  = String.from(extractField(webAppArr, 'id'));
    const webAppUrl = extractField(webAppArr, 'url');

    const status         = extractField(f, 'status') || '';
    const potential      = extractField(f, 'potential');
    const is_ignored     = extractField(f, 'isIgnored');
    const ignored_reason = extractField(f, 'ignoredReason') || '';
    const last_detected_at = extractField(f, 'lastDetectedDate') || '';

    if (status === 'FIXED') { 
      continue; 
    }

    const finding = {
      finding_id:       extractField(f, 'id'),
      qid:              qid,
      name:             name,
      risk_factor:      mapRiskFactor(severity),
      category:         type,
      uri:              webAppUrl,
      detection_score:  detectionScore,
      status:           status,
      potential:        potential,
      is_ignored:       is_ignored,
      ignored_reason:   ignored_reason,
      last_detected_at: last_detected_at,
      description:      '',
      solution:         '',
      cves:             [],
      cwe:              [],
      owasp:            [],
      cvss:             null,
      cvss_vector:      '',
      cvss3:            null,
      cvss3_vector:     '',
      evidence:         '',
      proof:            '',
      request_headers:  '',
      response_headers: '',
      payload:          '',
      input_name:       '',
      input_type:       '',
      group:            '',
      wasc:             '',
      times_detected:   null
    };

    if (!webApp_groups[webAppId]) {
      webApp_groups[webAppId] = {
        scan: {
          scan_id:    webAppId,
          target:     webAppUrl,
          started_at: firstDetected
        },
        findings: [finding]
      };
    } 
    else {
      Array.push(webApp_groups[webAppId].findings, finding);
    }
  }
}

// Shift the processed scan off the queue
Array.splice(data.successful_was_scans, 0, 1);

// Rebuild accumulated final_result
const final_result = [];
const keys = Object.keys(webApp_groups);
for (let k = 0; k < Array.length(keys); k++) {
  Array.push(final_result, webApp_groups[keys[k]]);
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Scans remaining: ' + Array.length(data.successful_was_scans) + '. Total findings so far: ' + Array.length(final_result));
}

// More scans to process — repeat the request with the next scan
if (Array.length(data.successful_was_scans) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Processed scan. ' + Array.length(data.successful_was_scans) + ' scan(s) remaining.'
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      successful_was_scans:   data.successful_was_scans,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      final_result:           final_result
    }
  };
}

// All scans processed — check if we have any findings at all
if (Array.length(final_result) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No findings returned from Qualys WAS.'
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      flow_status: { 
        step: 'Search Findings - Response Script', 
        status: 'NO_FINDINGS', 
        message: 'No findings returned from Qualys WAS Scan for scan: ' + data.scan_ref 
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Collected WAS findings across ' + Array.length(final_result) + ' web app(s).'
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    final_result:           final_result,
    scan_ref:               data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};

function extractField(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName][0]['#text'];
    }
  }
  return null;
}

function extractFieldRaw(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName];
    }
  }
  return null;
}

function mapRiskFactor(severity) {
  if (severity === 5) return 'critical';
  if (severity === 4) return 'high';
  if (severity === 3) return 'medium';
  if (severity === 2) return 'low';
  return 'info';
}

function findResponseCode(obj) { 
  if (Object.keys(obj)[0] === 'responseCode') return obj; 
}
function findData(obj) {
  if (Object.keys(obj)[0] === 'data') return obj;
}
```

**Action 3 - Get Findings**

* **Method**: GET
* **URL**: https\:///{{qualys\_api\_url}}/qps/rest/3.0/get/was/finding/{id}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.final_result) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id and final_result must be present in data.'
    }
  };
}

// Build enrichment queue on first call — one entry per finding_id
if (!data.enrichment_queue) {
  const queue = [];

  for (let r = 0; r < Array.length(data.final_result); r++) {
    const findings = data.final_result[r].findings;
    for (let f = 0; f < Array.length(findings); f++) {
      const finding = findings[f];
      if (finding.finding_id) {
        Array.push(queue, { 
          finding_id: finding.finding_id, 
          qid: finding.qid 
        });
      }
    }
  }

  data.enrichment_queue = queue;
  data.enrichment_total = Array.length(queue);
  data.enrichment_done  = 0;

  if (secrets.logging_level === 'debug') {
    Logger.debug('Built enrichment queue: ' + Array.length(queue) + ' findings to fetch.');
  }
}

if (Array.length(data.enrichment_queue) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'All WAS findings enriched. Proceeding to import.'
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      final_result:           data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

const current = data.enrichment_queue[0];
const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

return {
  decision: {
    status: 'continue',
    message: 'Fetching WAS finding detail for finding ID ' + current.finding_id + ' (QID: ' + current.qid + '). ' + (data.enrichment_done + 1) + '/' + data.enrichment_total + '.'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/get/was/finding/' + current.finding_id,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    final_result:           data.final_result,
    unsuccessful_was_scans: data.unsuccessful_was_scans || [],
    enrichment_queue:       data.enrichment_queue,
    enrichment_total:       data.enrichment_total,
    enrichment_done:        data.enrichment_done
  }
};
```

* **Response Script**:

```javascript
const current = data?.enrichment_queue?[0];

if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('HTTP error fetching WAS finding ' + current.finding_id + ': ' + response?.statusCode);
  }
  Array.splice(data.enrichment_queue, 0, 1);
  data.enrichment_done = data.enrichment_done + 1;

  if (Array.length(data.enrichment_queue) > 0) {
    return {
      decision: {
        status: 'repeat',
        message: 'HTTP error for finding ID ' + current.finding_id + '. Skipping. Remaining: ' + Array.length(data.enrichment_queue) + '.'
      },
      data: {
        project_id:             data?.project_id,
        project_name:           data?.project_name,
        scan_ref:               data?.scan_ref,
        final_result:           data?.final_result,
        unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
        enrichment_queue:       data?.enrichment_queue,
        enrichment_total:       data?.enrichment_total,
        enrichment_done:        data?.enrichment_done
      }
    };
  }
  return {
    decision: {
      status: 'next',
      message: 'Enrichment complete with some HTTP errors. Proceeding to import.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || []
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let responseCode;
if (xml_response) {
  responseCode = (Array.find(xml_response, findResponseCode))?.responseCode?[0]?['#text'];
}
if (!xml_response){
  const err_msg = 'Error: ServiceResponse does not exist in parsed XML body.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Get Findings - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
if (responseCode !== 'SUCCESS') {
  if (secrets.logging_level === 'debug') {
    Logger.debug('API error for finding ' + current.finding_id + ': responseCode=' + responseCode);
  }

  Array.splice(data.enrichment_queue, 0, 1);
  data.enrichment_done = data.enrichment_done + 1;

  if (Array.length(data.enrichment_queue) > 0) {
    return {
      decision: {
        status: 'repeat',
        message: 'API error for finding ID ' + current.finding_id + '. Skipping. Remaining: ' + Array.length(data.enrichment_queue) + '.'
      },
      data: {
        project_id:             data?.project_id,
        project_name:           data?.project_name,
        scan_ref:               data?.scan_ref,
        final_result:           data?.final_result,
        unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
        enrichment_queue:       data?.enrichment_queue,
        enrichment_total:       data?.enrichment_total,
        enrichment_done:        data?.enrichment_done
      }
    };
  }
  return {
    decision: {
      status: 'next',
      message: 'Enrichment complete with some API errors. Proceeding to import.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || []
    }
  };
}

const rawData   = Array.find(xml_response, findData);
const findingElement = rawData ? rawData.data[0].Finding : null;

if (!findingElement) {
  Array.splice(data.enrichment_queue, 0, 1);
  data.enrichment_done = data.enrichment_done + 1;
  if (Array.length(data.enrichment_queue) > 0) {
    return {
      decision: { status: 'repeat', message: 'No Finding element in response. Skipping.' },
      data: {
        project_id:             data?.project_id,
        project_name:           data?.project_name,
        scan_ref:               data?.scan_ref,
        final_result:           data?.final_result,
        unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
        enrichment_queue:       data?.enrichment_queue,
        enrichment_total:       data?.enrichment_total,
        enrichment_done:        data?.enrichment_done
      }
    };
  }
  return {
    decision: { status: 'next', message: 'Enrichment complete. Proceeding to import.' },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || []
    }
  };
}

// ── Parse shared fields (same for all findings with this QID) ──

const enrichedQid = String.from(extractField(findingElement, 'qid'));
const group       = extractField(findingElement, 'group') || '';

// CWE: <cwe><list><long>123</long></list></cwe>
const cwe = [];
const cweRaw  = extractFieldRaw(findingElement, 'cwe');
if (cweRaw) {
  const cweList = extractFieldRaw(cweRaw, 'list');
  if (cweList) {
    for (let c = 0; c < Array.length(cweList); c++) {
      if (cweList[c].long) {
        Array.push(cwe, cweList[c].long[0]['#text']);
      }
    }
  }
}

// OWASP: <owasp><list><OWASP><name/><code/><url/></OWASP></list></owasp>
// Step 5 expects [{ year, category }] — Qualys provides code (e.g. A01) without year
// Storing as { year: 'OWASP', category: code } so tag renders as OWASP-OWASP-A01
const owasp = [];
const owaspRaw = extractFieldRaw(findingElement, 'owasp');
if (owaspRaw) {
  const owaspList = extractFieldRaw(owaspRaw, 'list');
  if (owaspList) {
    for (let o = 0; o < Array.length(owaspList); o++) {
      if (owaspList[o].OWASP) {
        const owaspEntry = owaspList[o].OWASP;
        const code = extractField(owaspEntry, 'code') || '';
        const name = extractField(owaspEntry, 'name') || '';
        Array.push(owasp, { year: 'OWASP', category: code, name: name });
      }
    }
  }
}

// cvssV3: <cvssV3><base/><attackVector/></cvssV3>
const cvssV3Raw = extractFieldRaw(findingElement, 'cvssV3');
let cvss3        = null;
let cvss3_vector = '';
if (cvssV3Raw) {
  const base         = extractField(cvssV3Raw, 'base');
  const attackVector = extractField(cvssV3Raw, 'attackVector');
  if (base !== null)                        cvss3        = base;
  if (String.length(attackVector) > 0)      cvss3_vector = attackVector;
}

// Vulnerable parameter name
const input_name = extractField(findingElement, 'param') || '';

// Number of times the finding has been detected
const times_detected = extractField(findingElement, 'timesDetected');

// WASC stored as custom_field string
const wasc = [];
const wascRaw = extractFieldRaw(findingElement, 'wasc');
if (wascRaw) {
  const wascList = extractFieldRaw(wascRaw, 'list');
  if (wascList) {
    for (let w = 0; w < Array.length(wascList); w++) {
      if (wascList[w].WASC) {
        const wascEntry = wascList[w].WASC;
        const code = extractField(wascEntry, 'code') || '';
        const name = extractField(wascEntry, 'name') || '';
        Array.push(wasc, code + ' - ' + name);
      }
    }
  }
}

let uri             = '';
let payload         = '';
let request_headers = '';
let evidence        = '';

const resultListRaw = extractFieldRaw(findingElement, 'resultList');
if (resultListRaw) {
  const resultList = extractFieldRaw(resultListRaw, 'list');
  if (resultList && Array.length(resultList) > 0) {
    const firstResult = resultList[0].Result;
    if (firstResult) {
      const payloadsRaw = extractFieldRaw(firstResult, 'payloads');
      if (payloadsRaw) {
        const payloadList = extractFieldRaw(payloadsRaw, 'list');
        if (payloadList && Array.length(payloadList) > 0) {
          const firstPayload = payloadList[0].PayloadInstance;
          if (firstPayload) {
            payload = extractField(firstPayload, 'payload') || '';
            const requestRaw = extractFieldRaw(firstPayload, 'request');
            if (requestRaw) {
              uri             = extractField(requestRaw, 'link')    || '';
              request_headers = extractField(requestRaw, 'headers') || '';
            }
            evidence = extractField(firstPayload, 'response') || '';
          }
        }
      }
    }
  }
}

// enrichment
// QID level fields fan out to every finding sharing this QID.
// Instance fields (uri, payload, evidence, etc.) apply only to the specific finding fetched.

for (let r = 0; r < Array.length(data.final_result); r++) {
  const findings = data.final_result[r].findings;
  for (let f = 0; f < Array.length(findings); f++) {
    if (findings[f].qid === enrichedQid) {
      // QID-level — same value for all findings with this QID
      findings[f].cwe   = cwe;
      findings[f].owasp = owasp;
      findings[f].group = group;
      if (Array.length(wasc) > 0)            findings[f].wasc         = Array.join(wasc, '; ');
      if (cvss3 !== null)                    findings[f].cvss3        = cvss3;
      if (String.length(cvss3_vector) > 0)   findings[f].cvss3_vector = cvss3_vector;
    }

    if (findings[f].finding_id === current.finding_id) {
      // Instance-level — specific to this individual finding
      if (String.length(uri) > 0)             findings[f].uri             = uri;
      if (String.length(payload) > 0)         findings[f].payload         = payload;
      if (String.length(request_headers) > 0) findings[f].request_headers = request_headers;
      if (String.length(evidence) > 0)        findings[f].evidence        = evidence;
      if (String.length(input_name) > 0)      findings[f].input_name      = input_name;
      if (times_detected !== null)            findings[f].times_detected  = times_detected;
    }
  }
}

Array.splice(data.enrichment_queue, 0, 1);
data.enrichment_done = data.enrichment_done + 1;

if (secrets.logging_level === 'debug') {
  Logger.debug('Enriched finding ' + current.finding_id + ' (QID ' + enrichedQid + '). Progress: ' + data.enrichment_done + '/' + data.enrichment_total + '.');
}

if (Array.length(data.enrichment_queue) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Enriched finding ' + current.finding_id + ' (' + data.enrichment_done + '/' + data.enrichment_total + '). Fetching next.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      enrichment_queue:       data?.enrichment_queue,
      enrichment_total:       data?.enrichment_total,
      enrichment_done:        data?.enrichment_done
    }
  };
}

return {
  decision: {
    status: 'next',
    message: 'All ' + data.enrichment_total + ' findings enriched. Proceeding to import.'
  },
  data: {
    project_id:             data?.project_id,
    project_name:           data?.project_name,
    scan_ref:               data?.scan_ref,
    final_result:           data?.final_result,
    unsuccessful_was_scans: data?.unsuccessful_was_scans || []
  }
};

function extractField(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName][0]['#text'];
    }
  }
  return null;
}

function extractFieldRaw(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName];
    }
  }
  return null;
}

function findResponseCode(obj) {
  if (Object.keys(obj)[0] === 'responseCode') return obj; 
}
function findData(obj) { 
  if (Object.keys(obj)[0] === 'data')         return obj;
}
```

**Action 4 - Get Knowledgebase and Enrich**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/4.0/fo/knowledge\_base/vuln/?action=list\&details=All\&ids={{qids}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = AttackForge
  * Key = Accept; Type = Value; Value = application/xml
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to import.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.final_result) {
  return {
    decision: {
      status: 'next',
      message: 'Error: project_id and final_result must be present in data.'
    },
    data:{
      project_id:   data.project_id,
      project_name: data.project_name,
      flow_status:{
        step: 'Get Knowledgebase and Enrich- Request Script',
        status: 'VALIDATION_ERROR',
        message: 'project_id and final_result must both be present in this step.'
      }
    }
  };
}

// Collect all unique QIDs across all findings
const qid_seen = {};
const qids = [];
for (let r = 0; r < Array.length(data.final_result); r++) {
  const findings = data.final_result[r].findings;
  for (let f = 0; f < Array.length(findings); f++) {
    const qid = String.from(findings[f].qid);
    if (qid && !qid_seen[qid]) {
      qid_seen[qid] = true;
      Array.push(qids, qid);
    }
  }
}

if (Array.length(qids) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No QIDs found in findings. Skipping KB enrichment.'
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

const qids_str = Array.join(qids, ',');
const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

if (secrets.logging_level === 'debug') {
  Logger.debug('Fetching KB for ' + Array.length(qids) + ' unique QID(s): ' + qids_str);
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Qualys KB for ' + Array.length(qids) + ' unique QID(s).'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/4.0/fo/knowledge_base/vuln/?action=list&details=All&ids=' + qids_str,
    method: 'GET',
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref:     data.scan_ref,
    final_result: data.final_result,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  const err_msg = 'HTTP error fetching KB: ' + response?.statusCode + '. Proceeding to import without KB enrichment.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

const parsed = XML.parse(response.body);
let kbRoot;
if (parsed && parsed[0]) {
  kbRoot = parsed[0].KNOWLEDGE_BASE_VULN_LIST_OUTPUT;
}
if (!kbRoot){
  const err_msg = 'Error: KNOWLEDGE_BASE_VULN_LIST_OUTPUT does not exist in parsed XML body.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Get Knowledgebase and Enrich - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const kbResponseEl = Array.find(kbRoot, findResponse);
const vulnListEl   = kbResponseEl ? Array.find(kbResponseEl.RESPONSE, findVulnList) : null;

if (!vulnListEl) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('No VULN_LIST in KB response. Passing through.');
  }
  return {
    decision: { status: 'next', message: 'No VULN_LIST in KB response. Proceeding to import.' },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

// Build QID → KB data map
const kb_map  = {};
const vulnList = vulnListEl.VULN_LIST;

for (let i = 0; i < Array.length(vulnList); i++) {
  if (Object.keys(vulnList[i])[0] !== 'VULN') continue;
  const vuln = vulnList[i].VULN;

  const qid         = String.from(extractField(vuln, 'QID') || '');
  const diagnosis   = extractField(vuln, 'DIAGNOSIS')   || '';
  const consequence = extractField(vuln, 'CONSEQUENCE') || '';
  const solution    = extractField(vuln, 'SOLUTION')    || '';

  const cves       = [];
  const cveListRaw = extractFieldRaw(vuln, 'CVE_LIST');
  if (cveListRaw) {
    for (let c = 0; c < Array.length(cveListRaw); c++) {
      if (Object.keys(cveListRaw[c])[0] === 'CVE') {
        const cveId = extractField(cveListRaw[c].CVE, 'ID');
        if (cveId) Array.push(cves, cveId);
      }
    }
  }

  if (qid) {
    kb_map[qid] = {
      description: diagnosis,
      consequence: consequence,
      solution:    solution,
      cves:        cves
    };
  }
}

// Apply KB enrichment to all findings
let enriched_count = 0;
for (let r = 0; r < Array.length(data.final_result); r++) {
  const findings = data.final_result[r].findings;
  for (let f = 0; f < Array.length(findings); f++) {
    const finding = findings[f];
    const kb = kb_map[String.from(finding.qid)];
    if (kb) {
      if (String.length(kb.description) > 0) finding.description = kb.description;
      if (String.length(kb.consequence) > 0) finding.consequence = kb.consequence;
      if (String.length(kb.solution)    > 0) finding.solution    = kb.solution;
      if (Array.length(kb.cves)         > 0) finding.cves        = kb.cves;
      enriched_count = enriched_count + 1;
    }
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('KB enrichment applied to ' + enriched_count + ' finding(s).');
}

return {
  decision: {
    status: 'next',
    message: 'KB enrichment complete. Enriched ' + enriched_count + ' finding(s). Proceeding to import.'
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref:     data.scan_ref,
    final_result: data.final_result,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};

function extractField(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName][0]['#text'];
    }
  }
  return null;
}

function extractFieldRaw(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName];
    }
  }
  return null;
}

function findResponse(obj) { 
  if (Object.keys(obj)[0] === 'RESPONSE')  return obj; 
}
function findVulnList(obj) { 
  if (Object.keys(obj)[0] === 'VULN_LIST') return obj; 
}
```

**Action 5 - Trigger Import Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_import\_vuln\_trigger\_flow\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.final_result) {
  const err_msg = 'Error: project_id and final_result must be present in data. Please check the log.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: {
        step: 'Trigger Import Flow- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

if (Array.length(data.final_result) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No WAS findings to import. Flow complete.'
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      no_findings:            "true"
    }
  };
}

let totalFindings = 0;
for (let r = 0; r < Array.length(data.final_result); r++) {
  if (data.final_result[r].findings) {
    totalFindings = totalFindings + Array.length(data.final_result[r].findings);
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Triggering WAS import for project ' + data.project_id + ': ' + totalFindings + ' finding(s) across ' + Array.length(data.final_result) + ' web apps.'); 
}

return {
  decision: {
    status: 'continue',
    message: 'Triggering import of ' + totalFindings + ' WAS finding(s) for project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_import_vuln_trigger_flow_id,
    body: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result
    }
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering WAS import flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error triggering import flow. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to cleanup and email report.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      flow_status: {
        step: 'Trigger Import Flow - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error triggering import flow for project: ' + (data?.project_id || 'unknown') + '.'
      }
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('WAS import flow triggered successfully for project: ' + data?.project_id + '.');
}

return {
  decision: {
    status: 'continue',
    message: 'WAS import flow triggered successfully for project: ' + data?.project_id + '. Proceeding to cleanup.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    unsuccessful_was_scans: data?.unsuccessful_was_scans || []
  }
};
```

**Action 6 - No Findings only - Add Note to Project**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}/note
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = apikey
* **Request Script**:

```javascript
if (!data?.flow_status || data.flow_status?.status !== 'NO_FINDINGS'){
  return {
    decision:{
      status: 'next',
      message: 'Not a NO_FINDINGS flow, skipping action.'
    },
    data: data
  };
}

if (!data?.project_id){
  const err_msg = 'Error: data.project_id is missing in NO_FINDINGS path. Please check the log.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: { 
        step: 'No Findings only - Add Note to Project- Request Script', 
        status: 'VALIDATION_ERROR', 
        message: err_msg 
      }
    }
  };
}

const now = Date.format(Date.datetime('now'), 'default');

return {
  decision: {
    status: 'continue',
    message: 'Requesting create new note on project: ' + data.project_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id + '/note',
    body: {
      note: "No findings from Qualys WAS Scan. \nScan Reference: " + data.scan_ref +'\nDate: '+now,
      is_private: false,
      is_exported_to_report: false
    }
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || [],
    flow_status: data.flow_status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  const err_msg = 'HTTP error creating note to project. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      flow_status: { 
        step: 'No Findings only - Add Note to Project- Response Script', 
        status: 'HTTP_ERROR', 
        message: err_msg 
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully added no findings note to project: '+ data?.project_name,
  },
  data: {
    project_id:             data?.project_id,
    project_name:           data?.project_name,
    scan_ref:               data?.scan_ref,
    unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
    flow_status: data?.flow_status
  }
};
```

**Action 7 - Update Project to Remove WAS Custom Fields**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{{id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = apikey
* **Request Script**:

```javascript
if (!data?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.project_id is missing. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Clearing Qualys WAS custom fields from project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        { key: 'qualys_was_active_scan', value: null },
        { key: 'qualys_was_scan_time',   value: null },
        { key: 'qualys_was_multiscan',   value: null }
      ]
    }
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error clearing WAS custom fields: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error clearing WAS custom fields from project: ' + data?.project_id + '. Proceeding to email report action.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      flow_status: {
        step: 'Update Project - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error clearing WAS custom fields from project: ' + (data?.project_id || 'unknown') + '.'
      }
    }
  };
}

// fire warning email for succeeded but some scans failed
if (data?.unsuccessful_was_scans && Array.length(data.unsuccessful_was_scans) > 0) {
  const fail_summaries = [];
  for (let i = 0; i < Array.length(data.unsuccessful_was_scans); i++) {
    const f = data.unsuccessful_was_scans[i];
    Array.push(fail_summaries, 'Scan ' + f.scan_id + ': status=' + f.status + ' (consolidatedStatus=' + f.consolidated_status + ') aborted status: ' + f.consolidated_status + '.');
  }
  const err_msg = 'Import completed for successful scans, but ' + Array.length(data?.unsuccessful_was_scans) + ' scan(s) also failed. ' + Array.join(fail_summaries, ' ');
  return {
    decision: {
      status: 'next',
      message: 'Import completed but ' + Array.length(data?.unsuccessful_was_scans) + ' scan(s) failed. Proceeding to email report action.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans,
      flow_status: {
        step:    'Update Project - Response Script',
        status:  'PARTIAL_FAILURE',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully cleared WAS custom fields from project: ' + data?.project_id + '.'
  },
  data: {
    project_id:   data?.project_id,
    project_name: data?.project_name,
    scan_ref:     data?.scan_ref
  }
};
```

**Action 8 - Send Email for Error Reporting**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/email
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = apikey
* **Request Script**:

```javascript
if (data?.flow_status) {
  if (data.project_name && data.scan_ref && data.project_id) {
    const curr_date = Date.datetime('now');
    const curr_year = Date.format(curr_date, 'yyyy');
    const current_display_date = Date.format(curr_date, 'default');
    const emailHeader = "<!doctype html> <html lang='en' xmlns:v='urn:schemas-microsoft-com:vml' style='color-scheme: light dark'> <head> <meta charset='utf-8'> <meta name='x-apple-disable-message-reformatting'> <meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='format-detection' content='telephone=no, date=no, address=no, email=no, url=no'> <meta name='color-scheme' content='light dark'> <meta name='supported-color-schemes' content='light dark'> <link rel='preconnect' href='https://fonts.googleapis.com'> <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin> <link href='https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;700&display=swap' rel='stylesheet' media='screen'> <style> .body-sub { margin-top: 25px; border-top-width: 1px; padding-top: 25px; border-top-color: #eaeaec; border-top-style: solid; } body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } .sm-w-full { width: 100% !important; } h1 { margin-top: 0; text-align: left; font-size: 24px; font-weight: 700; color: #333333 } p { margin-bottom: 5px; margin-top: 6px; font-size: 16px; line-height: 24px; color: #51545e } .styled-table { width: 600px; border-collapse: collapse; } .styled-table th { border-collapse: collapse; border: 1px solid white; } .styled-table td { border-collapse: collapse; border: 1px solid white; padding-left: 20px; } .styled-button { border: none; border-collapse: collapse; } .styled-button td { border: 1px solid; border-radius: 5px; border-color: transparent; background-color: #469cf0 !important; padding: 15px 30px; } .styled-button a { background-color: #469cf0 !important; display: inline-block; font-size: 17px; color: #ffffff; text-decoration: none; font-family: sans-serif; } </style> </head> <body style='margin: 0; width: 100%; background-color: #f2f4f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word'> <div role='article' aria-roledescription='email' aria-label lang='en'> <table class='email-wrapper' style='width: 100%; background-color: #1e293b; font-family: &quot;Inter&quot;, ui-sans-serif, system-ui, -apple-system, &quot;Segoe UI&quot;, sans-serif;' cellpadding='0' cellspacing='0' role='none' > <tr> <td align='center'> <table class='email-content' style='width: 100%' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' class='email-masthead' style='display: flex; justify-content: center; gap: 8px; padding-top: 25px; padding-bottom: 25px; text-align: center; font-size: 16px'> </td> </tr> <tr> <td class='email-body' style='width: 100%; background-color: #fff'> <table align='center' class='email-body_inner sm-w-full' style='margin-left: auto; margin-right: auto; width: 800px; background-color: #fff' cellpadding='0' cellspacing='0' role='none'> <tr> <td style='padding: 45px'>";
    const emailFooter = "</td> </tr> </table> </td> </tr> <tr> <td> <table align='center' class='email-footer sm-w-full' style='margin-left: auto; margin-right: auto; width: 570px; text-align: center' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' style='padding: 45px; font-size: 16px'> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'> This is a system generated email and reply is not monitored. <br> For any queries please reach out to <span style='text-decoration-line: underline'>support@attackforge.com</span> </p> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'>&copy; 2018-"+ curr_year +" AttackForge&reg;</p> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </div> </body> </html>";

    let content = '<p><strong>Attention: Qualys WAS Poll and Get Findings Flow has detected an error.</strong></p>'
    + '<p>Please check the AttackForge Flow log and Qualys WAS scan log for details.</p>'
    + '<br>'
    + '<table class="styled-table" align="center">'
    + '<tr><th width="200">Field</th><th width="200">Value</th></tr>'
    + '<tr><td> Scan Reference </td><td>' + data.scan_ref + '</td></tr>'
    + '<tr><td> Project </td><td>' + data.project_name + '</td></tr>'
    + '<tr><td> Step </td><td>' + data.flow_status?.step + '</td></tr>'
    + '<tr><td> Status </td><td>' + data.flow_status?.status + '</td></tr>'
    + '<tr><td> Message </td><td>' + data.flow_status?.message + '</td></tr>'
    + '<tr><td> Time </td><td>' + current_display_date + '</td></tr>'
    + '</table>'
    + '<br>';

    if (data.unsuccessful_was_scans && Array.length(data.unsuccessful_was_scans) > 0) {
      content = content
        + '<p><strong>Failed Scans (' + Array.length(data.unsuccessful_was_scans) + '):</strong></p>'
        + '<table class="styled-table" align="center">'
        + '<tr><th width="120">Scan ID</th><th width="120">Status</th><th width="200">Consolidated Status</th><th width="100">Fail Type</th></tr>';
      for (let i = 0; i < Array.length(data.unsuccessful_was_scans); i++) {
        const f = data.unsuccessful_was_scans[i];
        content = content + '<tr><td>' + f.scan_id + '</td><td>' + f.status + '</td><td>' + f.consolidated_status + '</td><td>' + (f.fail_type || '') + '</td></tr>';
      }
      content = content + '</table><br>';
    }

    content = content
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.af_hostname+'/flows/'+secrets.was_poll_flow_id+'/home">View Flow Run</a></td></tr></table>'
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.qualys_web_base_url+'was/#/scans/scans?searchPivotToken=SCAN&source=&pageSize=50&pageNumber=0">View Qualys WAS Board</a></td></tr></table>'
    + '<br>';

    return {
      decision: {
        status: 'continue',
        message: 'Sending WAS error report email.'
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/email',
        body: {
          to: [secrets.admin_email],
          subject: '[Flow Notification] Non successful scan discovered from Qualys WAS Scanning',
          html: emailHeader + content + emailFooter
        }
      }
    };
  } 
  else {
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_name, scan_ref, project_id must all be present in data. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No flow_status found. Flow completed successfully. Exiting.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error sending WAS report email: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'finish',
      message: 'Error sending report email. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully sent WAS error report email to admin. Reached end of the flow.'
  }
};
```

## Qualys VM - Poll and Fetch Findings \[Step 4 of 5]

<figure><img src="/files/Edn0XZFWhWR0YIj9CmXc" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export the results of a VM scan in Qualys.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * current\_flow\_id - the Id for this Flow in AttackForge once it's created (used for error reporting in email)
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_import\_vuln\_trigger\_flow\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM & WAS - Import Vulnerabilities \[Step 5 of 5\]](#qualys-vm-and-was-import-vulnerabilities-step-5-of-5)
  * qualys\_vm\_launch\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM - Launch Scan \[Step 2 of 5\]](#qualys-vm-launch-scan-step-2-of-5)
  * qualys\_web\_base\_url - your Qualys web console base URL e.g. *<https://qualysguard.qualys.com>*
  * admin\_email - the email address to send error and alert notifications
  * logging\_level - set to "debug" for additional logging

**Action 1 - Poll Scan Status**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/2.0/fo/scan/vm/summary/?action=list\&scan\_reference=scan/{{scan\_ref}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
const project = data?.jsonBody?.project;

if (!project?.scan_ref || !project?.project_id || !project?.project_name) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('project: ', JSON.stringify(project));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: scan_ref, project_id, project_name must all be present in the project. Please check the log.'
    }
  };
}
if (!secrets.qualys_auth || secrets.qualys_auth === ""){
  if (secrets.logging_level === 'debug'){
    Logger.debug('secrets.qualys_auth: ', secrets.qualys_auth);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: secrets.qualys_auth must be present in the secrets. Please check the log.'
    }
  };
}

const auth_header  = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');
return {
  decision: {
    status: 'continue',
    message: 'Requesting VM Scan Summary (Polling).',
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/2.0/fo/scan/vm/summary/?action=list&scan_reference=scan/' + project.scan_ref,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: project.project_id,
    scan_ref: project.scan_ref,
    project_name: project.project_name
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  let err_message = '';
  const err_body = XML.parse(response.body)[0];
  if (err_body?.SIMPLE_RETURN?[0]) {
    const err_response = err_body?.SIMPLE_RETURN?[0]?.RESPONSE;
    if (err_response) {
      function findText(obj) {
        return Object.keys(obj)[0] === 'TEXT';
      }
      const err_message_object = Array.find(err_response, findText);
      if (err_message_object) {
        err_message = err_message_object['TEXT'][0]['#text'];
      }
    }
  }
  if (secrets.logging_level === 'debug') {
    Logger.debug('Poll Scan Status failed. HTTP ' + response.statusCode + ': ' + err_message);
  }
  return {
    decision: {
      status: 'next',
      message: 'Error while polling scan status. HTTP ' + response.statusCode + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      flow_status: {
        step: 'Poll Scan Status - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + response.statusCode + (err_message ? ': ' + err_message : '') + '.'
      }
    }
  };
}

if (!response?.body) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: response.body does not exist. Please check the log.'
    },
    data: {
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      flow_status: {
        step: 'Poll Scan Status - Response Script',
        status: 'HTTP_ERROR',
        message: 'Empty response body received from Qualys scan status poll.'
      }
    }
  };
}

const xml = response?.body;
const xml_parsed = XML.parse(xml)[0];
const flat = {};
const stack = [];

// Initialise the stack
Array.push(stack, { node: xml_parsed, parentKey: '' });
// Collision keys are ommited such as ID, Name Count.
while (Array.length(stack) > 0) {
  const item = Array.shift(stack);
  const node = item.node;
  const parentKey = item.parentKey;

  if (Array.isArray(node)) {
    for (let i = 0; i < Array.length(node); i++) {
      Array.push(stack, { node: node[i], parentKey: parentKey });
    }
  } 
  else if (node !== null && typeof node === 'object') {
    const entries = Object.entries(node);
    if (Array.length(entries) === 1 && entries[0][0] === '#text') {
      if (parentKey) {
        flat[parentKey] = entries[0][1];
      }
    } 
    else {
      for (let i = 0; i < Array.length(entries); i++) {
        const key = entries[i][0];
        const value = entries[i][1];
        const newKey = parentKey ? parentKey + '_' + key : key;
        Array.push(stack, { node: value, parentKey: key });
      }
    }
  }
}

if (secrets.logging_level === 'debug'){
  Logger.debug(JSON.stringify(flat));
}

// Extract IP ranges and DNS targets directly from xml_parsed
let targets_ip = [];
let targets_dns = [];

let targets_node = null;
const targets_search_stack = [xml_parsed];
while (Array.length(targets_search_stack) > 0 && targets_node === null) {
  const targets_search_item = Array.shift(targets_search_stack);
  if (Array.isArray(targets_search_item)) {
    for (let i = 0; i < Array.length(targets_search_item); i++) {
      Array.push(targets_search_stack, targets_search_item[i]);
    }
  } 
  else if (targets_search_item !== null && typeof targets_search_item === 'object') {
    const targets_search_keys = Object.keys(targets_search_item);
    if (Array.includes(targets_search_keys, 'TARGETS')) {
      targets_node = targets_search_item['TARGETS'];
    } 
    else {
      const targets_search_entries = Object.entries(targets_search_item);
      for (let i = 0; i < Array.length(targets_search_entries); i++) {
        Array.push(targets_search_stack, targets_search_entries[i][1]);
      }
    }
  }
}

if (targets_node) {
  const ip_range_list = [];
  const ip_stack = [targets_node];
  while (Array.length(ip_stack) > 0) {
    const ip_item = Array.shift(ip_stack);
    if (Array.isArray(ip_item)) {
      for (let i = 0; i < Array.length(ip_item); i++) {
        Array.push(ip_stack, ip_item[i]);
      }
    } 
    else if (ip_item !== null && typeof ip_item === 'object') {
      const ip_keys = Object.keys(ip_item);
      if (Array.includes(ip_keys, 'RANGE')) {
        const range_arr = ip_item['RANGE'];
        if (Array.isArray(range_arr)) {
          for (let i = 0; i < Array.length(range_arr); i++) {
            if (range_arr[i]['#text'] !== undefined) {
              Array.push(ip_range_list, String.from(range_arr[i]['#text']));
            }
          }
        }
      } 
      else if (Array.includes(ip_keys, 'IP_CSV')) {
        const csv_arr = ip_item['IP_CSV'];
        if (Array.isArray(csv_arr) && Array.length(csv_arr) > 0 && csv_arr[0] !== null) {
          Array.push(ip_range_list, String.from(csv_arr[0]['#text'] || ''));
        }
      } 
      else {
        const ip_entries = Object.entries(ip_item);
        for (let i = 0; i < Array.length(ip_entries); i++) {
          Array.push(ip_stack, ip_entries[i][1]);
        }
      }
    }
  }
  targets_ip = ip_range_list;

  const dns_stack = [targets_node];
  while (Array.length(dns_stack) > 0) {
    const dns_item = Array.shift(dns_stack);
    if (Array.isArray(dns_item)) {
      for (let i = 0; i < Array.length(dns_item); i++) {
        Array.push(dns_stack, dns_item[i]);
      }
    } 
    else if (dns_item !== null && typeof dns_item === 'object') {
      const dns_keys = Object.keys(dns_item);
      if (Array.includes(dns_keys, 'DNS_CSV')) {
        const csv_arr = dns_item['DNS_CSV'];
        if (Array.isArray(csv_arr) && Array.length(csv_arr) > 0 && csv_arr[0] !== null) {
          const dns_csv_str = String.from(csv_arr[0]['#text'] || '');
          targets_dns = String.split(dns_csv_str, ',');
        }
      } 
      else {
        const dns_entries = Object.entries(dns_item);
        for (let i = 0; i < Array.length(dns_entries); i++) {
          Array.push(dns_stack, dns_entries[i][1]);
        }
      }
    }
  }
}

if (flat['STATUS']){
  // prepare body to parse here
  const dataObject = {
    project_id: data.project_id,
    scan_ref: data.scan_ref,
    project_name: data.project_name,
    status: flat['STATUS']
  };

  const processing_status = ['RUNNING', 'QUEUED'];
  if (Array.includes(processing_status, flat['STATUS'])){
    if (secrets.logging_level === 'debug'){
      Logger.debug('Scan status: ', flat['STATUS']);
    }
    return {
      decision:{
        status:'finish',
        message:'Scan in status: ' + flat['STATUS'] + '. No further actions necessary. Exiting the flow.'
      }
    };
  }

  // If status successful
  if (flat['STATUS'] === 'FINISHED'){
    return {
      decision: {
        status: 'continue',
        message: 'Scan finished successfully. Proceeding to fetching scan results.'
      },
      data: dataObject
    };
  }

  // skip all the way to notes
  if (flat['STATUS'] === 'NOVULNSFOUND'){
    dataObject.flow_status = {
      step: 'Poll Scan Status- Response Script',
      status: 'NOVULNSFOUND',
      message: 'No vulnerability found from scan: ' + data.scan_ref
    };
    return {
      decision: {
        status: 'continue',
        message: 'No vulnerability found from scan: ' + data.scan_ref + '. Proceeding to adding no vulnerability found note to project: ' + data.project_id
      },
      data: dataObject
    };
  }

  if (flat['STATUS'] === 'INTERRUPTED'){
    let combined_targets = [];
    combined_targets = Array.concat(combined_targets, targets_ip);
    combined_targets = Array.concat(combined_targets, targets_dns);
    dataObject.targets = combined_targets;
    return {
      decision: {
        status: 'continue',
        message: 'Detected '+flat['STATUS']+' status on scan/'+ data.scan_ref+'. Proceeding to automatic relaunch of the scan.'
      },
      data: dataObject
    };
  }

  const email_report_status = ['ERROR', 'NOHOSTALIVE', 'CANCELED', 'PAUSED'];

  if (Array.includes(email_report_status, flat['STATUS'])) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Unsuccessful scan. Scan status: ', flat['STATUS']);
    }
    dataObject.flow_status = {
      step: 'Poll Scan Status - Response Script',
      status: flat['STATUS'],
      message: 'Scan ended with status: ' + flat['STATUS'] + '.'
    };
    return {
      decision: {
        status: 'next',
        message: 'Unsuccessful scan status detected: ' + flat['STATUS'] + '. Proceeding to email report action.'
      },
      data: dataObject
    };
  }

  if (secrets.logging_level === 'debug') {
    Logger.debug('Unrecognised scan status detected: ', flat['STATUS']);
    Logger.debug('data: ', JSON.stringify(data));
  }
  dataObject.flow_status = {
    step: 'Poll Scan Status - Response Script',
    status: flat['STATUS'],
    message: 'Unrecognised scan status detected: ' + flat['STATUS'] + '.'
  };
  return {
    decision: {
      status: 'next',
      message: 'Error: detected unrecognised scan status: ' + flat['STATUS'] + '. Proceeding to email report action.'
    },
    data: dataObject
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('STATUS field not found in XML response.');
  Logger.debug('response: ', JSON.stringify(response));
}
return {
  decision: {
    status: 'next',
    message: 'Error: STATUS field not found in Qualys API response. Proceeding to email report action.'
  },
  data: {
    project_id: data?.project_id,
    scan_ref: data?.scan_ref,
    project_name: data?.project_name,
    flow_status: {
      step: 'Poll Scan Status - Response Script',
      status: 'UNKNOWN',
      message: 'STATUS field not found in Qualys API response.'
    }
  }
};
```

**Action 2 - If Interrupted - Get Project Assets**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/project/{{id}}
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.status !== 'INTERRUPTED') {
  return {
    decision: {
      status: 'next',
      message: data.status + ' status detected. Skipping to next action.'
    },
    data: data
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug(data.status + ' status detected. Initiating retry phase.');
}

if (!secrets.af_hostname || !data?.project_id) {
  const err_msg = 'secrets.af_hostname and data.project_id must be present. secrets.af_hostname: '+ secrets?.af_hostname + '. data.project_id: ' + data?.project_id;
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data:{
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      flow_status: {
        step: '[Error route] Get Project Assets - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching asset information from project: ' + data.project_id + '.',
  },
  request: {
    url: 'https://'+secrets.af_hostname+'/api/ss/project/'+data?.project_id,
  },
  data:{
    project_id: data?.project_id,
    scan_ref: data?.scan_ref,
    project_name: data?.project_name,
    status: data?.status,
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: ' + JSON.stringify(response));
  }
  const err_msg = 'Error while fetching project asset. Please check the log.';
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data:{
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      flow_status: {
        step: '[Error route] Get Project Assets - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP Error:' + response?.statusCode + '. ' + err_msg
      }
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug(data.status + ' status detected. Initiating retry phase.');
}

let assets;
if (response?.jsonBody){
  if (response.jsonBody?.project?.project_scope){
    assets = response.jsonBody.project.project_scope;
  } 
  else {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Missing project or project_scope: ' + JSON.stringify(response.jsonBody?.project));
    }
    const err_msg = 'Error while fetching project asset. Missing project or project_scope. Please check the log.';
    return {
      decision: {
        status: 'next',
        message: err_msg
      },
      data:{
        project_id: data?.project_id,
        scan_ref: data?.scan_ref,
        project_name: data?.project_name,
        status: data?.status,
        flow_status: {
          step: '[Error route] Get Project Assets - Response Script',
          status: 'VALIDATION_ERROR',
          message: err_msg
        }
      }
    };
  }
} 
else {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: ' + JSON.stringify(response));
  }
  const err_msg = 'Error while fetching project asset. response.jsonBody is not present. Please check the log.';
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data:{
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      flow_status: {
        step: '[Error route] Get Project Assets - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully fetched valid project assets from project: '+ data.project_id + '.',
  },
  data: {
    project_id: data?.project_id,
    scan_ref: data?.scan_ref,
    project_name: data?.project_name,
    status: data?.status,
    assets: assets
  }
};
```

**Action 3 - Re-launch Interrupted Scan**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_launch\_scan\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.status !== 'INTERRUPTED') {
  return {
    decision: {
      status: 'next',
      message: data.status + ' status detected. Skipping to next action.'
    },
    data: data
  };
}

if (!data.scan_ref || !data.project_id || !data.assets || !data.project_name) {
  const err_message = "Error while relaunching Interrupted Scan. scan_ref, project_id, project_name, and targets must all be present in data object.";
  return {
    decision:{
      status: 'next',
      message: err_message
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status,
      assets: data.assets,
      flow_status: {
        step: 'Re-launch Interrupted Scan- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_message
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Relaunching Interrupted Scan: ' + data.scan_ref + ' on assets: ' + JSON.stringify(data.assets),
  },
  request: {
    url: 'https://'+secrets.af_hostname+'/api/flows/'+secrets.qualys_launch_scan_flow_trigger_id,
    body: {
      project_id: data.project_id,
      project_name: data.project_name,
      assets: data.assets
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Error while relaunching the scan. Please check the log.',
    },
    data: {
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      targets: data?.targets,
      flow_status: {
        step: 'Re-launch Interrupted Scan- Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + response.statusCode + '. Error while relaunching the interrupted scan.'
      }
    },
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully requested relaunch of the interrupted scan.',
  }
};
```

**Action 4 - Fetch Scan Result**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/api/3.0/fo/scan/?action=fetch\&scan\_ref=scan/{{scan\_ref}}\&output\_format=json
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.project_name || !data?.scan_ref) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id, project_name, scan_ref must all be present in data. Please check the log.'
    }
  };
}

const success_status = ['FINISHED'];
if (!Array.includes(success_status, data.status)) {
  return {
    decision: {
      status: 'next',
      message: 'Status ' + data.status + ' detected. Skipping to report.'
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status
    }
  };
}

if (!secrets.qualys_auth || secrets.qualys_auth === undefined || !secrets.qualys_api_url || secrets.qualys_api_url === undefined) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('qualys_auth: ', secrets.qualys_auth);
    Logger.debug('qualys_api_url: ', secrets.qualys_api_url);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: qualys_auth and qualys_api_url must be registered in secrets. Please check the log.'
    }
  };
}

const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

return {
  decision: {
    status: 'continue',
    message: 'Fetching scan results for scan_ref: ' + data.scan_ref + '.'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/4.0/fo/scan/?action=fetch&scan_ref=scan/' + data.scan_ref + '&output_format=json',
    method: 'GET',
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching scan result: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error fetching Qualys scan result. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Scan Result - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error fetching Qualys scan result.'
      }
    }
  };
}

const body = response?.body;
const results = body ? JSON.parse(body) : response?.jsonBody;

if (!results) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: Empty response body from Qualys scan fetch. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Scan Result - Response Script',
        status: 'HTTP_ERROR',
        message: 'Empty response body from Qualys scan fetch.'
      }
    }
  };
}

if (!results || !Array.isArray(results)) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('results: ', JSON.stringify(results));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: Invalid JSON array returned from Qualys scan fetch. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Scan Result - Response Script',
        status: 'HTTP_ERROR',
        message: 'Invalid JSON array returned from Qualys scan fetch.'
      }
    }
  };
}

const qid_seen = {};
const unique_qids = [];
for (let i = 0; i < Array.length(results); i++) {
  const qid = results[i]?.qid;
  if (qid && !qid_seen[String.from(qid)]) {
    qid_seen[String.from(qid)] = true;
    Array.push(unique_qids, String.from(qid));
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Fetched ' + Array.length(results) + ' raw detections, ' + Array.length(unique_qids) + ' unique QIDs.');
}

return {
  decision: {
    status: 'continue',
    message: 'Fetched ' + Array.length(results) + ' raw detection(s). Proceeding to KnowledgeBase enrichment.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    raw_detections: results,
    qids: unique_qids
  }
};
```

**Action 5 - Fetch Knowledgebase**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/4.0/fo/knowledge\_base/vuln/?action=list\&details=All\&ids={{qids\_param}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.project_name || !data?.scan_ref) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id, project_name, scan_ref must all be present in data. Please check the log.'
    }
  };
}

// No QIDs means nothing to enrich - pass empty records forward
if (!data?.qids || Array.length(data.qids) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No QIDs to enrich. Skipping KnowledgeBase lookup.'
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      records: []
    }
  };
}

const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');
const qids_param = Array.join(data.qids, ',');

return {
  decision: {
    status: 'continue',
    message: 'Fetching KnowledgeBase metadata for ' + Array.length(data.qids) + ' unique QID(s).'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/4.0/fo/knowledge_base/vuln/?action=list&details=All&ids=' + qids_param,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status,
    raw_detections: data.raw_detections,
    qids: data.qids
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching KnowledgeBase: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error fetching Qualys KnowledgeBase. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Knowledgebase - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error fetching Qualys KnowledgeBase.'
      }
    }
  };
}

const kb_xml = response?.body;
if (!kb_xml) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: Empty response body from Qualys KnowledgeBase API. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Knowledgebase - Response Script',
        status: 'HTTP_ERROR',
        message: 'Empty response body from Qualys KnowledgeBase API.'
      }
    }
  };
}

const xml_parsed = XML.parse(kb_xml)[0];
if (secrets.logging_level === 'debug') {
  Logger.debug('KB XML parsed: ', JSON.stringify(xml_parsed));
}

// nav is cursor to VULN_LIST
let nav = xml_parsed;
if (nav && nav.KNOWLEDGE_BASE_VULN_LIST_OUTPUT){
  nav = nav.KNOWLEDGE_BASE_VULN_LIST_OUTPUT;
  if (Array.isArray(nav)) nav = nav[0];
  if (nav && nav.RESPONSE) {
    const response_items = Array.isArray(nav.RESPONSE) ? nav.RESPONSE : [nav.RESPONSE];
    nav = null;
    for (let i = 0; i < Array.length(response_items); i++){
      if (Object.keys(response_items[i])[0] === 'VULN_LIST'){
        nav = response_items[i].VULN_LIST;
        break;
      }
    }
  }
}

let vuln_array = [];
if (Array.isArray(nav)) {
  for (let i = 0; i < Array.length(nav); i++) {
    const item = nav[i];
    if (item && item.VULN && Array.isArray(item.VULN)) {
      Array.push(vuln_array, flattenVuln(item.VULN));
    }
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('KB vuln count: ', Array.length(vuln_array));
}

// Build QID -> KB metadata map
const kb_map = {};
for (let i = 0; i < Array.length(vuln_array); i++) {
  const vuln = vuln_array[i];
  if (!vuln) continue;
  const qid_str = extractText(vuln.QID);

  if (!qid_str) continue;

  // Severity mapping: Qualys 1-5 -> Tenable Risk/Severity 0-4
  const sl_raw = Number.parseInt(extractText(vuln.SEVERITY_LEVEL)) || 1;
  let risk = 'None';
  if (sl_raw === 2) risk = 'Low';
  else if (sl_raw === 3) risk = 'Medium';
  else if (sl_raw === 4) risk = 'High';
  else if (sl_raw === 5) risk = 'Critical';
  const severity_str = String.from(sl_raw > 1 ? sl_raw - 1 : 0);

  // CVE list: comma-separated IDs
  let cve_str = '';
  if (vuln.CVE_LIST && vuln.CVE_LIST?[0]?.CVE) {
    const cve_nodes = Array.isArray(vuln.CVE_LIST[0].CVE) ? vuln.CVE_LIST[0].CVE : [vuln.CVE_LIST[0].CVE];
    const cve_ids = [];
    for (let j = 0; j < Array.length(cve_nodes); j++) {
      const id = extractText(cve_nodes[j]?.ID);
      if (id) Array.push(cve_ids, id);
    }
    cve_str = Array.join(cve_ids, ', ');
  }

  // CVSS v2
  let cvss_base = '0.0';
  let cvss_temporal = '';
  let cvss_vector = '';
  if (vuln.CVSS) {
    cvss_base = extractText(vuln.CVSS.BASE) || '0.0';
    cvss_temporal = extractText(vuln.CVSS.TEMPORAL) || '';
    cvss_vector = extractText(vuln.CVSS.VECTOR_STRING) || '';
  }

  // CVSS v3 (element name is CVSS_V3 per DTD)
  let cvss3_base = '0.0';
  let cvss3_temporal = '';
  let cvss3_vector = '';
  if (vuln.CVSS_V3) {
    cvss3_base = extractText(vuln.CVSS_V3.BASE) || '0.0';
    cvss3_temporal = extractText(vuln.CVSS_V3.TEMPORAL) || '';
    cvss3_vector = extractText(vuln.CVSS_V3.VECTOR_STRING) || '';
  }

  // See Also: vendor references and bugtraq URLs
  const see_also_urls = [];
  if (vuln.VENDOR_REFERENCE_LIST && vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE) {
    const refs = Array.isArray(vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE) ? vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE : [vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE];
    for (let j = 0; j < Array.length(refs); j++) {
      const url = extractText(refs[j]?.URL);
      if (url) Array.push(see_also_urls, url);
    }
  }
  if (vuln.BUGTRAQ_LIST && vuln.BUGTRAQ_LIST.BUGTRAQ) {
    const bugs = Array.isArray(vuln.BUGTRAQ_LIST.BUGTRAQ) ? vuln.BUGTRAQ_LIST.BUGTRAQ : [vuln.BUGTRAQ_LIST.BUGTRAQ];
    for (let j = 0; j < Array.length(bugs); j++) {
      const url = extractText(bugs[j]?.URL);
      if (url) Array.push(see_also_urls, url);
    }
  }

  const discovery_remote = vuln.DISCOVERY && extractText(vuln.DISCOVERY.REMOTE) === '1' ? 'true' : 'false';

  kb_map[qid_str] = {
    title:          extractText(vuln.TITLE),
    risk:           risk,
    severity:       severity_str,
    category:       extractText(vuln.CATEGORY),
    diagnosis:      extractText(vuln.DIAGNOSIS),
    consequence:    extractText(vuln.CONSEQUENCE) || '',
    solution:       extractText(vuln.SOLUTION) || 'N/A',
    cve:            cve_str,
    cvss:           cvss_base,
    cvss_temporal:  cvss_temporal,
    cvss_vector:    cvss_vector,
    cvss3:          cvss3_base,
    cvss3_temporal: cvss3_temporal,
    cvss3_vector:   cvss3_vector,
    see_also:       Array.join(see_also_urls, ', '),
    patchable:      extractText(vuln.PATCHABLE) === '1' ? 'true' : 'false',
    published:      extractText(vuln.PUBLISHED_DATETIME) || '',
    last_modified:  extractText(vuln.LAST_SERVICE_MODIFICATION_DATETIME) || '',
    discovery_remote:    discovery_remote
  };
}

// Join raw_detections with KB metadata to produce AttackForge-compatible records
const raw_detections = data?.raw_detections || [];
const records = [];
if (secrets.logging_level === 'debug') {
  Logger.debug('raw_detections count:', Array.length(data.raw_detections || []));
}
for (let i = 0; i < Array.length(raw_detections); i++) {
  const det = raw_detections[i];
  if (!det) continue;

  const qid = String.from(det.qid || '');
  const kb = kb_map[qid] || {};

  Array.push(records, {
    'QualysId':                            qid,
    'CVE':                                 kb.cve || '',
    'CVSS Base Score':                     kb.cvss || '0.0',
    'CVSS3 Base Score':                    kb.cvss3 || '0.0',
    'CVSS3 Vector':                        kb.cvss3_vector || '',
    'Risk':                                kb.risk || 'None',
    'Host':                                det.dns || det.ip || '',
    'IP Address':                          det.ip || '',
    'FQDN':                                det.dns || '',
    'Name':                                kb.title || 'QID ' + qid,
    'Synopsis':                            kb.diagnosis || '',
    'Description':                         kb.consequence || '',
    'Solution':                            kb.solution || 'N/A',
    'See Also':                            kb.see_also || '',
    'Result':                              det.result || '',
    'Vulnerability State':                 'Active',
    'Category':                            kb.category || '',
    'Vulnerability Priority Rating (VPR)': 'null',
    'Exploit Available':                   'false',
    'Patch Available':                     kb.patchable || 'false',
    'Remote':                              kb.discovery_remote
  });
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Built ' + Array.length(records) + ' enriched record(s).');
}

if (secrets.logging_level === 'debug') {
  Logger.debug('records: ', JSON.stringify(records));
}

return {
  decision: {
    status: 'continue',
    message: 'Built ' + Array.length(records) + ' enriched record(s). Proceeding to import trigger.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    records: records
  }
};

function extractText(node){
  if (!node) return '';
  if (node['#text'] !== undefined) return String.from(node['#text']);
  if (typeof node === 'string') return node;
  if (typeof node === 'number') return String.from(node);
  if (Array.isArray(node) && Array.length(node) > 0) {
    const node_obj = node[0];
    if (node_obj && node_obj['#text'] !== undefined) return String.from(node_obj['#text']);
    if (typeof node_obj === 'string') return node_obj;
  }
  return '';
}

// XML.parse gives each VULN as an array of {fieldname: [value]} objects — flatten to a plain object
function flattenVuln(arr) {
  const obj = {};
  for (let i = 0; i < Array.length(arr); i++) {
    const k = Object.keys(arr[i])[0];
    obj[k] = arr[i][k];
  }
  return obj;
}
```

**Action 6 - Fetch Knowledgebase QVS**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/2.0/fo/knowledge\_base/qvs/?action=list\&details=All\&cve={{cves\_param}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.project_name || !data?.scan_ref) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id, project_name, scan_ref must all be present in data. Please check the log.'
    }
  };
}

const records = data.records || [];

if (Array.length(records) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No records to enrich. Skipping QVS lookup.'
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      records: records
    }
  };
}

// Collect unique CVEs across all records
const cve_seen = {};
const unique_cves = [];
for (let i = 0; i < Array.length(records); i++) {
  const cve_field = records[i]['CVE'] || '';
  if (!cve_field) continue;
  const parts = String.split(cve_field, ',');
  for (let j = 0; j < Array.length(parts); j++) {
    const cve = String.trim(parts[j]);
    if (cve && !cve_seen[cve]) {
      cve_seen[cve] = true;
      Array.push(unique_cves, cve);
    }
  }
}

if (Array.length(unique_cves) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No CVEs found in records. Skipping QVS lookup.'
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      records: records
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Unique CVEs for QVS lookup: ', Array.join(unique_cves, ', '));
}

const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');
const cves_param = Array.join(unique_cves, ',');

return {
  decision: {
    status: 'continue',
    message: 'Fetching QVS scores for ' + Array.length(unique_cves) + ' unique CVE(s).'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/2.0/fo/knowledge_base/qvs/?action=list&details=All&cve=' + cves_param,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status,
    records: records
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('QVS fetch failed: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'QVS fetch failed (HTTP ' + (response?.statusCode || 'unknown') + '). Continuing without QVS enrichment.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      records: data?.records || [],
    }
  };
}

const body = response?.body;
const qvs_data = body ? JSON.parse(body) : response?.jsonBody;

if (!qvs_data) {
  return {
    decision: {
      status: 'continue',
      message: 'Empty QVS response. Continuing without QVS enrichment.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      records: data?.records || []
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('QVS response: ', JSON.stringify(qvs_data));
}

const enriched_records = data?.records || [];

for (let i = 0; i < Array.length(enriched_records); i++) {
  const rec = enriched_records[i];
  if (!rec || !rec['CVE']) continue;

  const cves = String.split(rec['CVE'], ',');
  let max_qvs = 0;
  let max_epss = 0;
  let has_exploit = false;
  let has_malware = false;
  let is_cisa_kev = false;
  let max_cvss3 = 0;
  let best_cvss3_score = '';
  let best_cvss3_vector = '';
  let max_cvss2 = 0;
  let best_cvss2_score = '';
  let max_cvss4 = 0;
  let best_cvss4_score = '';
  let best_cvss4_vector = '';

  const maturity_seen = {};
  const maturity_list = [];
  const malware_seen = {};
  const malware_list = [];
  const actor_seen = {};
  const actor_list = [];

  for (let j = 0; j < Array.length(cves); j++) {
    const cve = String.trim(cves[j]);
    if (!cve || !qvs_data[cve]) continue;

    const entry = qvs_data[cve];

    const qvs_score = Number.parseFloat(entry?.base?.qvs || '0');
    if (qvs_score > max_qvs) max_qvs = qvs_score;

    const cvss_str = entry?.contributingFactors?.cvss || '';
    const cvss_version = entry?.contributingFactors?.cvssVersion || '';
    const cvss_vector = entry?.contributingFactors?.cvssString || '';
    if (cvss_str) {
      const cvss_val = Number.parseFloat(cvss_str);
      if (String.includes(cvss_version, 'v4') && cvss_val > max_cvss4) {
        max_cvss4 = cvss_val;
        best_cvss4_score = cvss_str;
        best_cvss4_vector = cvss_vector;
      } 
      else if (String.includes(cvss_version, 'v3') && cvss_val > max_cvss3) {
        max_cvss3 = cvss_val;
        best_cvss3_score = cvss_str;
        best_cvss3_vector = cvss_vector;
      } 
      else if (String.includes(cvss_version, 'v2') && cvss_val > max_cvss2) {
        max_cvss2 = cvss_val;
        best_cvss2_score = cvss_str;
      }
    }

    const epss_arr = entry?.contributingFactors?.epss || [];
    if (Array.length(epss_arr) > 0) {
      const epss_val = Number.parseFloat(epss_arr[0] || '0');
      if (epss_val > max_epss) max_epss = epss_val;
    }

    const maturity_raw = entry?.contributingFactors?.exploitMaturity || [];
    const maturity_parts = String.split(Array.join(maturity_raw, ','), ',');
    for (let k = 0; k < Array.length(maturity_parts); k++) {
      const m = String.trim(maturity_parts[k]);
      if (!m) continue;
      if (!maturity_seen[m]) { maturity_seen[m] = true; Array.push(maturity_list, m); }
      if (m === 'weaponized') has_exploit = true;
    }

    const malware_raw = entry?.contributingFactors?.malwareName || [];
    const malware_parts = String.split(Array.join(malware_raw, ','), ',');
    for (let k = 0; k < Array.length(malware_parts); k++) {
      const m = String.trim(malware_parts[k]);
      if (!m) continue;
      has_malware = true;
      if (!malware_seen[m]) { malware_seen[m] = true; Array.push(malware_list, m); }
    }

    const cisa_arr = entry?.contributingFactors?.cisaVuln || [];
    if (Array.includes(cisa_arr, 'YES')) is_cisa_kev = true;

    const actor_raw = entry?.contributingFactors?.threatActors || [];
    const actor_parts = String.split(Array.join(actor_raw, ','), ',');
    for (let k = 0; k < Array.length(actor_parts); k++) {
      const a = String.trim(actor_parts[k]);
      if (!a) continue;
      if (!actor_seen[a]) { actor_seen[a] = true; Array.push(actor_list, a); }
    }
  }

  if (max_qvs > 0) {
    const scaled = Math.round(max_qvs / 10);
    rec['Vulnerability Priority Rating (VPR)'] = String.from(scaled < 1 ? 1 : scaled);
  }

  if (best_cvss3_score && (rec['CVSS3 Base Score'] === '0.0' || !rec['CVSS3 Base Score'])) {
    rec['CVSS3 Base Score'] = best_cvss3_score;
  }
  if (best_cvss3_vector && !rec['CVSS3 Vector']) {
    rec['CVSS3 Vector'] = best_cvss3_vector;
  }
  if (best_cvss2_score && (rec['CVSS Base Score'] === '0.0' || !rec['CVSS Base Score'])) {
    rec['CVSS Base Score'] = best_cvss2_score;
  }
  if (best_cvss4_score) {
    rec['CVSS4 Base Score'] = best_cvss4_score;
  }
  if (best_cvss4_vector) {
    rec['CVSS4 Vector'] = best_cvss4_vector;
  }

  if (has_exploit) rec['Exploit Available'] = 'true';
  if (has_malware) rec['Exploited by Malware'] = 'true';
  if (is_cisa_kev) rec['In The News'] = 'true';

  const intel_lines = [];
  if (max_epss > 0) {
    Array.push(intel_lines, 'EPSS Score: ' + String.from(max_epss) + ' (' + String.from(Math.round(max_epss * 100)) + '% exploitation probability)');
  }
  if (is_cisa_kev) {
    Array.push(intel_lines, 'CISA Known Exploited Vulnerability: YES');
  }
  if (Array.length(maturity_list) > 0) {
    Array.push(intel_lines, 'Exploit Maturity: ' + Array.join(maturity_list, ', '));
  }
  if (Array.length(malware_list) > 0) {
    Array.push(intel_lines, 'Associated Malware: ' + Array.join(malware_list, ', '));
  }
  if (Array.length(actor_list) > 0) {
    Array.push(intel_lines, 'Threat Actors: ' + Array.join(actor_list, ', '));
  }

  if (Array.length(intel_lines) > 0) {
    const existing_output = rec['Plugin Output'] || '';
    rec['Plugin Output'] = existing_output + (existing_output ? '\n\n' : '') + '--- Qualys Threat Intelligence ---\n' + Array.join(intel_lines, '\n');
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('QVS enrichment complete for ' + Array.length(enriched_records) + ' record(s).');
}

return {
  decision: {
    status: 'continue',
    message: 'QVS enrichment complete. Proceeding to import trigger.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    records: enriched_records
  }
};
```

**Action 7 - Trigger Import Vulnerabilities Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{qualys\_import\_vuln\_trigger\_flow\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (data.status !== 'FINISHED') {
  return {
    decision: {
      status: 'next',
      message: 'Status ' + data.status + ' detected. Skipping to report.'
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status
    }
  };
}

if (!data?.records || Array.length(data.records) === 0) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('No records found for project: ', data?.project_id);
  }
  return {
    decision: {
      status: 'continue',
      message: 'No records to import. Proceeding to add note and clear active scan.'
    },
    data: {
      project_id:  data?.project_id,
      project_name: data?.project_name,
      scan_ref:    data?.scan_ref,
      status:      data.status,
      no_records:  "true"
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Triggering import of ' + Array.length(data.records) + ' record(s) for project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_import_vuln_trigger_flow_id,
    body: {
      project_id: data.project_id,
      records: data.records
    }
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data.status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering import flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error triggering import flow. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Trigger Import Vulnerabilities Flow - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error triggering import flow.'
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully triggered import flow.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status
  }
};
```

**Action 8 - No Findings - Add Note to Project**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}/note
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.flow_status && data.flow_status?.status !== 'NOVULNSFOUND') {
  return {
    decision: {
      status: 'next',
      message: 'Error status detected. Skipping no-findings note.'
    },
    data: data
  };
}

// Only add a note when step 6 set no_records (records array was empty after a successful scan).
if ((!data?.no_records || data.no_records !== 'true') && data?.flow_status?.status !== 'NOVULNSFOUND') {
  return {
    decision: {
      status: 'continue',
      message: 'Findings were imported. Skipping no-findings note.'
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      status:       data.status
    }
  };
}

if (!data?.project_id) {
  const err_msg = 'Error: project_id is missing in no-findings path. Please check the log.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      flow_status: {
        step:    'No Findings - Add Note to Project - Request Script',
        status:  'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const now = Date.format(Date.datetime('now'), 'default');

return {
  decision: {
    status: 'continue',
    message: 'Adding no-findings note to project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id + '/note',
    body: {
      note: 'No vulnerabilities found by Qualys VM Scan.\nScan Reference: ' + data.scan_ref + '\nDate: ' + now,
      is_private: false,
      is_exported_to_report: false
    }
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref:     data.scan_ref,
    status:       data.status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  const err_msg = 'HTTP error creating note on project. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id:   data?.project_id,
      project_name: data?.project_name,
      scan_ref:     data?.scan_ref,
      status:       data?.status,
      flow_status: {
        step:    'No Findings - Add Note to Project - Response Script',
        status:  'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully added no-findings note to project: ' + data?.project_name + '.'
  },
  data: {
    project_id:   data?.project_id,
    project_name: data?.project_name,
    scan_ref:     data?.scan_ref,
    status:       data?.status
  }
};
```

**Action 9 - Clear Active Scan**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/project/{id}
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.flow_status) {
  if (secrets.logging_level === 'debug') {
    Logger.debug(JSON.stringify(data));
  }
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.project_id is missing. Please check the log.'
    }
  };
}

const clear_status = ['FINISHED', 'NOVULNSFOUND', 'CANCELED', 'NOHOSTALIVE'];

// INTERRUPTED and PAUSED to be captured here
if (!Array.includes(clear_status, data.status)){
  return {
    decision: {
      status: 'next',
      message: 'Status ' + (data.status || 'unknown') + ' detected. Skipping to report.'
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status,
      flow_status: data.flow_status
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Clearing qualys_vm_active_scan from project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: 'qualys_vm_active_scan',
          value: null
        }
      ]
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status,
    flow_status: data.flow_status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error clearing active scan field: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error clearing qualys_vm_active_scan from project: ' + data?.project_id + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Clear Active Scan - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error clearing qualys_vm_active_scan from project: ' + (data?.project_id || 'unknown') + '.'
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully cleared qualys_vm_active_scan from project: ' + data?.project_id + '.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    flow_status: data?.flow_status
  }
};
```

**Action 10 - Send Report Email**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/email
* **Headers**:
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.flow_status) {
  if (data.project_name && data.scan_ref && data.project_id) {
    const curr_date = Date.datetime('now');
    const curr_year = Date.format(curr_date, 'yyyy');
    const current_display_date = Date.format(curr_date, 'default');
    const emailHeader = "<!doctype html> <html lang='en' xmlns:v='urn:schemas-microsoft-com:vml' style='color-scheme: light dark'> <head> <meta charset='utf-8'> <meta name='x-apple-disable-message-reformatting'> <meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='format-detection' content='telephone=no, date=no, address=no, email=no, url=no'> <meta name='color-scheme' content='light dark'> <meta name='supported-color-schemes' content='light dark'> <link rel='preconnect' href='https://fonts.googleapis.com'> <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin> <link href='https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;700&display=swap' rel='stylesheet' media='screen'> <style> .body-sub { margin-top: 25px; border-top-width: 1px; padding-top: 25px; border-top-color: #eaeaec; border-top-style: solid; } body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } .sm-w-full { width: 100% !important; } /*@media (prefers-color-scheme: dark) { body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } } :root { color-scheme: light dark; }*/ /*@media (max-width: 600px) { .sm-w-full { width: 100% !important; } }*/ h1 { margin-top: 0; text-align: left; font-size: 24px; font-weight: 700; color: #333333 } p { margin-bottom: 5px; margin-top: 6px; font-size: 16px; line-height: 24px; color: #51545e } .styled-table { width: 600px; border-collapse: collapse; } .styled-table th { border-collapse: collapse; border: 1px solid white; } .styled-table td { border-collapse: collapse; border: 1px solid white; padding-left: 20px; } .styled-button { border: none; border-collapse: collapse; } .styled-button td { border: 1px solid; border-radius: 5px; border-color: transparent; background-color: #469cf0 !important; padding: 15px 30px; } .styled-button a { background-color: #469cf0 !important; display: inline-block; font-size: 17px; color: #ffffff; text-decoration: none; font-family: sans-serif; } </style> <!--[if mso]> <style> .styled-button td { border: 1px solid; border-radius: 5px; border-color: #469cf0; background-color: #469cf0 !important; mso-padding-alt: 15px 30px; } </style> <![endif]--> </head> <body style='margin: 0; width: 100%; background-color: #f2f4f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word'> <div role='article' aria-roledescription='email' aria-label lang='en'> <table class='email-wrapper' style='width: 100%; background-color: #1e293b; font-family: &quot;Inter&quot;, ui-sans-serif, system-ui, -apple-system, &quot;Segoe UI&quot;, sans-serif;' cellpadding='0' cellspacing='0' role='none' > <tr> <td align='center'> <table class='email-content' style='width: 100%' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' class='email-masthead' style='display: flex; justify-content: center; gap: 8px; padding-top: 25px; padding-bottom: 25px; text-align: center; font-size: 16px'> <img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAABnCAYAAADc1TyNAAAEDmlDQ1BrQ0dDb2xvclNwYWNlR2VuZXJpY1JHQgAAOI2NVV1oHFUUPpu5syskzoPUpqaSDv41lLRsUtGE2uj+ZbNt3CyTbLRBkMns3Z1pJjPj/KRpKT4UQRDBqOCT4P9bwSchaqvtiy2itFCiBIMo+ND6R6HSFwnruTOzu5O4a73L3PnmnO9+595z7t4LkLgsW5beJQIsGq4t5dPis8fmxMQ6dMF90A190C0rjpUqlSYBG+PCv9rt7yDG3tf2t/f/Z+uuUEcBiN2F2Kw4yiLiZQD+FcWyXYAEQfvICddi+AnEO2ycIOISw7UAVxieD/Cyz5mRMohfRSwoqoz+xNuIB+cj9loEB3Pw2448NaitKSLLRck2q5pOI9O9g/t/tkXda8Tbg0+PszB9FN8DuPaXKnKW4YcQn1Xk3HSIry5ps8UQ/2W5aQnxIwBdu7yFcgrxPsRjVXu8HOh0qao30cArp9SZZxDfg3h1wTzKxu5E/LUxX5wKdX5SnAzmDx4A4OIqLbB69yMesE1pKojLjVdoNsfyiPi45hZmAn3uLWdpOtfQOaVmikEs7ovj8hFWpz7EV6mel0L9Xy23FMYlPYZenAx0yDB1/PX6dledmQjikjkXCxqMJS9WtfFCyH9XtSekEF+2dH+P4tzITduTygGfv58a5VCTH5PtXD7EFZiNyUDBhHnsFTBgE0SQIA9pfFtgo6cKGuhooeilaKH41eDs38Ip+f4At1Rq/sjr6NEwQqb/I/DQqsLvaFUjvAx+eWirddAJZnAj1DFJL0mSg/gcIpPkMBkhoyCSJ8lTZIxk0TpKDjXHliJzZPO50dR5ASNSnzeLvIvod0HG/mdkmOC0z8VKnzcQ2M/Yz2vKldduXjp9bleLu0ZWn7vWc+l0JGcaai10yNrUnXLP/8Jf59ewX+c3Wgz+B34Df+vbVrc16zTMVgp9um9bxEfzPU5kPqUtVWxhs6OiWTVW+gIfywB9uXi7CGcGW/zk98k/kmvJ95IfJn/j3uQ+4c5zn3Kfcd+AyF3gLnJfcl9xH3OfR2rUee80a+6vo7EK5mmXUdyfQlrYLTwoZIU9wsPCZEtP6BWGhAlhL3p2N6sTjRdduwbHsG9kq32sgBepc+xurLPW4T9URpYGJ3ym4+8zA05u44QjST8ZIoVtu3qE7fWmdn5LPdqvgcZz8Ww8BWJ8X3w0PhQ/wnCDGd+LvlHs8dRy6bLLDuKMaZ20tZrqisPJ5ONiCq8yKhYM5cCgKOu66Lsc0aYOtZdo5QCwezI4wm9J/v0X23mlZXOfBjj8Jzv3WrY5D+CsA9D7aMs2gGfjve8ArD6mePZSeCfEYt8CONWDw8FXTxrPqx/r9Vt4biXeANh8vV7/+/16ffMD1N8AuKD/A/8leAvFY9bLAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAJYoAMABAAAAAEAAABnAAAAAOLtLv4AADQ/SURBVHgB7Z0HvBXF9cfvAykC9oqiYgNjj778Y9eoqAkW1GgUC2JDjGKsMcbYS2woYAmKLfZeExVr7CViF0GsKFZEEIX7//6e78ll3+zu7N699Z3z+fze3p05c+bMb2d3z8zO7tvlanjy+XwXsE4SF9GfH6yWpIzpGgPGgDFgDBgDxoAxUCcMkDQNfB7cnKTB6PcCX4ELQc8kZU3XGDAGjAFjwBgwBoyBhmSAoGgVcCOYCSRXJmko+j3BFBVEPgKHg65JbJiuMWAMGAPGgDFgDBgDdcUAgU9ncBAYD1xyVpLGYUBrsD5xGSLtTrBWEpumawwYA8aAMWAMGAPGQF0xQLCzLXgaRMkxSRqFIS2Mfze6wfelaf3WEknsmq4xYAwYA8aAMWAMGAM1zQBByjbgaeCRfZM0BoNNQIvj4+RjFI4Atj4rCcGmawyUIwMEOGyfIIMdAAAASUVORK5CYII=' alt='AttackForge Logo' style='width: 200px'> </td> </tr> <tr> <td class='email-body' style='width: 100%; background-color: #fff'> <table align='center' class='email-body_inner sm-w-full' style='margin-left: auto; margin-right: auto; width: 800px; background-color: #fff' cellpadding='0' cellspacing='0' role='none'> <tr> <td style='padding: 45px'>";
    const emailFooter = "</td> </tr> </table> </td> </tr> <tr> <td> <table align='center' class='email-footer sm-w-full' style='margin-left: auto; margin-right: auto; width: 570px; text-align: center' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' style='padding: 45px; font-size: 16px'> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'> This is a system generated email and reply is not monitored. <br> For any queries please reach out to <span style='text-decoration-line: underline'>support@attackforge.com</span> </p> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'>&copy; 2018-"+ curr_year +" AttackForge&reg;</p> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </div> </body> </html>";

    const content = '<p><strong>Attention: Qualys Scan Poll and Import Flow has detected an error.</strong></p>'
    + '<p>Please check the AttackForge Flow log and Qualys scan log for details.</p>'
    + '<br>'
    + '<table class="styled-table" align="center">'
    + '<tr><th width="200">Field</th><th width="200">Value</th></tr>'
    + '<tr><td> Scan Reference </td><td>' + data.scan_ref + '</td></tr>'
    + '<tr><td> Step </td><td>' + data.flow_status.step + '</td></tr>'
    + '<tr><td> Status </td><td>' + data.flow_status.status + '</td></tr>'
    + '<tr><td> Message </td><td>' + data.flow_status.message + '</td></tr>'
    + '<tr><td> Time </td><td>' + current_display_date + '</td></tr>'
    + '</table>'
    + '<br>'
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.af_hostname+'/flows/'+secrets.current_flow_id+'/home">View Flow Run</a></td></tr></table>'
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.qualys_web_base_url+'/fo/scan/scanList.php">View Qualys Scan Board</a></td></tr></table>'
    + '<br>';

    return {
      decision: {
        status: 'continue',
        message: 'Sending error report email.',
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/email',
        body: {
          to: [secrets.admin_email],
          subject: '[Flow Notification] Non successful Scan reference discovered from Qualys VM Scanning',
          html: emailHeader + content + emailFooter
        },
      }
    };
  } 
  else {
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_name, scan_ref, project_id must all be present in data. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No flow_status found. Current scan status: ' + (data?.status || 'unknown') + '. Exiting the flow.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  return {
    decision: {
      status: 'abort',
      message: 'Error sending error email',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      flow_status: {
        step: 'Send Report Email- Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error sending report email to: ' + (secrets.admin_email || 'unknown') + '.'
      }
    },
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully sent email to admin. Reached end of the flow.',
  }
};
```

## Qualys VM & WAS - Import Vulnerabilities \[Step 5 of 5]

<figure><img src="/files/1WDSPS3LCdX4CtZ0Vu7E" alt=""><figcaption></figcaption></figure>

The purpose of this example is to import vulnerabilities from a Qualys Web Application or VM scan.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **HTTP Trigger**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * import\_to\_library - optional AttackForge Writeup library. Must be either "Imported Vulnerabilities", "Main Vulnerabilities", "Project Vulnerabilities" or key for a custom library (if custom libraries are used).
  * logging\_level - set to "debug" for additional logging

**Action 1 - Validate Transform Group**

* **Script**:

```javascript
if (!data?.jsonBody?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.jsonBody.project_id is required. Please check the log.'
    }
  };
}

// Detect source: Qualys VM sends records[], Qualys WAS sends final_result[]
const isVM  = data.jsonBody?.records && Array.isArray(data.jsonBody.records);
const isWAS = !isVM && data.jsonBody?.final_result && Array.isArray(data.jsonBody.final_result);

if (!isVM && !isWAS) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('jsonBody: ', JSON.stringify(data.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Unable to detect source format. Expected data.jsonBody.records (VM) or data.jsonBody.final_result (WAS). Please check the log.'
    }
  };
}

const sourceType = isVM ? 'VM' : 'WAS';

// Normalize into flat work items
const items = [];

if (isVM) {
  for (let i = 0; i < Array.length(data.jsonBody.records); i++) {
    Array.push(items, {
      type: 'VM',
      record: data.jsonBody.records[i]
    });
  }
} 
else {
  // WAS: each entry in final_result is a scan wrapper { scan: {...}, findings: [...] }
  for (let r = 0; r < Array.length(data.jsonBody.final_result); r++) {
    const raw = data.jsonBody.final_result[r];
    const entry         = raw?.scan ? raw : (raw?.data || raw);
    const scanId        = entry?.scan?.scan_id    || '';
    const target        = entry?.scan?.target      || '';
    const scanStartedAt = entry?.scan?.started_at  || '';

    if (entry?.findings && Array.isArray(entry.findings)) {
      for (let i = 0; i < Array.length(entry.findings); i++) {
        Array.push(items, {
          type:           'WAS',
          finding:        entry.findings[i],
          scanId:         scanId,
          target:         target,
          scanStartedAt:  scanStartedAt
        });
      }
    }
  }
}

if (Array.length(items) === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No ' + sourceType + ' items found in input. Nothing to import.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug(sourceType + ' item count: ' + Array.length(items));
}

// Group by qid
const groupKeys  = [];
const groupVulns = [];

for (let i = 0; i < Array.length(items); i++) {
  const item = items[i];
  let qid;
  let priority;
  let assetName;

  if (item.type === 'VM') {
    qid       = item.record['QualysId'] || '0';
    priority  = mapPriorityVM(item.record['Risk']);
    assetName = item.record['FQDN'] || item.record['Host'] || item.record['IP Address'] || 'Unknown';
  } 
  else {
    qid       = item.finding.qid != null ? item.finding.qid + '' : '0';
    priority  = mapPriorityWAS(item.finding.risk_factor);
    assetName = item.target || item.finding.uri || 'Unknown';
  }

  let idx = -1;
  for (let g = 0; g < Array.length(groupKeys); g++) {
    if (groupKeys[g] === qid) {
      idx = g;
      break;
    }
  }

  if (idx === -1) {
    // New group — build base vuln
    const vuln = item.type === 'VM'
      ? buildVMVuln(item.record, qid, priority, assetName)
      : buildWASVuln(item.finding, item.scanId, item.target, item.scanStartedAt, qid, priority, assetName);

    const note = item.type === 'VM'
      ? buildVMNote(item.record, assetName)
      : buildWASNote(item.finding);

    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }

    Array.push(groupKeys, qid);
    Array.push(groupVulns, vuln);
  } 
  else {
    // Existing group — add asset + evidence
    const vuln = groupVulns[idx];

    let assetExists = false;
    for (let a = 0; a < Array.length(vuln.affected_assets); a++) {
      if (vuln.affected_assets[a].assetName === assetName) {
        assetExists = true;
        break;
      }
    }
    if (!assetExists) {
      Array.push(vuln.affected_assets, { assetName: assetName });
    }

    const note = item.type === 'VM'
      ? buildVMNote(item.record, assetName)
      : buildWASNote(item.finding);

    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }
  }
}

if (Array.length(groupVulns) === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No vulnerabilities to import after grouping.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Grouped ' + Array.length(items) + ' ' + sourceType + ' items into ' + Array.length(groupVulns) + ' vulnerabilities.');
}

return {
  decision: {
    status: 'continue',
    message: 'Grouped ' + Array.length(items) + ' ' + sourceType + ' items into ' + Array.length(groupVulns) + ' vulnerabilities.'
  },
  data: {
    vulns_to_import: groupVulns,
    total_count:     Array.length(groupVulns),
    imported_count:  0,
    project_id:      data.jsonBody.project_id,
    source_type:     sourceType
  }
};

// ── VM builder ──

function buildVMVuln(record, qid, priority, assetName) {
  const title = record['Name'] || 'QID ' + qid;

  const tags = [];
  if (record['CVE'] && String.length(record['CVE']) > 0) {
    const cves = String.split(record['CVE'], ',');
    for (let j = 0; j < Array.length(cves); j++) {
      const cve = String.trim(cves[j]);
      if (String.length(cve) > 0) {
        Array.push(tags, cve);
      }
    }
  }

  const custom_fields = [
    { key: 'qualys_ID',        value: qid },
    { key: 'qualys_scan_type',  value: 'VM' },
    { key: 'category',     value: record['Category']  || '' },
    { key: 'exploit_available', value: record['Exploit Available'] || 'false' },
    { key: 'patch_available',   value: record['Patch Available']   || 'false' },
    { key: 'remote', value: record['Remote'] || 'false'}
  ];

  if (record['CVSS4 Vector'] && String.length(record['CVSS4 Vector']) > 0) {
    Array.push(tags, record['CVSS4 Vector']);
  } 
  else if (record['CVSS3 Vector'] && String.length(record['CVSS3 Vector']) > 0) {
    Array.push(tags, record['CVSS3 Vector']);
  }

  const cvss4 = Number.parseFloat(record['CVSS4 Base Score'] || '0');
  const cvss3 = Number.parseFloat(record['CVSS3 Base Score'] || '0');
  const cvss2 = Number.parseFloat(record['CVSS Base Score']  || '0');
  const bestCvss = cvss4 > 0 ? cvss4 : cvss3 > 0 ? cvss3 : cvss2;
  let likelihood = 1;
  if (bestCvss > 0) {
    likelihood = Math.round(bestCvss);
    if (likelihood < 1)  likelihood = 1;
    if (likelihood > 10) likelihood = 10;
  }

  let solution = record['Solution'] || '';
  if (solution === 'N/A' || solution === 'n/a') solution = '';

  const is_zeroday = record['Exploit Available'] === 'true' && record['Patch Available'] !== 'true';

  const vuln = {
    projectId:                  data.jsonBody.project_id,
    title:                      title,
    priority:                   priority,
    description:                record['Description'] || record['Synopsis'] || '',
    attack_scenario:            '',
    steps_to_reproduce:         buildVMSteps(record),
    remediation_recommendation: solution,
    likelihood_of_exploitation: likelihood,
    affected_assets:            [{ assetName: assetName }],
    tags:                       tags,
    custom_fields:              custom_fields,
    cvssv4_vector:              record['CVSS4 Vector'] || '',
    is_zeroday:                 is_zeroday,
    import_source:              'Qualys VM',
    import_source_id:           'VM-' + qid,
    is_visible:                 true
  };

  if (secrets?.import_to_library) {
    vuln.import_to_library = secrets.import_to_library;
  }
  return vuln;
}

function buildVMNote(record, assetName) {
  if (record['Result'] && String.length(record['Result']) > 0) {
    let prefix = '[' + assetName;
    if (record['Port'] && record['Port'] !== '0' && String.length(record['Port']) > 0) {
      prefix = prefix + ':' + record['Port'];
      if (record['Protocol'] && String.length(record['Protocol']) > 0) {
        prefix = prefix + '/' + record['Protocol'];
      }
    }
    prefix = prefix + ']';
    return {
      note: prefix + '\n' + record['Result'],
      type: 'PLAINTEXT'
    };
  }
  return null;
}

function buildVMSteps(record) {
  const parts = [];
  if (record['OS'] && String.length(record['OS']) > 0) {
    Array.push(parts, 'OS: ' + record['OS']);
  }
  if (record['Port'] && record['Port'] !== '0' && String.length(record['Port']) > 0) {
    let portLine = 'Port: ' + record['Port'];
    if (record['Protocol'] && String.length(record['Protocol']) > 0) {
      portLine = portLine + '/' + record['Protocol'];
    }
    Array.push(parts, portLine);
  }
  return Array.join(parts, '\n');
}

// ── WAS builder ──
//   Expected finding fields from Qualys WAS poll flow:
//   qid, name, risk_factor (critical/high/medium/low/info), description, solution,
//   cves (string[]), cvss (float), cvss_vector, cvss3 (float), cvss3_vector,
//   uri, evidence, proof, request_headers, response_headers, payload,
//   input_name, input_type, category, cwe (int[]),
//   owasp ([{ year, category }])

function buildWASVuln(finding, scanId, target, scanStartedAt, qid, priority, assetName) {
  const title = finding.name || 'QID ' + qid;

  let remediation = finding.solution || '';
  if (remediation === 'N/A' || remediation === 'n/a') remediation = '';

  const tags = [];
  if (finding.cves && Array.isArray(finding.cves)) {
    for (let j = 0; j < Array.length(finding.cves); j++) {
      const cve = String.trim(finding.cves[j]);
      if (String.length(cve) > 0) {
        Array.push(tags, cve);
      }
    }
  }
  if (finding.cwe && Array.isArray(finding.cwe)) {
    for (let j = 0; j < Array.length(finding.cwe); j++) {
      Array.push(tags, 'CWE-' + finding.cwe[j]);
    }
  }
  if (finding.owasp && Array.isArray(finding.owasp)) {
    for (let j = 0; j < Array.length(finding.owasp); j++) {
      const o = finding.owasp[j];
      if (o && o.year && o.category) {
        Array.push(tags, 'OWASP-' + o.year + '-' + o.category);
      }
    }
  }

  const custom_fields = [
    { key: 'qualys_ID',        value: qid },
    { key: 'qualys_finding_id', value: String.from(finding.finding_id || '') },
    { key: 'qualys_scan_type',  value: 'WAS' },
    { key: 'scan_id',           value: scanId },
    { key: 'target_url',        value: target },
    { key: 'category',     value: finding.category || '' }
  ];

  if (finding.cvss3 !== null && finding.cvss3 !== undefined) {
    Array.push(custom_fields, { key: 'cvss3_base_score', value: finding.cvss3 + '' });
  }
  if (finding.cvss3_vector && String.length(finding.cvss3_vector) > 0) {
    Array.push(custom_fields, { key: 'cvss3_vector', value: finding.cvss3_vector });
  }
  if (finding.cvss !== null && finding.cvss !== undefined) {
    Array.push(custom_fields, { key: 'cvss2_base_score', value: finding.cvss + '' });
  }
  if (finding.cvss_vector && String.length(finding.cvss_vector) > 0) {
    Array.push(custom_fields, { key: 'cvss2_vector', value: finding.cvss_vector });
  }
  if (finding.cwe && Array.isArray(finding.cwe) && Array.length(finding.cwe) > 0) {
    Array.push(custom_fields, { key: 'cwe', value: Array.join(finding.cwe, ', ') });
  }
  if (finding.owasp && Array.isArray(finding.owasp) && Array.length(finding.owasp) > 0) {
    const owaspLabels = [];
    for (let j = 0; j < Array.length(finding.owasp); j++) {
      const o = finding.owasp[j];
      if (o && o.year && o.category) {
        Array.push(owaspLabels, o.year + '-' + o.category);
      }
    }
    if (Array.length(owaspLabels) > 0) {
      Array.push(custom_fields, { key: 'owasp', value: Array.join(owaspLabels, ', ') });
    }
  }
  if (finding.wasc && String.length(finding.wasc) > 0) {
    Array.push(custom_fields, { key: 'wasc', value: finding.wasc });
  }
  if (finding.group && String.length(finding.group) > 0) {
    Array.push(custom_fields, { key: 'qualys_group', value: finding.group });
  }
  if (finding.last_detected_at && String.length(finding.last_detected_at) > 0) {
    Array.push(custom_fields, { key: 'last_detected_at', value: finding.last_detected_at });
  }

  // Likelihood from CVSS3 > CVSS2 > default 1
  const cvssScore = finding.cvss3 !== null && finding.cvss3 !== undefined ? finding.cvss3
                  : finding.cvss  !== null && finding.cvss  !== undefined ? finding.cvss
                  : null;
  let likelihood = 1;
  if (cvssScore !== null) {
    likelihood = Math.round(cvssScore);
    if (likelihood < 1)  likelihood = 1;
    if (likelihood > 10) likelihood = 10;
  }

  const vuln = {
    projectId:                  data.jsonBody.project_id,
    title:                      title,
    priority:                   priority,
    description:                finding.description || finding.synopsis || '',
    attack_scenario:            finding.consequence || '',
    steps_to_reproduce:         buildWASSteps(finding),
    remediation_recommendation: remediation,
    likelihood_of_exploitation: likelihood,
    affected_assets:            [{ assetName: assetName }],
    tags:                       tags,
    custom_fields:              custom_fields,
    is_zeroday:                 false,
    import_source:              'Qualys WAS',
    import_source_id:           'WAS-' + qid,
    created:                    scanStartedAt,
    is_visible:                 true
  };

  if (secrets?.import_to_library) {
    vuln.import_to_library = secrets.import_to_library;
  }
  return vuln;
}

function buildWASNote(finding) {
  const parts = [];
  if (finding.uri && String.length(finding.uri) > 0) {
    Array.push(parts, '[' + finding.uri + ']');
  }
  if (finding.evidence && String.length(finding.evidence) > 0) {
    Array.push(parts, finding.evidence);
  }
  if (finding.proof && String.length(finding.proof) > 0) {
    Array.push(parts, 'Proof: ' + finding.proof);
  }
  if (finding.request_headers && String.length(finding.request_headers) > 0) {
    Array.push(parts, 'Request Headers:\n' + finding.request_headers);
  }
  if (finding.response_headers && String.length(finding.response_headers) > 0) {
    Array.push(parts, 'Response Headers:\n' + finding.response_headers);
  }
  if (finding.payload && String.length(finding.payload) > 0) {
    Array.push(parts, 'Payload: ' + finding.payload);
  }
  if (Array.length(parts) > 0) {
    return {
      note: Array.join(parts, '\n'),
      type: 'PLAINTEXT'
    };
  }
  return null;
}

function buildWASSteps(finding) {
  const parts = [];
  if (finding.uri && String.length(finding.uri) > 0) {
    Array.push(parts, 'URL: ' + finding.uri);
  }
  if (finding.input_name && String.length(finding.input_name) > 0) {
    let inputLine = 'Input: ' + finding.input_name;
    if (finding.input_type && String.length(finding.input_type) > 0) {
      inputLine = inputLine + ' (' + finding.input_type + ')';
    }
    Array.push(parts, inputLine);
  }
  return Array.join(parts, '\n');
}

// ── Priority helpers ──

function mapPriorityVM(risk) {
  if (risk === 'Critical') return 'Critical';
  if (risk === 'High')     return 'High';
  if (risk === 'Medium')   return 'Medium';
  if (risk === 'Low')      return 'Low';
  return 'Info';
}

function mapPriorityWAS(risk_factor) {
  if (risk_factor === 'critical') return 'Critical';
  if (risk_factor === 'high')     return 'High';
  if (risk_factor === 'medium')   return 'Medium';
  if (risk_factor === 'low')      return 'Low';
  return 'Info';
}
```

**Action 2 - Import Vulnerabilities**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (!data?.vulns_to_import || Array.length(data.vulns_to_import) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to import. Skipping.'
    },
    data: data
  };
}

const vuln      = data.vulns_to_import[0];
const remaining = Array.length(data.vulns_to_import) - 1;

if (secrets.logging_level === 'debug') {
  Logger.debug('Importing vulnerability: ' + (vuln.title || 'Untitled') + ' (' + Array.length(vuln.affected_assets) + ' asset(s)). ' + remaining + ' remaining.');
}

return {
  decision: {
    status: 'continue',
    message: 'Importing vulnerability (' + (data.imported_count + 1) + '/' + data.total_count + '): ' + (vuln.title || 'Untitled') + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability',
    body: vuln
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Import error response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error importing vulnerability. HTTP status: ' + (response?.statusCode || 'unknown') + '. Imported so far: ' + (data?.imported_count || 0) + '/' + (data?.total_count || 0) + '. Please check the log.'
    }
  };
}
if (data?.vulns_to_import) {
  Array.splice(data.vulns_to_import, 0, 1);
}
if (data?.imported_count !== undefined) {
  data.imported_count = (data.imported_count || 0) + 1;
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability imported. Progress: ' + data?.imported_count + '/' + data?.total_count + '.');
}

if (data?.vulns_to_import !== undefined && Array.length(data.vulns_to_import) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Imported (' + data?.imported_count + '/' + data?.total_count + '). Processing next vulnerability.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'finish',
    message: 'All ' + data?.source_type + ' vulnerabilities imported successfully. Total: ' + data?.imported_count + '.'
  }
};
```


# Slack

## Create Slack Message

{% embed url="<https://youtu.be/UxByLi-wNKI?si=Jvrwj_yYnw4AbZWX>" %}

The purpose of this example is to create a message in a [Slack Channel](https://slack.com/intl/en-au/help/articles/360017938993-What-is-a-channel) when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event**: Vulnerability Created
* **Secrets**:
  * slack\_auth - your [Slack Bot Token](https://api.slack.com/tutorials/tracks/getting-a-token)

**Action 1 - Post Slack Message**

* **Method**: POST
* **URL**: <https://slack.com/api/chat.postMessage>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = slack\_auth
* **Request Script**:

```javascript
return {
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  let afProjectId;
  let afProjectName;
  let channel;

  if (data.vulnerability_projects) {
    for (let i = 0; i < data.vulnerability_projects.length; i++) {
      if (data.vulnerability_projects[i].name) {
        afProjectName = data.vulnerability_projects[i].name;
      }

      if (data.vulnerability_projects[i].id) {
        afProjectId = data.vulnerability_projects[i].id;
      }

      if (data.vulnerability_projects[i].custom_fields) {
        for (let j = 0; j < data.vulnerability_projects[i].custom_fields.length; j++) {
          if (data.vulnerability_projects[i].custom_fields[j].key === 'slack_channel') {
            channel = data.vulnerability_projects[i].custom_fields[j].value;
            break;
          }
        }
      }
    }
  }

  let text = '';

  if (data.vulnerability_priority) {
    text += '[' + data.vulnerability_priority + '] ';
  }

  text += 'Vulnerability ';

  if (data.vulnerability_title) {
    text += '[' + data.vulnerability_title + '] ';
  }

  text += 'discovered in Project';

  if (afProjectName) {
    text += ' [' + afProjectName + ']';
  }

  if (afProjectId) {
    text += ' - <https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + '|View Vulnerability>';
  }

  return {
    channel: channel,
    text: text
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json; charset=utf-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (body?.ok === true) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'SLACK message not posted',
    },
  };   
}
```


# Microsoft Teams

## Create Teams Message

{% embed url="<https://youtu.be/vrplMhqzLNk?si=0OebTfCAkIGErnQ>\_" %}

The purpose of this example is to create a message in a [Teams Channel](https://learn.microsoft.com/en-us/microsoftteams/teams-channels-overview) when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event**: Vulnerability Created
* **Secrets**:
  * teams\_auth - your [Incoming Web Hook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498)

**Action 1 - Post Teams Message**

* **Method**: POST
* **URL**: \<YOUR-INCOMING-WEBHOOK>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = \*/\*
* **Request Script**:

```javascript
let afProjectId;
let afProjectName;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id && data.vulnerability_projects[i].name) {
      afProjectId = data.vulnerability_projects[i].id;
      afProjectName = data.vulnerability_projects[i].name;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is falsy'
    }
  };
}

if (!afProjectName) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectName is falsy'
    }
  };
}

let text = '';

if (data.vulnerability_priority) {
  text += '[' + data.vulnerability_priority + '] ';
}

text += 'Vulnerability ';

if (data.vulnerability_title) {
  text += '[' + data.vulnerability_title + '] ';
}

text += 
  'discovered in Project' + 
  ' [' + afProjectName + ']' + 
  ' - [View Vulnerability](https://afe1.attackforge.dev/projects/' + afProjectId + '/vulnerabilities/' + data.vulnerability_id + ')';


return {
  request: {
    url: 'https://prod-26.australiasoutheast.logic.azure.com:443/workflows/e25ebc22ccf5438190dc46a087450e5c/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=' + secrets.sig,
    body: {
      type: 'message',
      attachments: [
        {
          contentType: 'application/vnd.microsoft.card.adaptive',
          content: {
            $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
            type: 'AdaptiveCard',
            version: '1.2',
            body: [
              {
                type: 'TextBlock',
                text: text
              }
            ]
          }
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode === 202) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'TEAMS message not posted',
    },
  };   
}
```


# Microsoft PowerBI

## Send Vulnerability to PowerBI

{% embed url="<https://youtu.be/atT4u8HHp4s?si=pkUBRs4PCxomj40x>" %}

The purpose of this example is to send data to [PowerBI](https://app.powerbi.com) when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event**: Vulnerability Created
* **Secrets**:
  * powerbi\_key - your [Push Semantic Model - Push URL](https://learn.microsoft.com/en-us/power-bi/connect-data/service-real-time-streaming)

**Action 1 - Send Vulnerability to PowerBI**

* **Method**: POST
* **URL**: \<YOUR-PUSH-URL>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
return {
  request: {
    url: 'https://api.powerbi.com/beta/544b6efc-1149-4f8c-a17a-cad3c50cd5f8/datasets/d5ff54b4-2be2-4e02-baa2-9c6aa20a6c55/rows?experience=power-bi&key=' + secrets.powerbi_key,
    body: [
      {
        vuln_title: data.vulnerability_title,
        vuln_priority: data.vulnerability_priority
      }
    ]
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode === 200) {
  return {
    decision: 'continue'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'PowerBI data not posted',
    },
  };   
}
```


# Salesforce

## Create Salesforce Opportunity

{% embed url="<https://youtu.be/15KbJKPVrtM?si=OnfvPpcM9jDG1v2F>" %}

The purpose of this example is to create a [Salesforce Opportunity](https://help.salesforce.com/s/articleView?id=sales.opportunities.htm\&type=5) when a Project is requested in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event**: Project Requested
* **Secrets**:
  * sf\_client\_id - your [Salesforce OAuth Client Id](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm\&type=5)
  * sf\_client\_secret - your [Salesforce OAuth Client Secret](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm\&type=5)
  * sf\_password - Your [Salesforce User Password](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm\&type=5)
  * sf\_username - Your [SalesForce Username](https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_flows.htm\&type=5)

**Action 1 - Get Salesforce OAuth Token**

* **Method**: POST
* **URL**: <https://login.salesforce.com/services/oauth2/token>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
return {
  request: {
    body: 'grant_type=password&client_id=' + secrets.sf_client_id + '&client_secret=' + secrets.sf_client_secret + '&username=' + secrets.sf_username + '&password=' + secrets.sf_password
  }, 
  data: {
    project_request: data
  } 
};
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (body?.access_token) {
  return {
    data: {
      project_request: data.project_request,
      sf_access_token: body.access_token
    }  
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce access token not found'
    }
  };
}
```

**Action 2 - Create Salesforce Opportunity**

* **Method**: POST
* **URL**: https\://\<YOUR-SALESFORCE>/services/data/v63.0/sobjects/Opportunity
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.project_request && data?.sf_access_token) {
  let closeDate = '2030-12-31';
  
  if (data.project_request.project_request_end_date) {
    closeDate = String.slice(data.project_request.project_request_end_date, 0, 10);
  }

  return {
    request: {
      headers: {
        'Authorization': 'Bearer ' + data.sf_access_token
      },
      body: {
        'Name': data.project_request.project_request_name,
        'StageName': 'Needs Analysis',
        'CloseDate': closeDate
      }
    }
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce access token not found'
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (response.statusCode === 201 && body?.id && body.success === true) {
  return {
    decision: 'finish'
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Salesforce Opportunity not created'
    }
  };
}
```


# Synack

## Import Synack Vulns

<figure><img src="/files/wGtcUloS4EHrD04S2bfT" alt=""><figcaption></figcaption></figure>

The purpose of this example is to import vulnerabilities from [Synack](https://www.synack.com/) on a time-based schedule.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Type**: Schedule
* **Cron String**: Recommend at start of day, every 24-hours (0 9 \* \* \*)
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_synack\_asset\_library\_key** - if assets module is enabled, specify the library where Synack assets will be created. This key can be extracted from the Assets module by navigating to the relevant library.
  * **af\_synack\_project\_custom\_field\_key** - custom field key used to determine if an AF project is a Synack project. Used for correlating Synack vulns through Listing Codename -> Project Code.
  * **af\_synack\_project\_custom\_field\_type** - custom field type used to determine if an AF project is a Synack project. Must be "multi-select" or "select" depending on your custom field configuration. Used for correlating Synack vulns through Listing Codename -> Project Code.
  * **af\_synack\_project\_custom\_field\_value** - custom field value used to determine if an AF project is a Synack project. Used for correlating Synack vulns through Listing Codename -> Project Code.
  * **af\_synack\_writeups\_library\_key** - specify the Writeups library where Synack vulns will be created. This key can be extracted from the Writeups module by navigating to the relevant library.
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **skip\_updating\_synack\_status** - set to "yes" to skip updating Synack vulns to "Ticketed" status
  * **synack\_fetch\_changes\_from\_in\_days** - the number of days (in the past) to fetch vulnerabilities e.g. set to "1" for fetching new/updated Synack vulns in past 24-hours. Should match your Flow Cron String.
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."
  * **synack\_vuln\_acknowledged\_status\_id** - the id for the Synack vuln status for new vulnerabilities. For example, the id for the "Pending Review" status in prod could be "4024". Check this with your Synack account manager.
  * **synack\_vuln\_acknowledged\_status\_name** - the name for the Synack vuln status for new vulnerabilities. E.g. "Pending Review". Check this with your Synack account manager.
  * **synack\_vuln\_identified\_status\_id** - the id for the Synack vuln status for *Ticketed* vulnerabilities. For example, the id for the "Ticketed" status in prod could be "4025". Check this with your Synack account manager.

**Action 1 - Get Synack Vulns**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities?filter\[status\_id]\[]={{synack\_vuln\_identified\_status\_id}}\&page\[size]=5\&page\[number]={page}\&filter\[updated\_since]={isoDate}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.page) {
  data.page = 1;
}
if (!data.updated_since) {
  const getVulnChangesFrom = '-' + secrets.synack_fetch_changes_from_in_days + ' days';
  data.updated_since = Date.datetime('now', getVulnChangesFrom, 'isostring');
}
if (!data.vulns) {
  data.vulns = [];
}

let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities?';
url = url + 'filter[status_id][]=' + Number.parseInt(secrets.synack_vuln_identified_status_id);
url = url + '&page[size]=5';
url = url + '&page[number]=' + data.page;
url = url + '&filter[updated_since]=' + data.updated_since;

return {
  decision: {
    status: 'continue',
    message: 'Fetching Synack vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    page: data.page,
    vulns: data.vulns,
    updated_since: data.updated_since
  }
};
```

* **Response Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.page) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.page',
    }
  };
}
if (!data.updated_since) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.updated_since',
    }
  };
}

if (response.jsonBody
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'No vulns left to process',
    },
    data: {
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody 
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length >= 1
  && response.jsonBody.length <= 5
){
  Logger.info('Found ' + response.jsonBody.length + ' vulns');

  for (let x = 0; x < response.jsonBody.length; x++) {
    const vuln = response.jsonBody[x];
    Array.push(data.vulns, vuln);
  }

  data.page = data.page + 1;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vuln pages',
    },
    data: {
      page: data.page,
      vulns: data.vulns,
      updated_since: data.updated_since
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vulns',
    }
  };
}
```

**Action 2 - Get Synack AF Projects**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/projects?skip={skip}\&limit=50\&q={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (data.vulns && data.vulns.length === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No vulnerabilities to create',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/projects?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';

if (secrets.af_synack_project_custom_field_key && secrets.af_synack_project_custom_field_value) {
  if (secrets.af_synack_project_custom_field_type === 'multi-select') {
    url = url + '&q={custom_fields: {$elemMatch: {name: { $eq: "' + 
      secrets.af_synack_project_custom_field_key + '" }, value: { $in: ["' + 
      secrets.af_synack_project_custom_field_value + '"] } } } }';
  }
  else if (secrets.af_synack_project_custom_field_type === 'select') {
    url = url + '&q={custom_fields: {$elemMatch: {name: { $eq: "' + 
      secrets.af_synack_project_custom_field_key + '" }, value: { $eq: "' + 
      secrets.af_synack_project_custom_field_value + '" } } } }';
  }
}

if (!data.synackListingIdToAFProjectId) {
  data.synackListingIdToAFProjectId = {};
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Synack projects',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
    vulns: data.vulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.projects && Array.isArray(response.jsonBody.projects)
  && response.jsonBody.projects.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all projects',
    },
    data: {
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody?.projects && Array.isArray(response.jsonBody.projects)
  && response.jsonBody.projects.length <= 50
){
  Logger.info('Found ' + response.jsonBody.projects.length + ' Synack projects');

  for (let x = 0; x < response.jsonBody.projects.length; x++) {
    const project = response.jsonBody.projects[x];

    if (data.synackListingIdToAFProjectId && project.project_code && project.project_id) {
      data.synackListingIdToAFProjectId[project.project_code] = project.project_id;
    }
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more Synack projects',
    },
    data: {
      skip: data.skip,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retrieving Synack projects',
    }
  };
}
```

**Action 3 - Get Pending AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?pendingVulnerabilities=true\&skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?pendingVulnerabilities=true';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + '&skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

if (!data.existingVulns) {
  data.existingVulns = [];
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching pending vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
    vulns: data.vulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all pending vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' pending vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more pending vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving pending vulns',
    }
  };
}
```

**Action 4 - Get Visible AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching visible vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
    vulns: data.vulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all visible vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' visible vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more visible vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving visible vulns',
    }
  };
}
```

**Action 5 - Convert Markdown to Rich Text for Update Vulns**

* **Method**: POST
* **URL**: https\://{{af-tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (!data.vulnsToUpdate && data.existingVulns.length > 0) {
  data.vulnsToUpdate = [];

  const synackIdToExistingVuln = {};
  
  for (let x = 0; x < data.existingVulns.length; x++) {
    const vuln = data.existingVulns[x];

    if (vuln.vulnerability_custom_fields) {
      for (let y = 0; y < vuln.vulnerability_custom_fields.length; y++) {
        const customField = vuln.vulnerability_custom_fields[y];

        if (customField.key === 'synack_vuln_id' && customField.value){
          synackIdToExistingVuln[customField.value] = vuln;
        }
      }
    }
  }

  for (let x = 0; x < data.vulns.length; x++) {
    const vuln = data.vulns[x];

    if (vuln.id && synackIdToExistingVuln[vuln.id]) {
      const existingVuln = synackIdToExistingVuln[vuln.id];
      const updateVuln = {
        vuln_id: synackIdToExistingVuln[vuln.id].vulnerability_id,
        asset_library_ids: [
          secrets.af_synack_asset_library_key
        ]
      };
      if (vuln.listing?.codename && data.synackListingIdToAFProjectId[vuln.listing.codename]) {
        updateVuln.project_id = data.synackListingIdToAFProjectId[vuln.listing.codename];
      }
      else {
        // no matching project in AF to handle the Synack vuln
        continue;
      }

      let updated = false;

      if (vuln.vulnerability_status?.text) {
        let currentStatus;
        if (existingVuln.vulnerability_custom_fields) {
          for (let y = 0; y < existingVuln.vulnerability_custom_fields.length; y++) {
            const customField = existingVuln.vulnerability_custom_fields[y];

            if (customField.key === 'synack_vuln_status' && customField.value) {
              currentStatus = customField.value;
            }
          }
        }
        
        if (vuln.vulnerability_status.text !== currentStatus) {
          updateVuln.custom_fields = [
            {
              key: "synack_vuln_status",
              value: vuln.vulnerability_status.text
            }
          ];
          updated = true;
        }
      }

      if (vuln.title && existingVuln.vulnerability_title 
        && vuln.title !== existingVuln.vulnerability_title
      ) {
        updateVuln.title = vuln.title;
        updated = true;
      }
      
      if (vuln.description) {
        if (existingVuln.vulnerability_description_html
          && vuln.description !== existingVuln.vulnerability_description_html
        ){
          updateVuln.description = vuln.description;
          //updated = true;
        }
      }
      if (vuln.impact) {
        if (existingVuln.vulnerability_attack_scenario_html
          && vuln.impact !== existingVuln.vulnerability_attack_scenario_html
        ){
          updateVuln.attack_scenario = vuln.impact;
          //updated = true;
        }
      }
      if (vuln.recommended_fix) {
        if (existingVuln.vulnerability_remediation_recommendation_html
          && vuln.recommended_fix !== existingVuln.vulnerability_remediation_recommendation_html
        ){
          updateVuln.remediation_recommendation = vuln.recommended_fix;
          //updated = true;
        }
      }
      if (vuln.exploitable_locations && Array.isArray(vuln.exploitable_locations)) {
        const assets = {};
        const assetComponentMap = {};

        const newAssets = [];

        for (let y = 0; y < vuln.exploitable_locations.length; y++) {
          const asset = vuln.exploitable_locations[y];
          let assetName;

          if (asset.type === 'ip' && asset.address) {
            assetName = asset.address;
          }
          else if (asset.type === 'url' && asset.value) {
            assetName = asset.value;
          }
          else if (asset.type === 'app-location' && asset.value) {
            assetName = asset.value;
          }
          else if (asset.type === 'other' && asset.value) {
            assetName = asset.value;
          }
          else if (asset.value) {
            assetName = asset.value;
          }

          if (assetName && !assets[assetName]) {
            assets[assetName] = {
              components: []
            };
          }

          let component;
          let assetComponent;

          if (assetName && asset.protocol && asset.port) {
            component = asset.protocol + ':' + asset.port;
            assetComponent = assetName + ':' + component;
          }
          if (assetName && assets[assetName]?.components 
            && component && assetComponent && !assetComponentMap[assetComponent]
          ) {
            const newComponent = {
              name: component
            };

            Array.push(assets[assetName].components, newComponent);
            assetComponentMap[assetComponent] = assetComponent;
          }      
        }

        const parsedAssets = Object.keys(assets);

        for (let y = 0; y < Array.length(parsedAssets); y++) {
          const key = parsedAssets[y];
          const value = assets[key];

          const newAsset = {
            assetName: key,
            components: assets[key].components
          };

          if (newAsset.assetName) {
            Array.push(newAssets, newAsset);
          }
        }

        const existingAssets = {};
        if (existingVuln.vulnerability_affected_assets) {
          for (let y = 0; y < existingVuln.vulnerability_affected_assets.length; y++) {
            const existingAsset = existingVuln.vulnerability_affected_assets[y];

            if (existingAsset.asset?.name) {
              existingAssets[existingAsset.asset.name] = existingAsset.asset.name;
            } 
          }
        }

        let assetsChanged = false;
        for (let y = 0; y < newAssets.length; y++) {
          if (newAssets[y].assetName && !existingAssets[newAssets[y].assetName]) {
            assetsChanged = true;
          }
        }

        if (assetsChanged) {
          updateVuln.affected_assets = newAssets;
          updated = true;
        }
      }

      let steps_to_reproduce = '';
      if (vuln.validation_steps && Array.isArray(vuln.validation_steps)) {
        for (let x = 0; x < vuln.validation_steps.length; x++) {
          const step = vuln.validation_steps[x];

          if (step.detail) {
            steps_to_reproduce = steps_to_reproduce + step.detail + '\n\n';
          }
        }
      }
      if (vuln.http_requests && Array.isArray(vuln.http_requests)) {
        for (let x = 0; x < vuln.http_requests.length; x++) {
          steps_to_reproduce = steps_to_reproduce + 'HTTP Request\n\n';
          const httpRequest = vuln.http_requests[x];
          
          if (httpRequest.http_request) {
            steps_to_reproduce = steps_to_reproduce 
              + 'Request: ' + httpRequest.http_request + '\n\n';
          }
          if (httpRequest.vuln_param) {
            steps_to_reproduce = steps_to_reproduce 
              + 'Vulnerable Parameter: ' + httpRequest.vuln_param + '\n\n';
          }
          if (httpRequest.attack_payload) {
            steps_to_reproduce = steps_to_reproduce 
              + 'Attack Payload: ' + httpRequest.attack_payload + '\n\n';
          }
        }
      }
      if (steps_to_reproduce && existingVuln.vulnerability_steps_to_reproduce_HTML
        && steps_to_reproduce !== existingVuln.vulnerability_steps_to_reproduce_HTML
      ) {
        updateVuln.steps_to_reproduce = steps_to_reproduce;
        //updated = true;
      }
      let tags = [];
      let priority;
      let likelihood_of_exploitation;
      if (vuln.cvss_version === '3.1') {
        if (vuln.cvss_final) {
          const tag = 'CVSSv3.1 Base Score: ' + vuln.cvss_final;
          Array.push(tags, tag);
        }
        if (vuln.cvss_vector) {
          const tag = vuln.cvss_vector;
          Array.push(tags, tag);
        }
        if (vuln.cvss_final) {
          if (vuln.cvss_final > 0 && vuln.cvss_final <= 3.9) {
            priority = 'Low';
            likelihood_of_exploitation = 2;
          }
          else if (vuln.cvss_final >= 4 && vuln.cvss_final <= 6.9) {
            priority = 'Medium';
            likelihood_of_exploitation = 4;
          }
          else if (vuln.cvss_final >= 7 && vuln.cvss_final <= 8.9) {
            priority = 'High';
            likelihood_of_exploitation = 7;
          }
          else if (vuln.cvss_final >= 9 && vuln.cvss_final <= 10) {
            priority = 'Critical';
            likelihood_of_exploitation = 9;
          }
        }
      }
      if (likelihood_of_exploitation && existingVuln.vulnerability_likelihood_of_exploitation 
        && likelihood_of_exploitation !== existingVuln.vulnerability_likelihood_of_exploitation
      ) {
        updateVuln.likelihood_of_exploitation = likelihood_of_exploitation;
        updated = true;
      }
      if (vuln.tag_list && Array.isArray(vuln.tag_list)) {
        for (let x = 0; x < vuln.tag_list.length; x++) {
          const tag = vuln.tag_list[x];

          if (tag.name) {
            Array.push(tags, tag.name);
          }
        }
      }
      if (vuln.cve_ids && Array.isArray(vuln.cve_ids)) {
        for (let x = 0; x < vuln.cve_ids.length; x++) {
          let cve = "";
          let tag = "" + vuln.cve_ids[x];
          if (!String.startsWith(tag, 'CVE-')) {
            cve = cve + 'CVE-';
          }
          cve = cve + vuln.cve_ids[x];
          Array.push(tags, cve);
        }
      }
      if (vuln.cwe_ids && Array.isArray(vuln.cwe_ids)) {
        for (let x = 0; x < vuln.cwe_ids.length; x++) {
          let cwe = "";
          let tag = "" + vuln.cwe_ids[x];
          if (!String.startsWith(tag, 'CWE-')) {
            cwe = cwe + 'CWE-';
          }
          cwe = cwe + vuln.cwe_ids[x];
          Array.push(tags, cwe);
        }
      }
      if (vuln.category?.display) {
        const tag = 'Category: ' + vuln.category.display;
        Array.push(tags, tag);
      }
      if (vuln.category?.parent) {
        const tag = 'Parent Category: ' + vuln.category.parent;
        Array.push(tags, tag);
      }
      if (existingVuln.vulnerability_tags 
        && JSON.stringify(existingVuln.vulnerability_tags) !== JSON.stringify(tags)
      ) {
        updateVuln.tags = tags;
        updated = true;
      }
      if (priority && existingVuln.vulnerability_priority 
        && priority !== existingVuln.vulnerability_priority
      ) {
        updateVuln.priority = priority;
        updated = true;
      }
      
      if (updated === true) {
        Array.push(data.vulnsToUpdate, updateVuln);
      }
    }
  }
}

if (data.vulnsToUpdate && data.vulnsToUpdate.length > 0) {
  const vulnMap = {};
  
  for (let x = 0; x < data.vulnsToUpdate.length; x++) {
    const vuln = data.vulnsToUpdate[x];

    if (vuln.vuln_id) {
      if (vuln.description) {
        vulnMap[vuln.vuln_id + 'description'] = vuln.description;
      }
      if (vuln.attack_scenario) {
        vulnMap[vuln.vuln_id + 'attack_scenario'] = vuln.attack_scenario;
      }
      if (vuln.remediation_recommendation) {
        vulnMap[vuln.vuln_id + 'remediation_recommendation'] = vuln.remediation_recommendation;
      }
      if (vuln.steps_to_reproduce) {
        vulnMap[vuln.vuln_id + 'steps_to_reproduce'] = vuln.steps_to_reproduce;
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Convert markdown to rich text',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
      body: vulnMap
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to update',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody) {
  const vulnMap = response.jsonBody;
  
  for (let x = 0; x < data.vulnsToUpdate.length; x++) {
    const vuln = data.vulnsToUpdate[x];

    if (vuln.vuln_id) {
      if (vulnMap[vuln.vuln_id + 'description']) {
        vuln.description = vulnMap[vuln.vuln_id + 'description'];
      }
      if (vulnMap[vuln.vuln_id + 'attack_scenario']) {
        vuln.attack_scenario = vulnMap[vuln.vuln_id + 'attack_scenario'];
      }
      if (vulnMap[vuln.vuln_id + 'remediation_recommendation']) {
        vuln.remediation_recommendation = vulnMap[vuln.vuln_id + 'remediation_recommendation'];
      }
      if (vulnMap[vuln.vuln_id + 'steps_to_reproduce']) {
        vuln.steps_to_reproduce = vulnMap[vuln.vuln_id + 'steps_to_reproduce'];
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Converted markdown fields to rich text',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error converting markdown to rich text'
    }
  };
}
```

**Action 6 - Update AF Vulnerabilities**

* **Method**: PUT
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (data.vulnsToUpdate && data.vulnsToUpdate[0]) {
  const vulnId = JSON.parse(JSON.stringify(data.vulnsToUpdate[0].vuln_id));
  delete data.vulnsToUpdate[0].vuln_id;

  return {
    decision: {
      status: 'continue',
      message: 'Update vulnerability',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vulnId,
      body: data.vulnsToUpdate[0]
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to update',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  Array.shift(data.vulnsToUpdate);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating vulnerability'
    }
  };
}
```

**Action 7 - Convert Markdown to Rich Text for Create Vulns**

* **Method**: POST
* **URL**: https\://{{af-tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (!data.vulnsToCreate) {
  const synackIdToExistingVuln = {};
  
  for (let x = 0; x < data.existingVulns.length; x++) {
    const vuln = data.existingVulns[x];

    if (vuln.vulnerability_custom_fields) {
      for (let y = 0; y < vuln.vulnerability_custom_fields.length; y++) {
        const customField = vuln.vulnerability_custom_fields[y];

        if (customField.key === 'synack_vuln_id' && customField.value){
          synackIdToExistingVuln[customField.value] = vuln;
        }
      }
    }
  }

  data.vulnsToCreate = [];
  data.vulnsToUpdateToTicketed = [];

  for (let x = 0; x < data.vulns.length; x++) {
    const vuln = data.vulns[x];

    if (vuln.id && !synackIdToExistingVuln[vuln.id]) {
      if (vuln.listing?.codename && data.synackListingIdToAFProjectId[vuln.listing.codename]) {
        Array.push(data.vulnsToCreate, vuln);
      }
    }
  }
}

if (data.vulnsToCreate && data.vulnsToCreate.length > 0) {
  data.vulnsToCreateUpdated = [];

  for (let x = 0; x < data.vulnsToCreate.length; x++) {
    const vuln = data.vulnsToCreate[x];

    const newVuln = {
      synackVulnId: vuln.id,
      title: 'Synack Vulnerability',
      priority: 'Info',
      likelihood_of_exploitation: 1,
      description: '<p>N/A</p>',
      attack_scenario: '<p>N/A</p>',
      remediation_recommendation: '<p>N/A</p>',
      steps_to_reproduce: '<p>N/A</p>',
      affected_assets: [],
      is_visible: true,
      import_source: 'Synack',
      import_source_id: vuln.id,
      import_to_library: secrets.af_synack_writeups_library_key,
      tags: [],
      asset_library_ids: [
        secrets.af_synack_asset_library_key
      ],
      custom_fields: [
        {
          key: "synack_vuln_id",
          value: vuln.id
        },
        {
          key: "synack_vuln_link",
          value: vuln.link
        },
        {
          key: "synack_vuln_status",
          value: vuln.vulnerability_status?.text
        }
      ]
    };

    if (vuln.listing?.codename && data.synackListingIdToAFProjectId[vuln.listing.codename]) {
      newVuln.projectId = data.synackListingIdToAFProjectId[vuln.listing.codename];
    }

    if (vuln.title) {
      newVuln.title = vuln.title;
    }
    if (vuln.description) {
      newVuln.description = vuln.description;
    }
    if (vuln.impact) {
      newVuln.attack_scenario = vuln.impact;
    }
    if (vuln.recommended_fix) {
      newVuln.remediation_recommendation = vuln.recommended_fix;
    }
    if (vuln.exploitable_locations && Array.isArray(vuln.exploitable_locations)) {
      const assets = {};
      const assetComponentMap = {};

      for (let x = 0; x < vuln.exploitable_locations.length; x++) {
        const asset = vuln.exploitable_locations[x];
        let assetName;

        if (asset.type === 'ip' && asset.address) {
          assetName = asset.address;
        }
        else if (asset.type === 'url' && asset.value) {
          assetName = asset.value;
        }
        else if (asset.type === 'app-location' && asset.value) {
          assetName = asset.value;
        }
        else if (asset.type === 'other' && asset.value) {
          assetName = asset.value;
        }
        else if (asset.value) {
          assetName = asset.value;
        }

        if (assetName && !assets[assetName]) {
          assets[assetName] = {
            components: []
          };
        }

        let component;
        let assetComponent;

        if (assetName && asset.protocol && asset.port) {
          component = asset.protocol + ':' + asset.port;
          assetComponent = assetName + ':' + component;
        }
        if (assetName && assets[assetName]?.components 
          && component && assetComponent && !assetComponentMap[assetComponent]
        ) {
          const newComponent = {
            name: component
          };

          Array.push(assets[assetName].components, newComponent);
          assetComponentMap[assetComponent] = assetComponent;
        }      
      }

      const newAssets = Object.keys(assets);

      for (let x = 0; x < Array.length(newAssets); x++) {
        const key = newAssets[x];
        const value = assets[key];

        const newAsset = {
          assetName: key,
          components: assets[key].components
        };

        if (newAsset.assetName) {
          Array.push(newVuln.affected_assets, newAsset);
        }
      }
    }

    if (vuln.validation_steps && Array.isArray(vuln.validation_steps)) {
      newVuln.steps_to_reproduce = '';

      for (let x = 0; x < vuln.validation_steps.length; x++) {
        const step = vuln.validation_steps[x];

        if (step.detail) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce + step.detail + '\n\n';
        }
      }
    }
    if (vuln.http_requests && Array.isArray(vuln.http_requests)) {
      for (let x = 0; x < vuln.http_requests.length; x++) {
        newVuln.steps_to_reproduce = newVuln.steps_to_reproduce + 'HTTP Request\n\n';
        const httpRequest = vuln.http_requests[x];
        
        if (httpRequest.http_request) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce 
            + 'Request: ' + httpRequest.http_request + '\n\n';
        }
        if (httpRequest.vuln_param) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce 
            + 'Vulnerable Parameter: ' + httpRequest.vuln_param + '\n\n';
        }
        if (httpRequest.attack_payload) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce 
            + 'Attack Payload: ' + httpRequest.attack_payload + '\n\n';
        }
      }
    }
    if (vuln.cvss_version === '3.1') {
      if (vuln.cvss_final) {
        const tag = 'CVSSv3.1 Base Score: ' + vuln.cvss_final;
        Array.push(newVuln.tags, tag);
      }
      if (vuln.cvss_vector) {
        const tag = vuln.cvss_vector;
        Array.push(newVuln.tags, tag);
      }
      if (vuln.cvss_final) {
        if (vuln.cvss_final > 0 && vuln.cvss_final <= 3.9) {
          newVuln.priority = 'Low';
          newVuln.likelihood_of_exploitation = 2;
        }
        else if (vuln.cvss_final >= 4 && vuln.cvss_final <= 6.9) {
          newVuln.priority = 'Medium';
          newVuln.likelihood_of_exploitation = 4;
        }
        else if (vuln.cvss_final >= 7 && vuln.cvss_final <= 8.9) {
          newVuln.priority = 'High';
          newVuln.likelihood_of_exploitation = 7;
        }
        else if (vuln.cvss_final >= 9 && vuln.cvss_final <= 10) {
          newVuln.priority = 'Critical';
          newVuln.likelihood_of_exploitation = 9;
        }
      }
    }
    if (vuln.tag_list && Array.isArray(vuln.tag_list)) {
      for (let x = 0; x < vuln.tag_list.length; x++) {
        const tag = vuln.tag_list[x];

        if (tag.name) {
          Array.push(newVuln.tags, tag.name);
        }
      }
    }
    if (vuln.cve_ids && Array.isArray(vuln.cve_ids)) {
      for (let x = 0; x < vuln.cve_ids.length; x++) {
        let cve = "";
        let tag = "" + vuln.cve_ids[x];
        if (!String.startsWith(tag, 'CVE-')) {
          cve = cve + 'CVE-';
        }
        cve = cve + vuln.cve_ids[x];
        Array.push(newVuln.tags, cve);
      }
    }
    if (vuln.cwe_ids && Array.isArray(vuln.cwe_ids)) {
      for (let x = 0; x < vuln.cwe_ids.length; x++) {
        let cwe = "";
        let tag = "" + vuln.cwe_ids[x];
        if (!String.startsWith(tag, 'CWE-')) {
          cwe = cwe + 'CWE-';
        }
        cwe = cwe + vuln.cwe_ids[x];
        Array.push(newVuln.tags, cwe);
      }
    }
    if (vuln.category?.display) {
      const tag = 'Category: ' + vuln.category.display;
      Array.push(newVuln.tags, tag);
    }
    if (vuln.category?.parent) {
      const tag = 'Parent Category: ' + vuln.category.parent;
      Array.push(newVuln.tags, tag);
    }

    Array.push(data.vulnsToCreateUpdated, newVuln);
  }
  
  const vulnMap = {};
  
  for (let x = 0; x < data.vulnsToCreateUpdated.length; x++) {
    const vuln = data.vulnsToCreateUpdated[x];

    if (vuln.synackVulnId) {
      if (vuln.description) {
        vulnMap[vuln.synackVulnId + 'description'] = vuln.description;
      }
      if (vuln.attack_scenario) {
        vulnMap[vuln.synackVulnId + 'attack_scenario'] = vuln.attack_scenario;
      }
      if (vuln.remediation_recommendation) {
        vulnMap[vuln.synackVulnId + 'remediation_recommendation'] = vuln.remediation_recommendation;
      }
      if (vuln.steps_to_reproduce) {
        vulnMap[vuln.synackVulnId + 'steps_to_reproduce'] = vuln.steps_to_reproduce;
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Convert markdown to rich text',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
      body: vulnMap
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to create'
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody) {
  const vulnMap = response.jsonBody;
  
  for (let x = 0; x < data.vulnsToCreateUpdated.length; x++) {
    const vuln = data.vulnsToCreateUpdated[x];

    if (vuln.synackVulnId) {
      if (vulnMap[vuln.synackVulnId + 'description']) {
        vuln.description = vulnMap[vuln.synackVulnId + 'description'];
      }
      if (vulnMap[vuln.synackVulnId + 'attack_scenario']) {
        vuln.attack_scenario = vulnMap[vuln.synackVulnId + 'attack_scenario'];
      }
      if (vulnMap[vuln.synackVulnId + 'remediation_recommendation']) {
        vuln.remediation_recommendation = vulnMap[vuln.synackVulnId + 'remediation_recommendation'];
      }
      if (vulnMap[vuln.synackVulnId + 'steps_to_reproduce']) {
        vuln.steps_to_reproduce = vulnMap[vuln.synackVulnId + 'steps_to_reproduce'];
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Converted markdown fields to rich text',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error converting markdown to rich text'
    }
  };
}
```

**Action 8 - Create AF Vulnerabilities**

* **Method**: POST
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (data.vulnsToCreateUpdated && data.vulnsToCreateUpdated[0]) {
  let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability';
  delete data.vulnsToCreateUpdated[0].synackVulnId;
  
  return {
    decision: {
      status: 'continue',
      message: 'Create vulnerability',
    },
    request: {
      url: url,
      body: data.vulnsToCreateUpdated[0]
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to create'
    },
    data: {
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerability?.vulnerability_id) {
  const vulnId = response.jsonBody.vulnerability.vulnerability_id;

  Array.shift(data.vulnsToCreateUpdated);

  if (response.jsonBody.vulnerability.vulnerability_custom_fields) {
    for (let x = 0; x < response.jsonBody.vulnerability.vulnerability_custom_fields.length; x++) {
      const customField = response.jsonBody.vulnerability.vulnerability_custom_fields[x];

      if (customField.key === 'synack_vuln_id' && customField.value) {
        Array.push(data.vulnsToUpdateToTicketed, {
          afVulnId: vulnId,
          synackVulnId: customField.value
        });
      }
    }
  }

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error creating vulnerability'
    }
  };
}
```

**Action 9 - Update Synack Vulns to Ticketed Status**

* **Method**: PUT
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (secrets.skip_updating_synack_status === 'yes') {
  return {
    decision: {
      status: 'finish',
      message: 'Skipping updating Synack ticketed status',
    }
  };
}

if (!data.vulnsToUpdateToTicketed) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulnsToUpdateToTicketed',
    }
  };
}

if (!data.vulnsToUpdateStatus) {
  data.vulnsToUpdateStatus = [];
}

if (data.vulnsToUpdateToTicketed && data.vulnsToUpdateToTicketed[0]
  && data.vulnsToUpdateToTicketed[0].afVulnId && data.vulnsToUpdateToTicketed[0].synackVulnId
) {
  const afVulnId = data.vulnsToUpdateToTicketed[0].afVulnId;
  const synackVulnId = data.vulnsToUpdateToTicketed[0].synackVulnId;
  const synackVulnStatusName = secrets.synack_vuln_acknowledged_status_name;
  const synackVulnStatusId = secrets.synack_vuln_acknowledged_status_id;

  Array.push(data.vulnsToUpdateStatus, {
    afVulnId: afVulnId,
    synackVulnStatusName: synackVulnStatusName
  });
  
  const url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + synackVulnId;
  const body = {
    status_id: synackVulnStatusId
  };

  return {
    decision: {
      status: 'continue',
      message: 'Updating status for Synack vulnerability',
    },
    request: {
      url: url,
      body: body
    },
    data: {
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No Synack vulnerabilities to process',
    },
    data: {
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.id) {
  Array.shift(data.vulnsToUpdateToTicketed);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating Synack vulnerability status'
    }
  };
}
```

**Action 10 - Update AF Vulns to Ticketed Status**

* **Method**: PUT
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulnsToUpdateStatus) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulnsToUpdateStatus',
    }
  };
}

if (data.vulnsToUpdateStatus && data.vulnsToUpdateStatus[0]
  && data.vulnsToUpdateStatus[0].afVulnId && data.vulnsToUpdateStatus[0].synackVulnStatusName
) {
  const afVulnId = data.vulnsToUpdateStatus[0].afVulnId;
  const synackVulnStatusName = data.vulnsToUpdateStatus[0].synackVulnStatusName;
  
  const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + afVulnId;
  const body = {
    custom_fields: [
      {
        key: 'synack_vuln_status',
        value: synackVulnStatusName
      }
    ]
  };

  return {
    decision: {
      status: 'continue',
      message: 'Updating Synack status for AF vulnerability',
    },
    request: {
      url: url,
      body: body
    },
    data: {
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Successfully completed flow',
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  Array.shift(data.vulnsToUpdateStatus);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating Synack vulnerability status for AF vulnerability'
    }
  };
}
```

## Update Synack when Vuln is Ready for Retest

<figure><img src="/files/QVaxAyX2LKEqzY3vZlTr" alt=""><figcaption></figcaption></figure>

The purpose of this example is to update a vulnerability in [Synack](https://www.synack.com/) when a vulnerability is marked as Ready for Retest in AF.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Type**: Event - vulnerability-updated
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."

**Action 1 - Update Synack Vuln to Retest Status**

* **Method**: POST
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}/patch\_verifications
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (data.vulnerability_retest !== 'Yes') {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability is not ready for retest',
    }
  };
}

let synackVulnId;
let synackIsRetest = false;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'synack_vuln_status' && customField.value === 'Patch Requested') {
      synackIsRetest = true;
    }
    else if (customField.key === 'synack_vuln_id' && customField.value) {
      synackVulnId = customField.value;
    }
  }
}
if (!synackVulnId) {
  return {
    decision: {
      status: 'finish',
      message: 'Synack vulnerability id not found',
    }
  };
}
if (synackIsRetest) {
  return {
    decision: {
      status: 'finish',
      message: 'Synack vulnerability is already set to Patch Requested',
    }
  };
}

const url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + synackVulnId + '/patch_verifications';
const body = {
  message: 'AttackForge - Retest Requested'
};

return {
  decision: {
    status: 'continue',
    message: 'Update Synack vuln status to Patch Requested',
  },
  request: {
    url: url,
    body: body
  },
  data: {
    vuln: data
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.id) {
  return {
    decision: {
      status: 'continue',
      message: 'Updated Synack vuln status',
    },
    data: {
      vuln: data.vuln
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating Synack vulnerability status'
    }
  };
}
```

**Action 2 - Update AF Vuln with Synack Retest Status**

* **Method**: PUT
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vuln.vulnerability_id;
const body = {
  custom_fields: [
    {
      key: 'synack_vuln_status',
      value: 'Patch Requested'
    }
  ]
};

return {
  decision: {
    status: 'continue',
    message: 'Updating Synack status for AF vulnerability',
  },
  request: {
    url: url,
    body: body
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  return {
    decision: {
      status: 'finish',
      message: 'Successfully updated vuln status to Patch Requested'
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating Synack vulnerability status for AF vulnerability'
    }
  };
}
```

## Close Vuln in AF when Synack Vuln is Fixed

<figure><img src="/files/Gaf7eehDP3bvtK5OEmaO" alt=""><figcaption></figcaption></figure>

The purpose of this example is to close a vulnerability in AF when its fixed in [Synack](https://www.synack.com/).

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Type**: Schedule
* **Cron String**: Recommend at start of day, every 24-hours (15 9 \* \* \*)
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."
  * **synack\_fetch\_changes\_from\_in\_days** - the number of days (in the past) to fetch vulnerabilities e.g. set to "1" for fetching updated Synack vulns in past 24-hours. Should match your Flow Cron String.

**Action 1 - Get Synack Vulns**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities?page\[size]=5\&page\[number]={page}\&filter\[updated\_since]={isoDate}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.page) {
  data.page = 1;
}
if (!data.updated_since) {
  const getVulnChangesFrom = '-' + secrets.synack_fetch_changes_from_in_days + ' days';
  data.updated_since = Date.datetime('now', getVulnChangesFrom, 'isostring');
}
if (!data.vulns) {
  data.vulns = [];
}

let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities?';
url = url + '&page[size]=5';
url = url + '&page[number]=' + data.page;
url = url + '&filter[updated_since]=' + data.updated_since;

return {
  decision: {
    status: 'continue',
    message: 'Fetching Synack vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    page: data.page,
    vulns: data.vulns,
    updated_since: data.updated_since
  }
};
```

* **Response Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.page) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.page',
    }
  };
}
if (!data.updated_since) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.updated_since',
    }
  };
}

if (response.jsonBody
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'No vulns left to process',
    },
    data: {
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody 
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length >= 1
  && response.jsonBody.length <= 5
){
  Logger.info('Found ' + response.jsonBody.length + ' vulns');

  for (let x = 0; x < response.jsonBody.length; x++) {
    const vuln = response.jsonBody[x];
    Array.push(data.vulns, vuln);
  }

  data.page = data.page + 1;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vuln pages',
    },
    data: {
      page: data.page,
      vulns: data.vulns,
      updated_since: data.updated_since
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vulns',
    }
  };
}
```

**Action 2 - Get Synack Patch Verified Vulns**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}/patch\_verifications
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (!data.patchVerifiedVulns) {
  data.patchVerifiedVulns = [];
}

if (data.vulns[0] && data.vulns[0].id) {
  let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + data.vulns[0].id + '/patch_verifications';

  return {
    decision: {
      status: 'continue',
      message: 'Fetching Synack vulnerability patch verifications',
    },
    request: {
      url: url
    },
    data: {
      vulns: data.vulns,
      patchVerifiedVulns: data.patchVerifiedVulns,
      vuln: data.vulns[0]
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities left to get patch verifications'
    },
    data: {
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
```

* **Response Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}

if (response.jsonBody && Array.isArray(response.jsonBody) && response.jsonBody.length === 0 ){
  Logger.info('Found 0 patch verifications');

  Array.shift(data.vulns);

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vulns',
    },
    data: {
      vulns: data.vulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else if (response.jsonBody && Array.isArray(response.jsonBody) && response.jsonBody.length > 0 ){
  Logger.info('Found ' + response.jsonBody.length + ' patch verifications');

  Array.shift(data.vulns);

  for (let x = 0; x < response.jsonBody.length; x++) {
    const patchVerification = response.jsonBody[x];

    if (patchVerification.status?.status_text === 'Verified') {
      Array.push(data.patchVerifiedVulns, data.vuln);
    }
  }

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vulns',
    },
    data: {
      vulns: data.vulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vuln patch verification',
    }
  };
}
```

**Action 3 - Get Pending AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?pendingVulnerabilities=true\&skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}
if (data.patchVerifiedVulns && data.patchVerifiedVulns.length === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No vulnerabilities to close',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?pendingVulnerabilities=true';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + '&skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

if (!data.existingVulns) {
  data.existingVulns = [];
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching pending vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    patchVerifiedVulns: data.patchVerifiedVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all pending vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' pending vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more pending vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving pending vulns',
    }
  };
}
```

**Action 4 - Get Visible AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching visible vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    patchVerifiedVulns: data.patchVerifiedVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all visible vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' visible vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more visible vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving visible vulns',
    }
  };
}
```

**Action 5 - Close AF Vulns**

* **Method**: PUT
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}

if (!data.vulnsToUpdate && data.existingVulns.length > 0) {
  data.vulnsToUpdate = [];

  const synackIdToExistingVuln = {};
  
  for (let x = 0; x < data.existingVulns.length; x++) {
    const vuln = data.existingVulns[x];

    if (vuln.vulnerability_custom_fields) {
      for (let y = 0; y < vuln.vulnerability_custom_fields.length; y++) {
        const customField = vuln.vulnerability_custom_fields[y];

        if (customField.key === 'synack_vuln_id' && customField.value){
          synackIdToExistingVuln[customField.value] = vuln;
        }
      }
    }
  }

  for (let x = 0; x < data.patchVerifiedVulns.length; x++) {
    const vuln = data.patchVerifiedVulns[x];

    if (vuln.id && synackIdToExistingVuln[vuln.id]) {
      const existingVuln = synackIdToExistingVuln[vuln.id];

      let afVulnIsClosed = false;
      if (existingVuln.vulnerability_status === 'Closed') {
        afVulnIsClosed = true;
      }

      if (!afVulnIsClosed) {
        Array.push(data.vulnsToUpdate, existingVuln.vulnerability_id);
      }
    }
  }
}

if (data.vulnsToUpdate && data.vulnsToUpdate[0]) {
  const vulnId = data.vulnsToUpdate[0];

  return {
    decision: {
      status: 'continue',
      message: 'Update vulnerability',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vulnId,
      body: {
        status: 'Closed'
      }
    },
    data: {
      vulnsToUpdate: data.vulnsToUpdate,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Successfully completed flow',
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  Array.shift(data.vulnsToUpdate);
  Logger.info('Remaining vulns to close: ' + data.vulnsToUpdate.length);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      vulnsToUpdate: data.vulnsToUpdate,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error closing vulnerability'
    }
  };
}
```

## Get Latest Synack Vuln Comments

<figure><img src="/files/XvoS94Rmo77TeC8tbPHZ" alt=""><figcaption></figcaption></figure>

The purpose of this example is to fetch vulnerability comments in [Synack](https://www.synack.com/) and create remediation notes in AF.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Type**: Schedule
* **Cron String**: Recommend at start of day, every 24-hours (30 9 \* \* \*)
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."

**Action 1 - Get Pending AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?pendingVulnerabilities=true\&skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?pendingVulnerabilities=true';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + '&skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

if (!data.existingVulns) {
  data.existingVulns = [];
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching pending vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all pending vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' pending vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more pending vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving pending vulns',
    }
  };
}
```

**Action 2 - Get Visible AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'finish',
      message: 'Missing data.existingVulns',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching visible vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all visible vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' visible vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more visible vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving visible vulns',
    }
  };
}
```

**Action 3 - Get Synack Vuln Comments**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}/comments
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}

if (!data.commentsToCreate) {
  data.commentsToCreate = [];
}

if (data.existingVulns && data.existingVulns[0]) {
  const vuln = data.existingVulns[0];
  let synackVulnId;
  let projectId;

  if (vuln.vulnerability_custom_fields) {
    for (let x = 0; x < vuln.vulnerability_custom_fields.length; x++) {
      const customField = vuln.vulnerability_custom_fields[x];

      if (customField.key === 'synack_vuln_id' && customField.value) {
        synackVulnId = customField.value;
      }
    }
  }

  if (vuln.vulnerability_projects) {
    for (let x = 0; x < vuln.vulnerability_projects.length; x++) {
      const project = vuln.vulnerability_projects[x];

      if (project.id) {
        projectId = project.id;
      }
    }
  }

  if (!synackVulnId) {
    return {
      decision: {
        status: 'abort',
        message: 'Missing synackVulnId',
      }
    };
  }
  else if (!projectId) {
    return {
      decision: {
        status: 'abort',
        message: 'Missing projectId',
      }
    };
  }

  let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + synackVulnId + '/comments';

  return {
    decision: {
      status: 'continue',
      message: 'Fetching Synack vulnerability comments',
    },
    request: {
      url: url
    },
    data: {
      existingVulns: data.existingVulns,
      commentsToCreate: data.commentsToCreate,
      vuln: vuln
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No existing comments left to process'
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
```

* **Response Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.commentsToCreate) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.commentsToCreate',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

if (response.jsonBody && Array.isArray(response.jsonBody)){
  const remediationNotes = [];

  if (data.vuln.vulnerability_remediation_notes) {
    for (let x = 0; x < data.vuln.vulnerability_remediation_notes.length; x++) {
      const note = data.vuln.vulnerability_remediation_notes[x];

      if (note.note_html) {
        Array.push(remediationNotes, note.note_html);
      }
    }
  }

  let projectId;

  if (data.vuln.vulnerability_projects) {
    for (let x = 0; x < data.vuln.vulnerability_projects.length; x++) {
      const project = data.vuln.vulnerability_projects[x];

      if (project.id) {
        projectId = project.id;
      }
    }
  }

  const comments = response.jsonBody;

  for (let x = 0; x < comments.length; x++) {
    const comment = comments[x];

    let commentExists = false;
    let commentText = '';
    let commentTimestamp = '';
    if (comment.created_at) {
      commentTimestamp = Date.datetime(comment.created_at * 1000);
    }

    if (comment.body && comment.created_at && comment.User?.name) {
      commentText = commentText + 'Synack: ' + comment.User.name 
        + ' on ' + commentTimestamp + '\n\n' + comment.body;
    }

    for (let y = 0; y < remediationNotes.length; y++) {
      const note = remediationNotes[y];

      if (String.includes(note, commentTimestamp) === true) {
        commentExists = true;
      }
    }

    if (!commentExists) {
      Array.push(data.commentsToCreate, {
        projectId: projectId,
        vulnId: data.vuln.vulnerability_id,
        note: commentText,
        commentId: comment.id
      });
    }
  }

  Array.shift(data.existingVulns);

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more Synack vulnerability comments',
    },
    data: {
      existingVulns: data.existingVulns,
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retrieving Synack vulnerability comments',
    }
  };
}
```

**Action 4 - Convert Markdown to Rich Text for Comments**

* **Method**: POST
* **URL**: https\://{{af-tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.commentsToCreate) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.commentsToCreate',
    }
  };
}

if (data.commentsToCreate && data.commentsToCreate.length > 0) {
  const commentMap = {};
  
  for (let x = 0; x < data.commentsToCreate.length; x++) {
    const comment = data.commentsToCreate[x];

    if (comment.commentId && comment.note) {
      commentMap[comment.commentId] = comment.note;
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Convert markdown to rich text',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
      body: commentMap
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'No remediation notes to create'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody) {
  const commentMap = response.jsonBody;
  
  for (let x = 0; x < data.commentsToCreate.length; x++) {
    const comment = data.commentsToCreate[x];

    if (comment.commentId && commentMap[comment.commentId]) {
      comment.note = commentMap[comment.commentId];
    }
  }
  
  return {
    decision: {
      status: 'continue',
      message: 'Converted markdown fields to rich text',
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error converting markdown to rich text'
    }
  };
}
```

**Action 5 - Create Remediation Notes**

* **Method**: POST
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}/remediationNote
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.commentsToCreate) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.commentsToCreate',
    }
  };
}

if (data.commentsToCreate && data.commentsToCreate[0]) {
  const note = data.commentsToCreate[0].note;
  const vulnId = data.commentsToCreate[0].vulnId;
  const projectId = data.commentsToCreate[0].projectId;

  const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vulnId + '/remediationNote';
  const body = {
    note: note,
    note_type: 'RICHTEXT',
    projectId: projectId
  };

  return {
    decision: {
      status: 'continue',
      message: 'Creating remediation note on vulnerability',
    },
    request: {
      url: url,
      body: body
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'No remediation notes left to create'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.note?.id) {
  Array.shift(data.commentsToCreate);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all remediation notes until none left',
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error creating remediation note'
    }
  };
}
```


# WIZ

## Export Vulnerability to WIZ on Vuln Created&#x20;

<figure><img src="/files/F7Qf70gGCtbfuy8rUPeK" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export a vulnerability to [WIZ](https://www.wiz.io) when a vulnerability is created.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and imported into your AttackForge.

**Initial Set Up**

* **Event**: Vuln Created
* **Secrets**:
  * af\_hostname - for example "demo.attackforge.com"
  * af\_key - your AttackForge user API key
  * logging\_level - set to "debug" if additional logging is required
  * wiz\_api\_host - for example "api.us17.app.wiz.io"
  * wiz\_client\_id - your WIZ Client Id
  * wiz\_client\_secret - your WIZ Client Secret
  * wiz\_integration\_id - your WIZ Intgration Id

**Action 1 - Validate Readiness for Export**

* **Script**:

```javascript
const vuln = data;

if (!vuln?.vulnerability_id) {
  return {
    decision: {
      status: 'abort',
      message: 'No vulnerability found in event data.'
    }
  };
};

// Check 1: Vuln is not deleted
const isDeleted = vuln?.vulnerability_is_deleted === true;
if (isDeleted) {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability is deleted. Skipping WIZ export.'
    }
  };
};

// Check 2: Vuln is not pending
const isPending = vuln?.vulnerability_is_visible === false;
if (isPending) {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability is pending. Skipping WIZ export.'
    }
  };
};

// Check 3: Vuln is not already exported to WIZ
const customFields = vuln?.vulnerability_custom_fields || [];
let exportedToWiz = false;
for (let i = 0; i < Array.length(customFields); i++) {
  const customField = customFields[i];
  if (customField.key === 'exported_to_wiz' && customField.value === 'Yes') {
    exportedToWiz = true;
  }
}
if (exportedToWiz) {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability already exported to WIZ. Skipping.'
    }
  };
};

// Check 4  - Ensure all necessary asset fields exist
let asset_hostname;
let asset_port;
let asset_protocol;
let asset_name;
let asset_id;
let asset;
if (vuln?.vulnerability_affected_assets?[0]?.asset) {
  asset = vuln.vulnerability_affected_assets[0].asset;
}
if (!asset) {
  return {
    decision: {
      status: 'abort',
      message: 'Asset not found on vulnerability.'
    }
  };
}
if (asset.name) {
  asset_name = asset.name;
}
if (asset.id) {
  asset_id = asset.id;
}
if (asset.custom_fields && Array.isArray(asset.custom_fields)) {
  const custom_fields = asset.custom_fields;
  for (let i = 0; i < Array.length(custom_fields); i++) {
    const customField = custom_fields[i];
    if (customField.key === 'af_sys_hostnames' && customField.value?[0]) {
      asset_hostname = customField.value[0];
    }
    else if (customField.key === 'af_sys_ports' && customField.value?[0]) {
      asset_port = Number.parseInt(customField.value[0], 10);
    }
    else if (customField.key === 'protocols' && customField.value?[0]) {
      asset_protocol = customField.value[0];
    }
  }
}
if (!asset_id || !asset_name || !asset_hostname || !asset_port || !asset_protocol) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('asset_id: ', asset_id);
    Logger.debug('asset_name: ', asset_name);
    Logger.debug('asset_hostname: ', asset_hostname);
    Logger.debug('asset_port: ', asset_port);
    Logger.debug('asset_protocol: ', asset_protocol);
  }
  return {
    deicision:{
      status: 'abort',
      message: 'Error: asset_id, asset_name, asset_hostname, asset_port, asset_protocol must all exist.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability ready for WIZ export: ', vuln.vulnerability_id);
}

return {
  decision: {
    status: 'continue',
    message: 'Vulnerability is ready for export to WIZ.'
  },
  data: {
    vuln: data,
    asset_id: asset_id,
    asset_name: asset_name,
    asset_hostname: asset_hostname,
    asset_port: asset_port,
    asset_protocol: asset_protocol
  }
};
```

**Action 2 - Build WIZ Payload**

* **Script**:

```javascript
const vuln = data?.vuln;
const vulnId = vuln?.vulnerability_id;
const payload = payloadGenerator(vuln);

if (secrets.logging_level === 'debug'){
  Logger.debug('payload built: ', JSON.stringify(payload));
}
return {
  decision:{
    status: 'continue',
    message: 'Successfully built JSON payload'
  },
  data: {
    vuln: vuln,
    wiz_payload: payload
  }
};

// Users can customise this to be multiple payload generation if needed.
function payloadGenerator(vuln){
  
  // Affected asset
  const endpoint = {
    assetId: data?.asset_id,
    assetName: data?.asset_name,
    host: data?.asset_hostname,
    port: data?.asset_port,
    protocol: data?.asset_protocol
  };

  // Severity mapping: AF → WIZ
  const severityMap = {
    'Critical': 'Critical',
    'High': 'High',
    'Medium': 'Medium',
    'Low': 'Low',
    'Info': 'None'
  };
  const afPriority = vuln?.vulnerability_priority || 'Low';
  const wizSeverity = severityMap[afPriority] || 'None';

  // description
  const description = (vuln?.vulnerability_description || '') + '\n\n' + (vuln?.vulnerability_attack_scenario || '');

  // assessmentDetails
  const affectedAssetsStr = '- ' + data?.asset_name + '\n';
  const notes = vuln?.vulnerability_notes || [];
  let notesStr = '';
  for (let i = 0; i < Array.length(notes); i++) {
    const noteText = notes[i]?.note || '';
    if (noteText) {
      notesStr = notesStr + noteText + '\n';
    }
  }
  const assessmentDetails = 'Affected Asset:\n\n' + (affectedAssetsStr || 'N/A')
    + '\nSteps to Reproduce:\n\n' + (vuln?.vulnerability_steps_to_reproduce || 'N/A')
    + '\n\nNotes:\n\n' + (notesStr || 'N/A');

  // Link to AttackForge vuln
  const projects = vuln?.vulnerability_projects || [];
  const projectId = (Array.length(projects) > 0) ? projects[0].id : '';
  const externalLink = 'https://' + secrets.af_hostname + '/projects/' + projectId + '/vulnerabilities/' + vulnId;

  // Stable datasource ID
  const datasourceId = secrets.wiz_datasource_id || ('attackforge-project-' + projectId);

  // Vulnerability type mapping
  const typeMap = {
    'Misconfiguration': 'Misconfiguration',
    'DAST': 'DAST',
    'SCA': 'SCA',
    'SAST': 'SAST',
    'IaC': 'IaC',
    'SecretDetection': 'SecretDetection',
    'ContainerScan': 'ContainerScan',
    'HostScan': 'HostScan'
  };
  const vulnType = 'DAST';
  const wizType = typeMap[vulnType] || 'DAST';

  const finding = {
    id: vulnId,
    name: vuln?.vulnerability_title || '',
    description: description,
    assessmentDetails: assessmentDetails,
    remediation: vuln?.vulnerability_remediation_recommendation || '',
    severity: wizSeverity,
    type: wizType,
    externalFindingLink: externalLink
  };  

  return {
    integrationId: secrets.wiz_integration_id,
    dataSources: [
      {
        id: datasourceId,
        analysisDate: vuln?.vulnerability_created || Date.datetime('now'),
        assets: [
          {
            details: {
              endpoint: endpoint
            },
            attackSurfaceFindings: [finding]
          }
        ]
      }
    ]
  };
}
```

**Action 3 - Get WIZ Access Token**

* **Method**: POST
* **URL**: <https://auth.app.wiz.io/oauth/token>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
const req_body = 'grant_type=client_credentials&audience=wiz-api&client_id=' + secrets.wiz_client_id + '&client_secret=' + secrets.wiz_client_secret;

return {
  decision: {
    status: 'continue',
    message: 'Fetching WIZ access token.'
  },
  request: {
    body: 'grant_type=client_credentials&audience=wiz-api&client_id=' + secrets.wiz_client_id + '&client_secret=' + secrets.wiz_client_secret
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching WIZ token: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to get WIZ access token. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

const token = response?.jsonBody?.access_token;
if (!token) {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ access_token not found in token response.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ access token obtained successfully.');
}

return {
  decision: {
    status: 'continue',
    message: 'WIZ access token obtained.'
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload,
    wiz_token: token
  }
};
```

**Action 4 - Request WIZ Upload URL**

* **Method**: POST
* **URL**: https\://{{wiz\_api\_host}}/graphql
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
const vuln = data?.vuln;
const currTime = Date.datetime("now", "epoch");
const wiz_upload_filename = 'attackforge-vuln-' + currTime + '.json';

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ enrichment payload: ', JSON.stringify(payload));
}

return {
  decision: {
    status: 'continue',
    message: 'Requesting WIZ upload URL for vulnerability ' + vuln.vulnerability_id + '.'
  },
  request: {
    url: 'https://' + secrets.wiz_api_host + '/graphql',
    headers: {
      'Authorization': 'Bearer ' + data.wiz_token,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: {
      query: 'query RequestSecurityScanUpload($filename: String!) { requestSecurityScanUpload(filename: $filename) { upload { id url systemActivityId } } }',
      variables: {
        filename: wiz_upload_filename
      }
    }
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload,
    wiz_token: data?.wiz_token
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('WIZ upload URL request error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to request WIZ upload URL. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

const upload = response?.jsonBody?.data?.requestSecurityScanUpload?.upload;
if (!upload?.url) {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ upload URL not found in response.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ upload URL obtained. Upload ID: ' + upload.id + ', Activity ID: ' + upload.systemActivityId);
}

return {
  decision: {
    status: 'continue',
    message: 'WIZ upload URL obtained successfully.'
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload,
    wiz_token: data?.wiz_token,
    wiz_upload_id: upload.id,
    wiz_upload_url: upload.url,
    wiz_system_activity_id: upload.systemActivityId
  }
};
```

**Action 5 - Upload Scan to WIZ S3 Bucket**

* **Method**: PUT
* **URL**: https\://{{preSignedUrl}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
const uploadUrl = data?.wiz_upload_url;
if (!uploadUrl) {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ upload URL not found in data. Cannot upload enrichment payload.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Uploading enrichment JSON to S3. Upload ID: ' + data.wiz_upload_id);
}

return {
  decision: {
    status: 'continue',
    message: 'Uploading enrichment JSON to WIZ S3 bucket.'
  },
  request: {
    url: uploadUrl,
    body: data?.wiz_payload
  },
  data: {
    vuln: data?.vuln,
    wiz_token: data?.wiz_token,
    wiz_upload_id: data?.wiz_upload_id,
    wiz_system_activity_id: data?.wiz_system_activity_id
  }
};
```

* **Response Script**:

```javascript
// S3 presigned PUT returns 200 on success
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('S3 upload error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to upload enrichment JSON to S3. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Enrichment JSON uploaded to S3 successfully. Upload ID: ' + data.wiz_upload_id);
  Logger.debug('response: ', JSON.stringify(response));
}

return {
  decision: {
    status: 'continue',
    message: 'Enrichment JSON uploaded to WIZ S3 bucket successfully.',
    delay: 3000
  },
  data: {
    vuln: data?.vuln,
    wiz_token: data?.wiz_token,
    wiz_system_activity_id: data?.wiz_system_activity_id
  }
};
```

**Action 6 - Check WIZ Upload Status**

* **Method**: POST
* **URL**: https\://{{wiz\_api\_host}}/graphql
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
const activityId = data?.wiz_system_activity_id;
if (!activityId) {
  return {
    decision: {
      status: 'abort',
      message: 'systemActivityId not found. Cannot check WIZ upload status.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Checking WIZ upload status for systemActivityId: ' + activityId);
}

const query = 'query SystemActivity($id: ID!) { systemActivity(id: $id) { id status statusInfo result { ... on SystemActivityEnrichmentIntegrationResult { dataSources { ... IngestionStatsDetails } findings { ... IngestionStatsDetails } events { ... IngestionStatsDetails } tags { ... IngestionStatsDetails } } } context { ... on SystemActivityEnrichmentIntegrationContext { fileUploadId } } } } fragment IngestionStatsDetails on EnrichmentIntegrationStats { incoming handled }';

return {
  decision: {
    status: 'continue',
    message: 'Checking WIZ upload status for activity ' + activityId + '.'
  },
  request: {
    url: 'https://' + secrets.wiz_api_host + '/graphql',
    headers: {
      'Authorization': 'Bearer ' + data.wiz_token,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: {
      query: query,
      variables: {
        id: activityId
      }
    }
  },
  data: {
    vuln: data?.vuln,
    wiz_token: data?.wiz_token,
    wiz_system_activity_id: data?.wiz_system_activity_id
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('WIZ status check error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to check WIZ upload status. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

const activity = response?.jsonBody?.data?.systemActivity;
if (!activity) {
  return {
    decision: {
      status: 'abort',
      message: 'systemActivity not found in WIZ status response.'
    }
  };
}

const activityStatus = activity?.status || '';
const statusInfo = activity?.statusInfo || '';

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ activity status: ' + activityStatus + ' — ' + statusInfo);
  Logger.debug('WIZ activity result: ', JSON.stringify(activity?.result));
}

if (activityStatus === 'FAILURE') {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ enrichment ingestion failed. Status: ' + activityStatus + ' — ' + statusInfo
    }
  };
}

// Success
if (activityStatus === 'SKIPPED' || activityStatus === 'SUCCESS'){
  // 
  return {
    decision: {
      status: 'continue',
      message: 'WIZ enrichment upload status: ' + activityStatus + '. Proceeding to mark vulnerability.'
    },
    data: {
      vuln: data?.vuln,
    }
  };
} 
else {
  return {
    decision:{
      status: 'repeat',
      message: 'Current Status: ' + activityStatus + '. Polling Check Upload Status again in 5 seconds...',
      delay: 5000
    },
    data: {
      vuln: data?.vuln,
      wiz_token: data?.wiz_token,
      wiz_system_activity_id: data?.wiz_system_activity_id
    }
  };
}
```

**Action 7 - Update Vuln - Mark Vuln as Exported to WIZ**

* **Method**: PUT
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
const vuln = data?.vuln;
const vulnId = vuln?.vulnerability_id;
const projects = vuln?.vulnerability_projects;
// Assume first project as project
const projectId = projects?[0]?.id ;

if (!vulnId || !projectId) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln: ', vuln);
    Logger.debug('project: ', vuln?.vulnerability_projects);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Vulnerability ID or Project ID not found. Cannot mark as exported to WIZ. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability as exported to WIZ.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + vulnId,
    body: {
      project_id: projectId,
      custom_fields: [
        { 
          key: 'exported_to_wiz',
          value: 'Yes' 
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error marking vulnerability as exported: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to update vulnerability as exported to WIZ. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability marked as exported to WIZ successfully.');
}

return {
  decision: {
    status: 'finish',
    message: 'Vulnerability exported to WIZ and marked as "exported_to_wiz" = "Yes".'
  }
};
```

## Export Vulnerability to WIZ on Vuln Updated

<figure><img src="/files/Wr4v6dGTKUon1OAugXvS" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export a vulnerability to [WIZ](https://www.wiz.io) when a vulnerability is updated, if it has not yet been exported to WIZ.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and imported into your AttackForge.

**Initial Set Up**

* **Event**: Vuln Updated
* **Secrets**:
  * af\_hostname - for example "demo.attackforge.com"
  * af\_key - your AttackForge user API key
  * logging\_level - set to "debug" if additional logging is required
  * wiz\_api\_host - for example "api.us17.app.wiz.io"
  * wiz\_client\_id - your WIZ Client Id
  * wiz\_client\_secret - your WIZ Client Secret
  * wiz\_integration\_id - your WIZ Intgration Id

**Action 1 - Validate Readiness for Export**

* **Script**:

```javascript
const vuln = data;

if (!vuln?.vulnerability_id) {
  return {
    decision: {
      status: 'abort',
      message: 'No vulnerability found in event data.'
    }
  };
};

// Check 1: Vuln is not deleted
const isDeleted = vuln?.vulnerability_is_deleted === true;
if (isDeleted) {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability is deleted. Skipping WIZ export.'
    }
  };
};

// Check 2: Vuln is not pending
const isPending = vuln?.vulnerability_is_visible === false;
if (isPending) {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability is pending. Skipping WIZ export.'
    }
  };
};

// Check 3: Vuln is not already exported to WIZ
const customFields = vuln?.vulnerability_custom_fields || [];
let exportedToWiz = false;
for (let i = 0; i < Array.length(customFields); i++) {
  const customField = customFields[i];
  if (customField.key === 'exported_to_wiz' && customField.value === 'Yes') {
    exportedToWiz = true;
  }
}
if (exportedToWiz) {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability already exported to WIZ. Skipping.'
    }
  };
};

// Check 4  - Ensure all necessary asset fields exist
let asset_hostname;
let asset_port;
let asset_protocol;
let asset_name;
let asset_id;
let asset;
if (vuln?.vulnerability_affected_assets?[0]?.asset) {
  asset = vuln.vulnerability_affected_assets[0].asset;
}
if (!asset) {
  return {
    decision: {
      status: 'abort',
      message: 'Asset not found on vulnerability.'
    }
  };
}
if (asset.name) {
  asset_name = asset.name;
}
if (asset.id) {
  asset_id = asset.id;
}
if (asset.custom_fields && Array.isArray(asset.custom_fields)) {
  const custom_fields = asset.custom_fields;
  for (let i = 0; i < Array.length(custom_fields); i++) {
    const customField = custom_fields[i];
    if (customField.key === 'af_sys_hostnames' && customField.value?[0]) {
      asset_hostname = customField.value[0];
    }
    else if (customField.key === 'af_sys_ports' && customField.value?[0]) {
      asset_port = Number.parseInt(customField.value[0], 10);
    }
    else if (customField.key === 'protocols' && customField.value?[0]) {
      asset_protocol = customField.value[0];
    }
  }
}
if (!asset_id || !asset_name || !asset_hostname || !asset_port || !asset_protocol) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('asset_id: ', asset_id);
    Logger.debug('asset_name: ', asset_name);
    Logger.debug('asset_hostname: ', asset_hostname);
    Logger.debug('asset_port: ', asset_port);
    Logger.debug('asset_protocol: ', asset_protocol);
  }
  return {
    deicision:{
      status: 'abort',
      message: 'Error: asset_id, asset_name, asset_hostname, asset_port, asset_protocol must all exist.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability ready for WIZ export: ', vuln.vulnerability_id);
}

return {
  decision: {
    status: 'continue',
    message: 'Vulnerability is ready for export to WIZ.'
  },
  data: {
    vuln: data,
    asset_id: asset_id,
    asset_name: asset_name,
    asset_hostname: asset_hostname,
    asset_port: asset_port,
    asset_protocol: asset_protocol
  }
};
```

**Action 2 - Build WIZ Payload**

* **Script**:

```javascript
const vuln = data?.vuln;
const vulnId = vuln?.vulnerability_id;
const payload = payloadGenerator(vuln);

if (secrets.logging_level === 'debug'){
  Logger.debug('payload built: ', JSON.stringify(payload));
}
return {
  decision:{
    status: 'continue',
    message: 'Successfully built JSON payload'
  },
  data: {
    vuln: vuln,
    wiz_payload: payload
  }
};

// Users can customise this to be multiple payload generation if needed.
function payloadGenerator(vuln){
  
  // Affected asset
  const endpoint = {
    assetId: data?.asset_id,
    assetName: data?.asset_name,
    host: data?.asset_hostname,
    port: data?.asset_port,
    protocol: data?.asset_protocol
  };

  // Severity mapping: AF → WIZ
  const severityMap = {
    'Critical': 'Critical',
    'High': 'High',
    'Medium': 'Medium',
    'Low': 'Low',
    'Info': 'None'
  };
  const afPriority = vuln?.vulnerability_priority || 'Low';
  const wizSeverity = severityMap[afPriority] || 'None';

  // description
  const description = (vuln?.vulnerability_description || '') + '\n\n' + (vuln?.vulnerability_attack_scenario || '');

  // assessmentDetails
  const affectedAssetsStr = '- ' + data?.asset_name + '\n';
  const notes = vuln?.vulnerability_notes || [];
  let notesStr = '';
  for (let i = 0; i < Array.length(notes); i++) {
    const noteText = notes[i]?.note || '';
    if (noteText) {
      notesStr = notesStr + noteText + '\n';
    }
  }
  const assessmentDetails = 'Affected Asset:\n\n' + (affectedAssetsStr || 'N/A')
    + '\nSteps to Reproduce:\n\n' + (vuln?.vulnerability_steps_to_reproduce || 'N/A')
    + '\n\nNotes:\n\n' + (notesStr || 'N/A');

  // Link to AttackForge vuln
  const projects = vuln?.vulnerability_projects || [];
  const projectId = (Array.length(projects) > 0) ? projects[0].id : '';
  const externalLink = 'https://' + secrets.af_hostname + '/projects/' + projectId + '/vulnerabilities/' + vulnId;

  // Stable datasource ID
  const datasourceId = secrets.wiz_datasource_id || ('attackforge-project-' + projectId);

  // Vulnerability type mapping
  const typeMap = {
    'Misconfiguration': 'Misconfiguration',
    'DAST': 'DAST',
    'SCA': 'SCA',
    'SAST': 'SAST',
    'IaC': 'IaC',
    'SecretDetection': 'SecretDetection',
    'ContainerScan': 'ContainerScan',
    'HostScan': 'HostScan'
  };
  const vulnType = 'DAST';
  const wizType = typeMap[vulnType] || 'DAST';

  const finding = {
    id: vulnId,
    name: vuln?.vulnerability_title || '',
    description: description,
    assessmentDetails: assessmentDetails,
    remediation: vuln?.vulnerability_remediation_recommendation || '',
    severity: wizSeverity,
    type: wizType,
    externalFindingLink: externalLink
  };  

  return {
    integrationId: secrets.wiz_integration_id,
    dataSources: [
      {
        id: datasourceId,
        analysisDate: vuln?.vulnerability_created || Date.datetime('now'),
        assets: [
          {
            details: {
              endpoint: endpoint
            },
            attackSurfaceFindings: [finding]
          }
        ]
      }
    ]
  };
}
```

**Action 3 - Get WIZ Access Token**

* **Method**: POST
* **URL**: <https://auth.app.wiz.io/oauth/token>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
const req_body = 'grant_type=client_credentials&audience=wiz-api&client_id=' + secrets.wiz_client_id + '&client_secret=' + secrets.wiz_client_secret;

return {
  decision: {
    status: 'continue',
    message: 'Fetching WIZ access token.'
  },
  request: {
    body: 'grant_type=client_credentials&audience=wiz-api&client_id=' + secrets.wiz_client_id + '&client_secret=' + secrets.wiz_client_secret
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching WIZ token: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to get WIZ access token. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

const token = response?.jsonBody?.access_token;
if (!token) {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ access_token not found in token response.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ access token obtained successfully.');
}

return {
  decision: {
    status: 'continue',
    message: 'WIZ access token obtained.'
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload,
    wiz_token: token
  }
};
```

**Action 4 - Request WIZ Upload URL**

* **Method**: POST
* **URL**: https\://{{wiz\_api\_host}}/graphql
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
const vuln = data?.vuln;
const currTime = Date.datetime("now", "epoch");
const wiz_upload_filename = 'attackforge-vuln-' + currTime + '.json';

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ enrichment payload: ', JSON.stringify(payload));
}

return {
  decision: {
    status: 'continue',
    message: 'Requesting WIZ upload URL for vulnerability ' + vuln.vulnerability_id + '.'
  },
  request: {
    url: 'https://' + secrets.wiz_api_host + '/graphql',
    headers: {
      'Authorization': 'Bearer ' + data.wiz_token,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: {
      query: 'query RequestSecurityScanUpload($filename: String!) { requestSecurityScanUpload(filename: $filename) { upload { id url systemActivityId } } }',
      variables: {
        filename: wiz_upload_filename
      }
    }
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload,
    wiz_token: data?.wiz_token
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('WIZ upload URL request error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to request WIZ upload URL. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

const upload = response?.jsonBody?.data?.requestSecurityScanUpload?.upload;
if (!upload?.url) {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ upload URL not found in response.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ upload URL obtained. Upload ID: ' + upload.id + ', Activity ID: ' + upload.systemActivityId);
}

return {
  decision: {
    status: 'continue',
    message: 'WIZ upload URL obtained successfully.'
  },
  data: {
    vuln: data?.vuln,
    wiz_payload: data?.wiz_payload,
    wiz_token: data?.wiz_token,
    wiz_upload_id: upload.id,
    wiz_upload_url: upload.url,
    wiz_system_activity_id: upload.systemActivityId
  }
};
```

**Action 5 - Upload Scan to WIZ S3 Bucket**

* **Method**: PUT
* **URL**: https\://{{preSignedUrl}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
const uploadUrl = data?.wiz_upload_url;
if (!uploadUrl) {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ upload URL not found in data. Cannot upload enrichment payload.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Uploading enrichment JSON to S3. Upload ID: ' + data.wiz_upload_id);
}

return {
  decision: {
    status: 'continue',
    message: 'Uploading enrichment JSON to WIZ S3 bucket.'
  },
  request: {
    url: uploadUrl,
    body: data?.wiz_payload
  },
  data: {
    vuln: data?.vuln,
    wiz_token: data?.wiz_token,
    wiz_upload_id: data?.wiz_upload_id,
    wiz_system_activity_id: data?.wiz_system_activity_id
  }
};
```

* **Response Script**:

```javascript
// S3 presigned PUT returns 200 on success
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('S3 upload error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to upload enrichment JSON to S3. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Enrichment JSON uploaded to S3 successfully. Upload ID: ' + data.wiz_upload_id);
  Logger.debug('response: ', JSON.stringify(response));
}

return {
  decision: {
    status: 'continue',
    message: 'Enrichment JSON uploaded to WIZ S3 bucket successfully.',
    delay: 3000
  },
  data: {
    vuln: data?.vuln,
    wiz_token: data?.wiz_token,
    wiz_system_activity_id: data?.wiz_system_activity_id
  }
};
```

**Action 6 - Check WIZ Upload Status**

* **Method**: POST
* **URL**: https\://{{wiz\_api\_host}}/graphql
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
const activityId = data?.wiz_system_activity_id;
if (!activityId) {
  return {
    decision: {
      status: 'abort',
      message: 'systemActivityId not found. Cannot check WIZ upload status.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Checking WIZ upload status for systemActivityId: ' + activityId);
}

const query = 'query SystemActivity($id: ID!) { systemActivity(id: $id) { id status statusInfo result { ... on SystemActivityEnrichmentIntegrationResult { dataSources { ... IngestionStatsDetails } findings { ... IngestionStatsDetails } events { ... IngestionStatsDetails } tags { ... IngestionStatsDetails } } } context { ... on SystemActivityEnrichmentIntegrationContext { fileUploadId } } } } fragment IngestionStatsDetails on EnrichmentIntegrationStats { incoming handled }';

return {
  decision: {
    status: 'continue',
    message: 'Checking WIZ upload status for activity ' + activityId + '.'
  },
  request: {
    url: 'https://' + secrets.wiz_api_host + '/graphql',
    headers: {
      'Authorization': 'Bearer ' + data.wiz_token,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: {
      query: query,
      variables: {
        id: activityId
      }
    }
  },
  data: {
    vuln: data?.vuln,
    wiz_token: data?.wiz_token,
    wiz_system_activity_id: data?.wiz_system_activity_id
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('WIZ status check error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to check WIZ upload status. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

const activity = response?.jsonBody?.data?.systemActivity;
if (!activity) {
  return {
    decision: {
      status: 'abort',
      message: 'systemActivity not found in WIZ status response.'
    }
  };
}

const activityStatus = activity?.status || '';
const statusInfo = activity?.statusInfo || '';

if (secrets.logging_level === 'debug') {
  Logger.debug('WIZ activity status: ' + activityStatus + ' — ' + statusInfo);
  Logger.debug('WIZ activity result: ', JSON.stringify(activity?.result));
}

if (activityStatus === 'FAILURE') {
  return {
    decision: {
      status: 'abort',
      message: 'WIZ enrichment ingestion failed. Status: ' + activityStatus + ' — ' + statusInfo
    }
  };
}

// Success
if (activityStatus === 'SKIPPED' || activityStatus === 'SUCCESS'){
  // 
  return {
    decision: {
      status: 'continue',
      message: 'WIZ enrichment upload status: ' + activityStatus + '. Proceeding to mark vulnerability.'
    },
    data: {
      vuln: data?.vuln,
    }
  };
} 
else {
  return {
    decision:{
      status: 'repeat',
      message: 'Current Status: ' + activityStatus + '. Polling Check Upload Status again in 5 seconds...',
      delay: 5000
    },
    data: {
      vuln: data?.vuln,
      wiz_token: data?.wiz_token,
      wiz_system_activity_id: data?.wiz_system_activity_id
    }
  };
}
```

**Action 7 - Update Vuln - Mark Vuln as Exported to WIZ**

* **Method**: PUT
* **URL**: https\://{{af-tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
const vuln = data?.vuln;
const vulnId = vuln?.vulnerability_id;
const projects = vuln?.vulnerability_projects;
// Assume first project as project
const projectId = projects?[0]?.id ;

if (!vulnId || !projectId) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln: ', vuln);
    Logger.debug('project: ', vuln?.vulnerability_projects);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Vulnerability ID or Project ID not found. Cannot mark as exported to WIZ. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability as exported to WIZ.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + vulnId,
    body: {
      project_id: projectId,
      custom_fields: [
        { 
          key: 'exported_to_wiz',
          value: 'Yes' 
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error marking vulnerability as exported: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to update vulnerability as exported to WIZ. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability marked as exported to WIZ successfully.');
}

return {
  decision: {
    status: 'finish',
    message: 'Vulnerability exported to WIZ and marked as "exported_to_wiz" = "Yes".'
  }
};
```


# Webhook

## Create a Webhook

{% embed url="<https://youtu.be/HHEZNcKFOKA?si=MAszie760hB7-l4L>" %}

The purpose of this example is to post data to a [Webhook](https://www.redhat.com/en/topics/automation/what-is-a-webhook) when a Vulnerability is created in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Event**: Vulnerability Created
* **Secrets**: None

**Action 1 - Send Data to Webhook**

* **Method**: POST
* **URL**: \<YOUR-WEBHOOK-ADDRESS>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = auth
* **Request Script**:

<pre class="language-javascript"><code class="lang-javascript"><strong>// Not required for this webhook
</strong></code></pre>

* **Response Script**:

```javascript
// Not required for this webhook
```


# Custom Emails

## Send Custom Email

{% embed url="<https://youtu.be/TKUJrk5WVvo?si=4vKztfvFX2ODN5Sf>" %}

The purpose of this example is to send an email to account managers when a Retest is requested.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Project Retest Requested
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).

**Action 1 - Send Email to Account Managers**

* **Method**: POST
* **URL**: https\://\<YOUR-ATTACKFORGE>/api/ss/email
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-Key; Type = Value; Value = af\_auth
* **Request Script**:

```javascript
return {
  request: { 
    body: {
      to: [
        "admin@attackforge.com",
        {
          "user_id": "67b45e6de6da84559d9860fc"
        },
      ],
      cc: [
        "support@attackforge.com"
      ],
      subject: "New Project Requested - Please Follow Up",
      text: data.project_request_name + ' just requested to start on ' + data.project_request_start_date,
      html: '<p>[<b>' + data.project_request_name + '</b>] just requested to start on [<b>' + data.project_request_start_date + '</b>]</p>'
    },
  },
};
```

* **Response Script**:

```javascript
const body = JSON.parse(response.body);

if (body?.status === 'Accepted') {
  return {
    decision: {  
      status: 'finish',
      message: 'Email sent!'
    },  
  };
}
else {
  return {
    decision: {  
      status: 'abort',
      message: 'Email not sent'
    }
  };
}
```


# AI MCP and Skills

## Overview

**Model Context Protocol (MCP)** is an open-source standard developed by [Anthropic](https://www.anthropic.com/) that enables AI assistants to securely connect to AttackForge with external data sources and tools.

MCP transforms AI from a conversational knowledge base into a practical assistant that can work with your actual AttackForge data and tools to get real work done, fast!

**Skills** are reusable packages of instructions, scripts, and resources that teach an AI model how to perform a specific task well. Think of them like specialized playbooks the AI loads on demand.

AttackForge build AI Skills to help your AI models perform tasks in a standardized way, with better and more consistent output and easier maintenance and iteration.

<figure><img src="/files/zGDHomxz65iT5Eg8YZL0" alt=""><figcaption></figcaption></figure>

MCP has significant benefits for AttackForge users:

#### **1. Better Confidentiality and Control**

**Data Stays Where It Belongs**

With MCP, your sensitive vulnerability data doesn't need to be sent to AI providers for training. Your AI accesses your data when needed and only for your specific requests. You can even use AI models which are local to your computer, so that data never leaves your computer.

**Granular Permissions**

You control exactly what data and capabilities the AI can access on behalf of any AttackForge user you authorize to use MCP, ensuring appropriate boundaries and compliance with your security requirements.

#### **2. More Helpful and Accurate Responses**

**Access to Current AttackForge Information**

Instead of being limited to training data, AI assistants using MCP can pull real-time information from AttackForge - for example access to your latest vulnerabilities and projects. This provides context and answers based on your latest data, not outdated information in pre-trained vendor AI models.

**Personalized Assistance**

MCP enables AI to access your specific context - your AttackForge vulnerabilities, writeups, assets, projects - making responses tailored to your actual situation rather than generic advice.

#### **3. Greater Productivity**

**Unified Interface**

Instead of switching between different APIs and creating complex scripts, you can interact with your AttackForge through a single conversational interface. Ask questions about your data, retrieve records, check statuses, all in one place.

**Automated Workflows**

The AI can perform multi-step tasks, like pulling data from AttackForge, analyzing it, and updating a spreadsheet or creating a presentation - all from a simple request.

#### **4. Future-Proof Investment**

**Vendor Independence**

If you build workflows using MCP, you're not locked into a specific AI provider (vendor supplied AI models). You can switch AI assistants while keeping all your integrations working.

**Growing Ecosystem**

As AttackForge continues to build more MCP tools and skills, you'll automatically gain access to new capabilities without needing custom development work and extensive professional services.

## AI Agentic Workflows

<figure><img src="/files/TjhXQUXDokIRdhcgbD2S" alt=""><figcaption></figcaption></figure>

Check [this link](https://support.attackforge.com/attackforge-enterprise/ai-agentic-workflows) for examples on how to configure your agents with AttackForge.

## Enabling MCP

To get started with MCP:

* AttackForge Enterprise - MCP is available with your licence.&#x20;
* AttackForge Core - MCP can be add-on from `Administration > Subscriptions`&#x20;

To enable MCP - go to `Administration > Integrations` and enable the toggle for MCP.

> **IMPORTANT:** When MCP is enabled, access to MCP Tools is not yet available. Each tool must be enabled by an AttackForge administrator on a per-user basis for maximum security.

<figure><img src="/files/0UazYdHyS7JhxGIuv4mU" alt=""><figcaption></figcaption></figure>

## Configuring Remote MCP

<figure><img src="/files/bk4VCGJU0YBpuqYoYUWw" alt=""><figcaption></figcaption></figure>

Remote MCP are remote Model Context Protocol servers that are hosted on the internet rather than on your local machine. Remote MCP servers extend AI applications' capabilities beyond your local environment, providing access to internet-hosted tools, services, and data sources. Unlike local MCP servers that run on your computer, remote servers are accessible from any MCP client with an internet connection.

The key advantage of remote MCP servers is their accessibility - unlike local servers that require installation and configuration on each device. This makes them particularly useful for web-based AI applications (like AttackForge) and services that require server-side processing or authentication.

Remote MCP servers expose tools, prompts, and resources that AI assistants can use. These servers can integrate with various services such as AttackForge.&#x20;

AttackForge has a built-in [OAuth v2.1](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) service which is used to authenticate users connecting to AttackForge MCP. Every AttackForge user connecting to AttackForge via remote MCP must explicitly grant the AI assistant permission to do so on behalf of the user.

## Self Registration

Self registration allows AttackForge users to register their AI assistant to use AttackForge MCP directly. This saves time and effort for AttackForge administrators having to manually create OAuth2 Client IDs and Client Secrets and having to share that information with enrolling users.

Self registration is handled using the built-in AttackForge [OAuth2 Authorization Server](https://datatracker.ietf.org/doc/html/rfc6749) which comes with every AttackForge tenant, and is made available when MCP is enabled in AttackForge.

To enable self registration - go to `Administration > Integrations > MCP` and toggle to option to enable self registration.

> **IMPORTANT:** When Self Registration is enabled, access to MCP Tools is not yet available. Each tool can be enabled on a per-user basis by the AttackForge administrators for maximum security.

<figure><img src="/files/Zn2vbmoc3fki90EBhVcR" alt=""><figcaption></figcaption></figure>

### Microsoft Copilot Studio Self Registration

1. Log in to [Microsoft Copilot Studio](https://copilotstudio.microsoft.com/). Select `Agents > Create blank agent`

<figure><img src="/files/4ojuERWBnYFp0eZ04XHf" alt=""><figcaption></figcaption></figure>

2. Click on `Tools > Add a tool`

<figure><img src="/files/s1xex4zdPk9ZuJkZIvRD" alt=""><figcaption></figcaption></figure>

3. Click on `Model Context Protocol > New tool`

<figure><img src="/files/tEr5USCndhQG5jOvuY5k" alt=""><figcaption></figcaption></figure>

4. Click on `Model Context Protocol`

<figure><img src="/files/RLXLaLrOptZrM95bbykV" alt=""><figcaption></figcaption></figure>

5. Enter in a name for your connecter. For the `Server URL` this should be in the following format: `https://{{ATTACKFORGE-HOSTNAME}}/mcp` . Select `OAuth 2.0` for Authentication. Select `Dynamic discovery` for Type. Click `Add`.

<figure><img src="/files/03uHSp0995jpMO7RmNSP" alt=""><figcaption></figcaption></figure>

6. Click on `Create new connection`

<figure><img src="/files/YLWoV2TIxkqmjlxLvLvT" alt=""><figcaption></figcaption></figure>

7. Click `Create`. You will be redirected to AttackForge. If you are not logged in - you will first need to log in. After you have logged in - you will see the screen below. Click on `Agree and Continue`. You will then be redirected back to Copilot Studio.

<figure><img src="/files/TeBeMUNYNACM0ButjKR4" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/uHTU885SErjBxgQYtKaJ" alt=""><figcaption></figcaption></figure>

8. You should see a successful connection. Click on `Add and configure`.

<figure><img src="/files/kQwVgNCA62kekvSgdVf8" alt=""><figcaption></figcaption></figure>

9. Your agent should now be connected to AttackForge.

<figure><img src="/files/gG3ifo1FN5qriYoIgyKP" alt=""><figcaption></figcaption></figure>

10. Click on `Tools`. Configure the [Tools](#tools) you want to use.

<figure><img src="/files/fUnkdRGtqyF6KR7YWQZf" alt=""><figcaption></figcaption></figure>

11. Try a prompt. If you see an error `Let's get you connected first, and then I can find that info for you` - click on the link to **Open connection manager** and click `Connect`. Otherwise your prompt should work!

<figure><img src="/files/EsiyH067DhWg7HghyE1l" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/ixKPPrJLFBXDgpJ5cDBh" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/8VnBgwJ8pycr0vb5fpFg" alt=""><figcaption></figcaption></figure>

### ChatGPT Self Registration

1. Log in to [ChatGPT](https://chatgpt.com/) and from the menu - select `Workspace settings`&#x20;

<figure><img src="/files/1LMhr1sGkFdE82TnoxGy" alt=""><figcaption></figcaption></figure>

2. Select `Apps & Connectors`

<figure><img src="/files/IWpBCDqnQAhfA3nByJCe" alt=""><figcaption></figcaption></figure>

3. Click on `Create`

<figure><img src="/files/q2N0p2G8Dpl9qSYJhRGX" alt=""><figcaption></figcaption></figure>

4. Enter in a name for your connecter. For the `MCP Server URL` this should be in the following format: `https://{{ATTACKFORGE-HOSTNAME}}/mcp` . Click `Create`.

<figure><img src="/files/AIQXi2ZvHNCZ22j7A3cQ" alt=""><figcaption></figcaption></figure>

5. You will be redirected to AttackForge. If you are not logged in - you will first need to log in. After you have logged in - you will see the screen below. Click on `Agree and Continue`. You will then be redirected back to ChatGPT.

<figure><img src="/files/uRpGa5Vb3p2i31tcuSbo" alt=""><figcaption></figcaption></figure>

6. Click `Publish`

<figure><img src="/files/ckSbrnZGr4AzuXQkayHA" alt=""><figcaption></figcaption></figure>

7. Review and actions then click `Publish`

<figure><img src="/files/chomhMfgGlbh8MxoSmcp" alt=""><figcaption></figcaption></figure>

8. AttackForge MCP will now be available.

<figure><img src="/files/mSEEMb2hWL58wTKEv47N" alt=""><figcaption></figcaption></figure>

9. Try the integration by going to a new chat. Click on `+` and select `More`.  Select the AttackForge connector, then try a prompt!

<figure><img src="/files/gkPkrtkiuNpqyJcYqkQC" alt=""><figcaption></figcaption></figure>

### Claude Self Registration

1. Log in to [Claude](https://claude.ai/) and from the menu - select `Settings`&#x20;

<figure><img src="/files/EyUPP5skGsxBsfMGB0Hx" alt=""><figcaption></figcaption></figure>

2. Select `Connectors`

<figure><img src="/files/jJyOfbFsTSQTcj0Y17BW" alt=""><figcaption></figcaption></figure>

3. Select `Add custom connector`

<figure><img src="/files/qYi3l2LPKzZPLwRFlril" alt=""><figcaption></figcaption></figure>

4. Enter in a name for your connecter. For the `MCP Server URL` this should be in the following format: `https://{{ATTACKFORGE-HOSTNAME}}/mcp` . Click `Add`.

<figure><img src="/files/048QuhCpONiXm3mIhHlN" alt=""><figcaption></figcaption></figure>

5. Click `Connect`

<figure><img src="/files/ZbFyPpU5kUJDU1MLchbG" alt=""><figcaption></figcaption></figure>

6. You will be redirected to AttackForge. If you are not logged in - you will first need to log in. After you have logged in - you will see the screen below. Click on `Agree and Continue`. You will then be redirected back to Claude.

<figure><img src="/files/2id1bihph1OqpXA7kMSb" alt=""><figcaption></figcaption></figure>

7. Try the integration by going to a new chat. Click on `Settings` and enable the AttackForge connector, then enabled the relevant tools you have access to. Try a prompt!

<figure><img src="/files/idPlEtoPW5Q23PGuSOWb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6NJpYxIMqoNrlEypoG5l" alt=""><figcaption></figcaption></figure>

## Assisted Registration

<figure><img src="/files/WPKBktoNzniA18f30WbE" alt=""><figcaption></figcaption></figure>

If self registration is disabled - AI assistants can still get access to AttackForge MCP using assisted registration. The process is as follows:

1. AttackForge admin [manually registers the AI assistant](#manually-registering-mcp-client) for the required AttackForge user. This process results in the creation of an OAuth 2 Client Id and Client Secret which is then shared with the AttackForge user.
2. AttackForge user supplies the Client Id and Client Secret to their AI assistant to configure the integration.

### Manually Registering MCP Clients

1. Go to `Users > (Select the user) > Applications > MCP Client`

<figure><img src="/files/ggqlmP8pMgxcM3jPOhX3" alt=""><figcaption></figcaption></figure>

2. Click on `Add Client`. Include a name for the client and insert the redirect URLs. Click `Add`

> **TIP:** Click on the down arrow to see common redirect URLs for popular remote MCP clients

<figure><img src="/files/DRQkYHrDnxxnekZYjgvt" alt=""><figcaption></figcaption></figure>

3. Copy the **Client Id** and **Client Secret**.&#x20;

> **IMPORTANT:** The Client Secret will only be shown one time. Make sure to copy it.

<figure><img src="/files/QlTJN7u2WrpJbQbQsfkd" alt=""><figcaption></figcaption></figure>

### Manually Enrolling ChatGPT

1. Log in to [ChatGPT](https://chatgpt.com/) and from the menu - select `Workspace settings`&#x20;

<figure><img src="/files/1LMhr1sGkFdE82TnoxGy" alt=""><figcaption></figcaption></figure>

2. Select `Apps & Connectors`

<figure><img src="/files/IWpBCDqnQAhfA3nByJCe" alt=""><figcaption></figcaption></figure>

3. Click on `Create`

<figure><img src="/files/q2N0p2G8Dpl9qSYJhRGX" alt=""><figcaption></figcaption></figure>

4. Enter in a name for your connecter. For the `MCP Server URL` this should be in the following format: `https://{{ATTACKFORGE-HOSTNAME}}/mcp` . Enter in the [OAuth 2 Client Id and Client Secret](#manually-registering-mcp-clients). Click `Create`.

<figure><img src="/files/hTfVNSznE3TVE1Gl6BdP" alt=""><figcaption></figcaption></figure>

5. You will be redirected to AttackForge. If you are not logged in - you will first need to log in. After you have logged in - you will see the screen below. Click on `Agree and Continue`. You will then be redirected back to ChatGPT.

<figure><img src="/files/uRpGa5Vb3p2i31tcuSbo" alt=""><figcaption></figcaption></figure>

6. Click `Publish`

<figure><img src="/files/ckSbrnZGr4AzuXQkayHA" alt=""><figcaption></figcaption></figure>

7. Review and actions then click `Publish`

<figure><img src="/files/chomhMfgGlbh8MxoSmcp" alt=""><figcaption></figcaption></figure>

8. AttackForge MCP will now be available.

<figure><img src="/files/mSEEMb2hWL58wTKEv47N" alt=""><figcaption></figcaption></figure>

9. Try the integration by going to a new chat. Click on `+` and select `More`.  Select the AttackForge connector, then try a prompt!

<figure><img src="/files/gkPkrtkiuNpqyJcYqkQC" alt=""><figcaption></figcaption></figure>

### Manually Enrolling Claude

1. Log in to [Claude](https://claude.ai/) and from the menu - select `Settings`&#x20;

<figure><img src="/files/EyUPP5skGsxBsfMGB0Hx" alt=""><figcaption></figcaption></figure>

2. Select `Connectors`

<figure><img src="/files/jJyOfbFsTSQTcj0Y17BW" alt=""><figcaption></figcaption></figure>

3. Select `Add custom connector`

<figure><img src="/files/qYi3l2LPKzZPLwRFlril" alt=""><figcaption></figcaption></figure>

4. Enter in a name for your connecter. For the `MCP Server URL` this should be in the following format: `https://{{ATTACKFORGE-HOSTNAME}}/mcp` . Click `Advanced Settings`. Enter in the [OAuth 2 Client Id and Client Secret](#manually-registering-mcp-clients). Click `Add`.

<figure><img src="/files/7QClC0QaOz91XaB9hGeJ" alt=""><figcaption></figcaption></figure>

5. Click `Connect`

<figure><img src="/files/ZbFyPpU5kUJDU1MLchbG" alt=""><figcaption></figcaption></figure>

6. You will be redirected to AttackForge. If you are not logged in - you will first need to log in. After you have logged in - you will see the screen below. Click on `Agree and Continue`. You will then be redirected back to Claude.

<figure><img src="/files/2id1bihph1OqpXA7kMSb" alt=""><figcaption></figcaption></figure>

7. Try the integration by going to a new chat. Click on `Settings` and enable the AttackForge connector, then enable the relevant tools you have access to. Try a prompt!

<figure><img src="/files/idPlEtoPW5Q23PGuSOWb" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/6NJpYxIMqoNrlEypoG5l" alt=""><figcaption></figcaption></figure>

## Configuring Local MCP

<figure><img src="/files/IGEoDSog5iDuz47kflDS" alt=""><figcaption></figcaption></figure>

Local MCP are local Model Context Protocol servers that run directly on your machine rather than connecting to remote services, like [Claude Desktop](https://www.claude.com/download) and [LM Studio](https://lmstudio.ai/).

The key advantages of local MCP servers include:

* **Complete Data Privacy**: Local servers process data on your device, offering complete data privacy since information never leaves your machine.
* **Offline Functionality**: Local servers work without internet connectivity once downloaded, which is crucial for developers working in secure environments or areas with unreliable internet access.
* **Predictable Performance**: Local servers offer predictable performance since they're not dependent on network latency or external service availability.

Local MCP is particularly valuable for sensitive data processing, secure enterprise environments, and scenarios where you need AI capabilities without relying on external services.

### LM Studio Configuration

1. Download and install [LM Studio](https://lmstudio.ai/)

<figure><img src="/files/24QaoOkzsIBAwtHQwEhg" alt=""><figcaption></figcaption></figure>

2. Open LM Studio. Load your model. Click on `Settings > Program > Install > Edit mcp.json`

<figure><img src="/files/U8TboFHeJkVTZAb7R7Lc" alt=""><figcaption></figcaption></figure>

3. Open `mcp.json` tab. Add the following local MCP server. Make sure to add your AttackForge hostname for the `"AF_HOSTNAME"` and your AttackForge User API Key for the `"AF_USER_KEY"`.&#x20;

```json
{
    "mcpServers": {
        "af-mcp-server": {
            "command": "npx",
            "args": [
                "-y",
                "@attackforge/mcp-server"
            ],
            "env": {
                "AF_HOSTNAME": "demo.attackforge.com",
                "AF_USER_KEY": "asjkdhuwqj...<REMOVED>....kashdkjhdkqhu"
            }
        }
    }
}
```

<figure><img src="/files/ezpfFaivV2tOy962TfL0" alt=""><figcaption></figcaption></figure>

4. Click Save when finished and you should see the following success message.

<figure><img src="/files/da8fyMXIUm1w3gP7O0lC" alt=""><figcaption></figcaption></figure>

5. Open a new chat. Enable `mcp/af-mcp-server.` Configure the AF MCP tools you want to run, then try a test prompt!

<figure><img src="/files/Frn0A7Rt3U0Nz7IOZpgG" alt=""><figcaption></figcaption></figure>

### Claude Desktop Configuration

1. Download and install [Claude Desktop](https://www.claude.com/download)

<figure><img src="/files/KrfEluOo1ClyKb3Jcbfk" alt=""><figcaption></figcaption></figure>

2. Open Claude Desktop

<figure><img src="/files/lGJZOOmCydd4RJORtOOa" alt=""><figcaption></figcaption></figure>

3. Click on `Profile > Settings`

<figure><img src="/files/CNV5y7XDi3ZpUxjkyY57" alt=""><figcaption></figcaption></figure>

4. Click on `Developer`

<figure><img src="/files/6xdU519rlvvUjyLQeS6N" alt=""><figcaption></figcaption></figure>

5. Within **Local MCP Servers** - click on `Edit Config`

<figure><img src="/files/c7wm4wWI2nZPCZue1275" alt=""><figcaption></figcaption></figure>

6. Open `claude_desktop_config.json` in a text editor, and add the following local MCP server. Make sure to add your AttackForge hostname for the `"AF_HOSTNAME"` and your AttackForge User API Key for the `"AF_USER_KEY"`&#x20;

```json
{
    "mcpServers": {
        "af-mcp-server": {
            "command": "npx",
            "args": [
                "-y",
                "@attackforge/mcp-server"
            ],
            "env": {
                "AF_HOSTNAME": "demo.attackforge.com",
                "AF_USER_KEY": "asjkdhuwqj...<REMOVED>....kashdkjhdkqhu"
            }
        }
    }
}
```

7. Save the file. Quit/Close Claude Desktop. Re-open Claude Desktop and open your Settings and click on `Developer`. You should see `af-mcp-server` is now running.

<figure><img src="/files/p3eEtnMNIlM45z6u6sMQ" alt=""><figcaption></figcaption></figure>

8. In Settings, click on `Connectors` and observe `af-mcp-server` is available. You can configure the integration and tools from here.

<figure><img src="/files/tbv2eJA5zgYyc1JH6rPI" alt=""><figcaption></figcaption></figure>

9. Try the integration by going to a new chat. Click on `Settings` and enable the AttackForge connector, then enable the relevant tools you have access to. Try a prompt!

<figure><img src="/files/SiIvdXa7En5BUqjpD0Tg" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Ey15nV7nKxncfjRStuS6" alt=""><figcaption></figcaption></figure>

## User Access to MCP

Once a user has connected their AI assistant to AttackForge, they can start to leverage the MCP Tools made available to them.

> **IMPORTANT:** Each MCP Tool must be be enabled on a per-user basis by the AttackForge administrators for maximum security.

### Access to Tools

1. Go to `Users > (Select the user) > Access > MCP`

<figure><img src="/files/gkibDsZHh5H9YTTlhuWH" alt=""><figcaption></figcaption></figure>

2. [View details for each Tool](#tools). Select the tools to enable for the user. Click `Add`

<figure><img src="/files/gud2QcN7PnoqAtVoGdWx" alt=""><figcaption></figcaption></figure>

Once the tools have been enabled, the user is now able to access those tools in their AI assistant.

### MCP Sessions

When a user has established a session with AttackForge MCP using their AI assistant, their sessions will become visible in `Users > (Select the user) > Applications > MCP Sessions`

<figure><img src="/files/ar9yYUJe8qD62hPp9LAP" alt=""><figcaption></figcaption></figure>

## Removing User Access to MCP

To remove a users' access to AttackForge MCP - apply each of the following steps.

1. Remove [access to tools](#access-to-tools)
2. Revoke [MCP sessions](#mcp-sessions)
3. Remove [MCP clients](#manually-registering-mcp-clients)

## Working With Tools

The AttackForge MCP tools fall into three groups:

* **Read tools** - `whoami`, `find_*`, `count_*`, `get_file`, `get_field_structure`, `get_args_schema_for_tool`. These never modify data.
* **Write tools** - `create_*` and `patch_*`. These create or partially update records.
* **Schema/discovery tools** - `get_field_structure` and `get_args_schema_for_tool`, used to discover the tenant-specific shape of your data model before reading custom fields or building write payloads.

The following concepts are shared across many tools and are described once here rather than repeated on every tool.

#### Filter Syntax <a href="#filter-syntax" id="filter-syntax"></a>

`find_*` and `count_*` tools accept an optional `filter` parameter expressed as a MongoDB-style `find()` filter.

#### Supported operators

```
$and  $or  $nor  $search  $eq  $exists  $gt  $gte  $in
$lt   $lte $ne   $nin     $all $regex   $options $elemMatch  $not
```

#### $regex accepts either form (prefer the second)

```json
{ field: { $regex: /str/i } }
{ field: { $regex: "str", $options: "i" } }
```

#### Quoting rules - string values use single or double quotes, but operators and field names MUST be unquoted

```json
Valid:   { field: { $eq: "value" } }
Invalid: { "field": { "$eq": "value" } }
```

#### Examples

```json
{ name: { $regex: 'tls', $options: 'i' } }
{ status: { $eq: 'Testing' } }
{ id: { $eq: ObjectId('65a440c08cade68ca7bc7192') } }
{ id: { $in: [ ObjectId('65a440c08cade68ca7bc7192'), ObjectId('65a440c08cade68ca7bc7193') ] } }
{ custom_tags: { $elemMatch: { name: { $eq: 'region' }, value: { $eq: 'emea' } } } }
{ custom_fields: { $elemMatch: { key: { $eq: 'engagement_type' }, value: { $eq: 'pentest' } } } }
```

The fields you may reference inside `filter` are listed under each tool's **Parameters** section.

> Some scoping concerns must NOT be expressed inside `filter` and have dedicated parameters instead - for example `project_ids` and `visibility` on vulnerabilities, `writeup_library_keys` on writeups, and `asset_library_key` on assets. See the relevant tool.

#### Selecting Fields and Custom Fields <a href="#selecting-fields-and-custom-fields" id="selecting-fields-and-custom-fields"></a>

`find_*` tools return only the `id` of each record by default. To return more:

* `fields` - an array of system field names to include. The allowed values are enumerated per tool (see each tool's **Parameters**).
* `custom_field_keys` - an array of custom field keys to include in each record's `custom_fields` array. Discover the available keys for a model with `get_field_structure`.

This replaces the older behaviour where custom fields were returned implicitly. You now request exactly the fields you need.

#### Pagination <a href="#pagination" id="pagination"></a>

`find_*` tools are paginated:

* `limit` - maximum records to return in this page. Defaults to `10`, capped at `50`.
* `skip` - number of records to skip (offset). Defaults to `0`.

Each response includes `count` (records in this page) and `total` (records matching overall). When `count` equals `limit` and `skip + count < total`, there are more pages - call again with `skip` advanced by `limit`. Use the `count_*` tools when you only need a total and not the records themselves.

#### Two-Step Workflow for Write Tools <a href="#two-step-workflow-for-write-tools" id="two-step-workflow-for-write-tools"></a>

The `create_*` and `patch_*` tools take a single `args` object whose shape is configured **per tenant** (custom fields, optional features, role-based access) and therefore cannot be hard-coded. Always use the two-step workflow:

1. Call `get_args_schema_for_tool` with the target tool name to retrieve the current JSON Schema for `args`.
2. Build an `args` object that satisfies that schema, then call the target `create_*` / `patch_*` tool.

The returned schema is the same one the server uses for validation, so any payload that satisfies it will pass. Tenant configuration can change between sessions, so do not cache the schema across conversations.

Any rich-text field referenced in the schema must use the AttackForge richtext format described in the `richtext_format` block returned alongside the schema by `get_args_schema_for_tool`.

`patch_*` tools apply a **partial** update: only the keys you supply in `args` are changed; omitted keys are left untouched.

## Test Cases and Methodology <a href="#test-cases-and-methodology" id="test-cases-and-methodology"></a>

AttackForge separates the **methodology** you plan to follow from the **record of testing it** on a particular engagement. A number of MCP tools cover this area, and choosing the right one depends on understanding which record you are working with.

#### The Record Types <a href="#the-record-types" id="the-record-types"></a>

**Testsuite**

A reusable methodology: a named, taggable collection of test cases. A testsuite can be added to a project's testing plan, or proposed on a project request when `is_visible_on_project_requests` is set.

Tools: `find_testsuites`, `count_testsuites`, `create_testsuite`, `patch_testsuite`.

**Library Test Case**

A reusable, taggable unit of testing methodology belonging to a testsuite. This is the record that carries the **descriptive** fields - `title`, `details`, `code`, `tags`, `sort_order` and `execution_flow`.

Tools: `find_testcases`, `count_testcases`, `create_testcase`, `patch_testcase`.

**Project Test Case**

A per-project instance of a library test case, created when the testsuite is allocated to a project. It carries the **testing state** for that project only - `status` (`Tested`, `Not Tested`, `Testing In Progress` or `Not Applicable`), `assigned_to`, `project_scope_ids`, `vulnerability_ids` and `locked`.

A project test case does NOT carry the descriptive fields. To get them, take `testcase_id` from the project test case and pass it to `find_testcases` via its `testcase_ids` parameter.

Tools: `find_project_testcases`, `count_project_testcases`, `patch_project_testcase`, `update_project_testcase_lock`.

**Abuse Case**

A test case belonging to one project only. It lives in that project's own abuse-case testsuite rather than in the reusable methodology library, so it is created against a **project** rather than a testsuite.

Create one with `create_abuse_case` - `create_testcase` refuses abuse-case testsuites. Once created, an abuse case is patched with `patch_testcase` like any other test case, and its per-project instance is patched with `patch_project_testcase`.

#### Resolving Descriptive Fields <a href="#resolving-descriptive-fields" id="resolving-descriptive-fields"></a>

Because the descriptive fields live on the library test case, a typical read is two calls:

1. `find_project_testcases(project_id=...)` requesting the `testcase_id` field.
2. `find_testcases(testcase_ids=[...])` requesting `title`, `details` and whatever else you need.

Use `testcase_ids` rather than a `filter` on `id` for this step. A search never returns deleted records, so filtering will silently drop retired methodology and project abuse cases; `testcase_ids` resolves them. Request the `is_deleted` field to tell retired methodology apart from current.

The same split applies in reverse: `title`, `details`, `code`, `tags`, `sort_order` and `testsuite_id` are **not** filterable on `find_project_testcases`. To narrow by any of them, call `find_testcases` first and filter project test cases on the resulting `testcase_id` values.

#### Notes and Workspace Notes <a href="#notes-and-workspace-notes" id="notes-and-workspace-notes"></a>

A project test case can carry many notes, which is why they are separate tools rather than fields on the test case. There are two kinds, and they are not interchangeable:

|               | Note                                                                | Workspace Note                                                               |
| ------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Purpose       | The running log of testing progress - what the tester did and found | Reference material for performing the test case - background, tips, payloads |
| Read when     | You want to know how testing is going                               | You want the material a tester needs to hand                                 |
| Has a `title` | No                                                                  | Yes                                                                          |
| Tools         | `find_` / `count_` / `create_` / `patch_project_testcase_note(s)`   | `find_` / `count_` / `create_` / `patch_project_testcase_workspace_note(s)`  |

Both are rich-text, both may carry attached `files` (fetch them with `get_file`), and neither `create_*` tool can attach files.

> Not to be confused with **remediation notes**, which hang off a vulnerability rather than a test case and record remediation progress or discussion. Those use `find_remediation_notes`, `count_remediation_notes`, `create_remediation_note` and `patch_remediation_note`.

#### Locking <a href="#locking" id="locking"></a>

A project test case can be locked to freeze it against further editing within the project.

* Use `update_project_testcase_lock` to lock or unlock. `patch_project_testcase` cannot change the lock, and refuses to touch a test case while it is locked.
* A locked test case is **excluded from the project's test case counters**, so locking or unlocking shifts the project's tested / not tested / total figures.
* Locking requires at least View access to the project, and the caller must be an Admin or Project Coordinator.

#### Custom Fields <a href="#custom-fields" id="custom-fields"></a>

Library test cases and project test cases have **separate** custom field structures. Discover them with `get_field_structure(model="testcase")` and `get_field_structure(model="project-testcase")` respectively.

Note that the `custom_fields` you pass to `create_abuse_case` are set on the **project test case** it creates, not on the abuse case itself - use `patch_testcase` with the returned `testcase_id` to set custom fields on the abuse case.

## Prompt Examples

### Interactive Attack Chain Explorer

<figure><img src="/files/ulXwXmUTlzJzcaUSqJwh" alt=""><figcaption></figcaption></figure>

The Attack Chain Explorer generates an interactive web page from real AttackForge testing data — turning a project's findings into an animated, explorable kill-chain report. Pick from various distinct attack chains — different routes an attacker could realistically take through the engagement. Watch each chain animate stage-by-stage: attacker → recon → actionable stages → outcome. Hit `Best fix` to see the single highest-leverage remediation, or `Break chain` to see where any one fix collapses the path.

#### Tools Required

* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Skill MD

{% code overflow="wrap" %}

```
Contact Us
```

{% endcode %}

### MITRE ATT\&CK Attack Chain - Style 1

<figure><img src="/files/FMQ887h6GIzOQpxfcWzM" alt=""><figcaption></figcaption></figure>

The MITRE ATT\&CK option will frame the project's findings against MITRE's tactics — Reconnaissance, Initial Access, Execution, Persistence, etc. It's built to land the attack chain narrative, the remediation plan, and the priority call - all on one page, so it suits board / leadership / engineering briefings.

**Style 1 — Executive landscape, one-pager**

#### Tools Required

* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Skill MD

{% code overflow="wrap" %}

```
Contact Us
```

{% endcode %}

### MITRE ATT\&CK Attack Chain - Style 2

<figure><img src="/files/9YKItKn1VBbOIIyxKVEF" alt=""><figcaption></figcaption></figure>

The MITRE ATT\&CK option will frame the project's findings against MITRE's tactics — Reconnaissance, Initial Access, Execution, Persistence, etc. It's built to land the attack chain narrative, the remediation plan, and the priority call - all on one page, so it suits board / leadership / engineering briefings.

**Style 2 — Portrait narrative, debrief leave-behind**

#### Tools Required

* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Skill MD

{% code overflow="wrap" %}

```
Contact Us
```

{% endcode %}

### Cyber Kill Chain

<figure><img src="/files/jkt5U1B5kLcnL9sgANbi" alt=""><figcaption></figcaption></figure>

The Cyber Kill Chain option maps every finding in an AttackForge project onto the seven stages of the Lockheed Martin Cyber Kill Chain — but with modernized offensive-security labels: Reconnaissance, Payload Development, Initial Access, Exploitation, Persistence, Command & Control, Mission Impact.

#### Tools Required

* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Skill MD

{% code overflow="wrap" %}

```
Contact Us
```

{% endcode %}

### Generate Pentest Executive Summary

<figure><img src="/files/nCzXt3QU6At12djgx2Vv" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Assume the role of a highly experienced Chief Information Security Officer (CISO) drafting a report for the Executive Committee. Your task is to analyze the provided penetration testing data for the {{INSERT-PROJECT-NAME}} Project (data includes scope, findings severity breakdown, exploited vulnerabilities, and remediation time estimates) and synthesize it into a concise, single-page Executive Summary designed for C-level risk assessment.
The output must be a unified report, strictly adhering to the following structure and constraints:
1.	NO METADATA: DO NOT include any title, header, footer, or boilerplate text such as "Prepared by," "Classification," or "Date." The output must begin immediately with the body of the report.
2.	Length: Do not exceed the content volume of a single, standard business page.
3.	Conditional Findings (MANDATORY): This is the core logic. You must select between one (1) and three (3) findings based only on the highest available severity level, ignoring the finding's current status (open or closed).
- Priority 1: Critical (CVSS > 9.0): If one or more Critical findings exist, use them first (up to three). If none exist, proceed to Priority 2.
- Priority 2: High (CVSS 7.0 - 8.9): If no Critical findings were found, use High findings (up to three). If none exist, proceed to Priority 3.
- Priority 3: Medium (CVSS 4.0 - 6.9): If no High findings were found, use Medium findings (up to three). If none exist, proceed to Priority 4.
- Priority 4: Low/Info: Only if no Critical, High, or Medium findings exist, use Low or Informational findings (up to three).
4.	Dynamic Headings (MANDATORY): Use only these three bolded headings, in this exact order. The second heading must dynamically reflect the highest severity level found in Step 3, along with the count of findings presented.
-	Executive Overview
-	Top [Number] [Severity Level] Findings (Example: "Top Three Critical Findings," or "Top One High Finding." If no relevant findings are found, use a title like "No Critical or High Findings Identified.")
-	Immediate Mitigation Strategy
5.	Content: Under the dynamic findings heading, use clear, concise bullet points for each selected finding, detailing the risk and business impact.
6.	Conclusion: The final section, Immediate Mitigation Strategy, must conclude with a single, highly prioritized action item that delivers the greatest risk reduction immediately.
7.	Language: Use formal, authoritative, and risk-focused business language throughout.
8.	Presentation: Downloadable Microsoft Word Document
```

{% endcode %}

### Generate Vulnerability Descriptions and Recommendations

<figure><img src="/files/vI4QqPOhGBXT9EPPNVss" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Writeups](#find-writeups)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
You are a CERTIFIED, world-class cybersecurity penetration tester and senior security report writer. Your role is to transform technical data, collected via the AttackForge Connector MCP for the project {{INSERT-PROJECT-NAME}}, into concise, implementation-agnostic technical findings suitable for an enterprise penetration test report.
Your output will be consumed exclusively by technical engineering teams (e.g., developers, network engineers) and must adhere strictly to the specifications below.
1. REQUIRED OUTPUT STRUCTURE
For each and every vulnerability provided in the Input Data, rewrite it into the following three clearly labelled sections, in this exact order:
Finding Title (Formatted): The title must be the first line of the finding, displayed in bold, and the label 'Finding Title' must be excluded. A concise, precise title (under 10 words) that captures the core flaw.
Description: A concise, implementation-agnostic technical explanation of the vulnerability. Explain the root cause at a system or application logic level, not step-by-step exploitation. Maintain strict technical clarity without business or operational context.
Recommendation: Provide a technical direction for remediation, focusing on what needs to be addressed, not how to implement it. Do not reference specific security controls (e.g. “input validation”), code-level fixes, libraries, or configuration syntaxes. Keep the guidance high-level but technically meaningful for engineers.
2. STRICT EXCLUSIONS (DO NOT INCLUDE)
The rewritten findings must not contain any of the following:
•	Executive summaries or introductory statements
•	CVSS scores, risk ratings, or severity levels
•	Business impact, operational context, or data-loss-style consequences
•	Exploitation steps, reproduction details, or proof-of-concept information
•	Remediation strategy specifics, code samples, or control frameworks
•	External references, citations, or URLs
•	Mitigation controls framed as tactical instructions (e.g., “sanitize input,” “enable MFA,”)
3. TONE, FORMATTING, AND STYLE GUIDELINES
•	Maintain a formal, authoritative, and objective security tone.
•	Use precise, accurate security terminology.
•	The Finding Title must be the first line of output for each finding, rendered in bold, and formatted as a center-aligned headline.
•	Keep each section concise (typically 3–6 sentences per section).
•	Treat each vulnerability independently and clearly separate findings using line breaks.
•	Do not number findings unless the input data explicitly includes numbering.
4. OUTPUT OBJECTIVE
Your final output must read as if written by a senior penetration tester producing an enterprise-grade technical report, with:
•	clean structure
•	consistent phrasing
•	internally coherent terminology
•	readable, engineering-focused insight
5. PRESENTATION
•	Downloadable Microsoft Word Document
```

{% endcode %}

### Determine Single Highest-Risk Vulnerability on Project

<figure><img src="/files/s7hX3w4890s2nEf3Ywcx" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Writeups](#find-writeups)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
You are a world-class cybersecurity penetration tester and senior security report analyst (OSCP/OSCE-level). Your role is to transform technical data, collected via the AttackForge Connector MCP for the project {{INSERT-PROJECT-NAME}}. You specialize in transforming pentest output into concise, accurate, and actionable intelligence for security leadership.
CORE TASK
From the vulnerabilities found on the project, identify the single highest-risk vulnerability and return a fully structured analysis. Ignoring the finding's current status (open or closed).
EVALUATION RULES
•	Determine “highest-risk” using severity first (Critical → High → Medium → Low → Informational).
•	If multiple vulnerabilities share the same severity, choose the one with:
1.	The widest asset impact, then
2.	The highest exploitability, then
3.	The highest business impact (as described or inferable from the data).
OUTPUT FORMAT
Return your findings using the exact structure below:
1.	Vulnerability Name
2.	Severity & Risk Rating (CVSS or vendor rating)
3.	Associated Assets
-	List all affected hosts, endpoints, URLs, applications, or systems.
4.	Technical Summary
-	Clear explanation of the root cause and why the vulnerability exists.
5.	Evidence / Key Technical Details
-	Summarize PoC, payloads, reproduction steps, or scanner evidence (if provided).
6.	Business Impact
-	Explain how this vulnerability threatens confidentiality, integrity, availability, or overall organizational risk.
7.	Remediation Recommendation
-	Provide a precise, technically accurate fix (configuration change, patch, architecture control, etc.).
8.	Priority Justification
-	Brief explanation of why this vulnerability was selected as the highest-risk.
STYLE REQUIREMENTS
•	Structure and Coherence: Maintain a clean structure, consistent phrasing, and internally coherent terminology.
•	Tone: Maintain a formal, authoritative, and objective security tone.
•	Precision: Use precise, accurate security terminology and readable, engineering-focused insight.
•	Formatting: The Finding Title must be the first line of output for the finding, rendered in bold, and formatted as a center-aligned headline.
•	Conciseness: Keep each numbered section concise (typically 3–6 sentences per section).
•	Independence: Treat the selected vulnerability independently.
•	Numbering: Do not number findings unless the input data explicitly includes numbering.
PRESENTATION
•	Downloadable Microsoft Word Document
```

{% endcode %}

### Show Vulnerabilities Assigned to Me

<figure><img src="/files/5CUbRxZ1LhSctsg49r2s" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Whoami](#whoami)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
You are a highly detail-oriented Cyber Remediation Analyst specializing in interpreting current vulnerability data from the AttackForge Connector MCP.
Your primary task is to generate a comprehensive vulnerability list for both immediate remediation teams and historical auditing. The list must focus exclusively on vulnerabilities assigned to the current user, regardless of project.
The data retrieval must adhere precisely to the following criteria:
1.	Source: All data must be sourced from the AttackForge Connector MCP.
2.	Assignment: Only include vulnerabilities explicitly assigned to the current user ("me"), using the “vulnerability owner” field.
3.	Priority Inclusion: Include all defined priority levels (Critical, High, Medium, Low, Informational).
4.	Included Statuses: Include all vulnerabilities that are currently Open, Retest, or Closed.
5.	Data Scope: Include all projects associated with the assigned vulnerabilities.
Present the filtered results as a single, clear Markdown table with the following five columns, in this exact order:
•	Column 1: Vulnerability Name (Required Data: The full, unique name of the finding. Formatting Note: The name must be displayed in bold.) The data in this column MUST also be horizontally and vertically centered within the table.
•	Column 2: Priority (Required Data: The current priority level. Formatting Note: Use the exact priority terms.) The data in this column MUST also be horizontally and vertically centered within the table.
•	Column 3: Affected Assets Count (Required Data: The total number of assets currently affected by this vulnerability. Formatting Note: Must be rendered as an integer with no commas.) The data in this column MUST also be horizontally and vertically centered within the table.
•	Column 4: Project Name (Required Data: The name of the project containing the vulnerability. Formatting Note: The name must be displayed in italics.) The data in this column MUST also be horizontally and vertically centered within the table.
•	Column 5: Status (Required Data: The current remediation status. Formatting Note: Use a fixed-width/code block style, e.g., `Open`, `Closed`, or `Retest`.) The data in this column MUST also be horizontally and vertically centered within the table.
The entire table must be sorted first by Priority in strict descending order (Critical → High → Medium → Low → Informational) and secondarily by Affected Assets Count in descending order.
Following the table, write a one-paragraph summary of exactly four sentences that clearly reports on the following metrics:
1.	The total count of unique vulnerabilities returned in the table.
2.	The count of vulnerabilities that are currently Closed.
3.	The count of all vulnerabilities categorized as Critical or High that are still Open or Retest.
4.	The single Vulnerability Name (from Column 1) that affects the largest number of assets.
Presentation and Delivery Requirements:
•	Convert the entire output into a downloadable Word file. 
•	Set the document title to: “Vulnerabilities Assigned”
```

{% endcode %}

### Create a Vulnerability Composition Metrics Dashboard

<figure><img src="/files/2c0RTkNEB9JNr6plonNI" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Whoami](#whoami)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Goal: Design a single-screen, Dark Mode Security Operations Center (SOC) dashboard focused on Vulnerability Composition Metrics. The objective is to provide immediate, comparative situational awareness of severity distribution across distinct time windows and enable rapid triage. Data for this dashboard will be collected from the project. 1. Data & Filters * Source: AttackForge Connector (MCP) * Project: {{INSERT-PROJECT-NAME}} * Metric: Vulnerability Count (Total N) * Global Filter: Vulnerability Age < {{INSERT-NUMBER-OF-DAYS}} days (calculated from date created to today's date). * Dimensions: Severity (Critical, High, Medium, Low, Informational) 2. Aesthetic & Encoding (Dark Mode) * Theme: Dark Mode, industrial aesthetic, optimized for continuous monitoring (low glare, high readability). * UX Note: Implement subtle animations to signify data freshness. * Color Palette (High-Contrast Sequential Alarm):    * Critical: `#E31A1C` (Red)    * High: `#FF7F00` (Orange)    * Medium: `#FFD92F` (Yellow)    * Low: `#1F78B4` (Blue)    * Informational: `#A6CEE3` (Light Blue) 3. Visual Architecture: Comparative Composition (Top Section) * Layout: Single horizontal row of visuals across the top of the dashboard. * Visual Type: Donut Charts. * Encoding: Display the Total Count (N) in the center-aligned vertically and horizontally of the chart. Include a common legend. * Visuals Required (View):     * Total Vulnerabilities 4. Actionable Triage Table (Bottom Section) * Layout: Full-width interactive data grid below the comparative charts. * Functionality:    * Searchable by Vulnerability Title    * Clickable Status Filters: All, Open, Closed, Retest. * Required Columns (Order): Vulnerability Title, Severity, Age (in days), Discovered Date, Status. * Default Sorting Logic: *  1. Primary: Severity (Descending: Critical → Informational). 2. Secondary: Age (Ascending: Oldest first). Do NOT create or add things based on assumption. Strictly follow what is written in the prompt.
```

{% endcode %}

### Top 10 Vulnerabilities Report

<figure><img src="/files/RnXL4fO7KkRtYES5vwlk" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Role Definition: Act as a Senior Cybersecurity Analyst executing a critical data synthesis task.
Core Task: Using the vulnerability data collected exclusively from the AttackForge Connector MCP source, you must identify and present the top 10 most common vulnerabilities by their total count of occurrences.
Sorting & Tie-Breaking:
1.	The final list must be sorted descending by the Total Number of Occurrences.
2.	Crucially, ensure any and all vulnerabilities tied for the 10th position are fully included in the output.
Required Output Fields (in order):
1.	Vulnerability Name (Full descriptive name). The data in this column MUST also be horizontally and vertically left -centered within the table. Add 6pt line spacing before and after.
2.	Total Number of Occurrences (Exact numerical count). The data in this column MUST also be horizontally and vertically centered within the table. Add 6pt line spacing before and after.
3.	CVSS v3.1 Base Score (The exact numerical score, e.g., 9.8). The data in this column MUST also be horizontally and vertically centered within the table. Add 6pt line spacing before and after.
4.	Assigned Priority Rating (The corresponding rating: Critical, High, Medium, Low, or Informational) in BOLD. The data in this column MUST also be horizontally and vertically centered within the table. Add 6pt line spacing before and after.
Format & Constraints (Guardrails):
•	Document Title: The generated document must be titled: "Top 10 Vulnerabilities Report"
•	Timestamp: The very first line of the document (before the table) must state the exact Date, Time, and Time Zone the data was generated (e.g., "Generated on: 27/11/2025 at 09:10:18 AM AEDT").
•	Present this information in a clear, sortable markdown table.
•	STRICTLY PROHIBITED: Do not include any notes, legends, introductory summaries, or concluding remarks (other than the required title/timestamp).
•	DATA INTEGRITY MANDATE: USE EXACT DATA. DO NOT APPROXIMATE OR ESTIMATE ANYTHING.
Final Delivery Requirement: After generating the title, timestamp, and markdown table, immediately convert the resulting output into a downloadable Microsoft Word Document.
```

{% endcode %}

### Top 10 Vulnerabilities Dashboard

<figure><img src="/files/RgSjB6wCnxgTXoSwGBi0" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/yxiSjsfOIT1FUa5XMRns" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Role Definition: Act as a Senior Cybersecurity Analyst creating executive dashboards.
1. Priority Metric Tiles (KPIs)
These key performance indicator (KPI) tiles provide an immediate, high-level view of the current active vulnerability landscape. For the following Project in AttackForge {{INSERT-PROJECT-NAME}}
Data Source & Scope: All vulnerabilities identified via the AttackForge Connector MCP will be used to create the dashboard.
Have the project Name in the Centre of the Dashboard.
1. Top N Analysis Chart
Priority Metric Tiles (KPIs) These key performance indicator (KPI) tiles provide an immediate, high-level view of the current active vulnerability landscape.
•	Title: "Total Vulnerabilities Distribution."
•	Visualization Type: Set of 5 Square Metric Tiles (Treemap Style).
•	Metrics & Display: Tiles must represent the count for each standard severity level:
o	Critical
o	High
o	Medium
o	Low
o	Informational
•	Design Rule:
o	Use standard industry color mapping (e.g., #D32F2F for Critical, #F57C00 for High, #FBC02D for Medium, #7CB342 for Low and #0288D1 for Informational).
o	The numeric count must be vertically and horizontally center-aligned inside the square tile for maximum visibility.
•	Make sure the data and calculation of finding the 10 Most Common Vulnerabilities by Count is done CORRECTLY
•	Centre aligned with the dashboard. 
2. Top N Analysis Chart
This chart focuses on identifying the most recurring vulnerability types.
•	Visualization Type: Horizontal Bar Chart, sorted descending.
•	Title: " Top 10 Most Common Vulnerabilities by Count."
•	Dynamic Chart Resolution Requirements: 
o	Implement Tooltip for Full Title: Modify the chart to use a tooltip or hover-over functionality for the vulnerability title labels (y-axis/row labels). When a user hovers over any truncated title, a pop-up must appear displaying the full, complete, and untruncated vulnerability title.
o	Optimize Layout and Bar Length: Re-scale the horizontal length of the bars (representing the count) so they dynamically fit within the dedicated chart drawing space and do not obscure the title labels. Dynamically allocate sufficient horizontal space for the row labels on the left side of the chart.
3. Detailed Vulnerability Table
This section provides the comprehensive, actionable list of vulnerabilities.
•	Content: Showing only the top 10 Most Common Vulnerabilities by Count
•	Required Columns:
o	Name
o	Priority (Color Coded e.g., #D32F2F for Critical, #F57C00 for High, #FBC02D for Medium, #7CB342 for Low and #0288D1 for Informational).
o	CVSS Base Score (cvssv3_1_score)
o	Likelihood of Exploitation 
o	Date Found
•	Default Sorting: Table must default to be sorted descending by CVSS Score to prioritize the riskiest items immediately, if does not exists then sorted descending by Priority.
•	Filtering: Ensure filter controls are available for Priority. If none exists for the selected filter, mention e.g. “No Low Vulnerabilities ”
•	Make sure the data and calculation of finding the 10 Most Common Vulnerabilities by Count is done CORRECTLY
IMPORTANT NOTE: Do NOT create or add things based on assumption. Strictly follow what is written in the prompt. 
For ChatGPT ONLY remove the () (Create this dashboard as a downloadable HTML file) else delete it.
```

{% endcode %}

### Create Interactive Vulnerabilities Chart

<figure><img src="/files/KSmuCeFBPc8gHvOWXpKA" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Objective: Generate a complete, interactive dashboard element featuring the required Radar Chart, title, subtitle, horizontal legend, and key summary metrics, perfectly matching the visual design.
Data Source: AttackForge MCP for project "{{INSERT-PROJECT-NAME}}"
Dashboard Element Structure & Design
1.	Overall Container: The entire output must be rendered as a single, cohesive, dark-themed dashboard card/container with the specified layout.
2.	Top Header (Small Text): A small, subtle text • CISO SECURITY DASHBOARD, centered.
3.	Main Title: Average Vulnerability Age - Large, bold, centered, and prominently displayed.
4.	Subtitle (Under Main Title): Open vulnerabilities by severity • {{INSERT-PROJECT-NAME}} - Smaller font, centered.
Core Visualization Enforcement
1.	Chart Type (STRICT): The central graphical element MUST BE a five-axis Radar Chart (or Web Chart). The use of any other chart type (e.g., Donut, Bar) is strictly forbidden.
2.	Interactivity Enforcement (STRICT): The output HTML file must contain JavaScript to enable full interactivity. When a user hovers over or clicks on a data point on the radar chart, a dynamic tooltip/modal window must appear, clearly showing the exact Average Age for that severity axis (e.g., 'Avg Age: 142 Days').
Radar Chart Visualization Requirements
1.	Axes: Critical, High, Medium, Low, Informational.
2.	Metric: Average Open Vulnerability Age (in days). This metric determines the magnitude plotted on each axis.
o	Robust Calculation Logic: The magnitude for each axis is the mathematical average of the Raw Age (days between Vulnerability created and Today's Date) for all open vulnerabilities in that severity group.
3.	Size & Scale: The radar chart must be large and visually dominant within the container.
4.	Aesthetics:
o	Dark background, consistent theme.
o	Use high-contrast colors (Red, Orange, Yellow, Blue, Green/Gray).
o	The plotted area/line within the radar chart must have a distinct color (e.g., Purple/Blue as shown in the example).
Legend & Summary Metrics
1.	Horizontal Legend (Under Chart):
o	Place the legend directly under the radar chart.
o	Items must be stacked horizontally in a single row.
o	Each legend item must include: Colored dot, Severity Name, Average Age (in days), AND Total Open Vulnerability Count.
o	Example: [Red Dot] Critical | 2630 Days | 4 vulns
2.	Auxiliary Metrics (Bottom Cards):
o	Display three distinct, rounded-corner cards/boxes at the very bottom, arranged horizontally, matching the visual style.
o	Card 1 (Left): Label: OLDEST AVG AGE, Value: (Highest average age).
o	Card 2 (Middle): Label: TOTAL OPEN VULNS, Value: (Sum of all open vulnerabilities).
o	Card 3 (Right): Label: SEVERITIES TRACKED, Value: (Count of severity categories with data).
[ (This block Only Valid for ChatGPT, else delete it.) Output Requirement
Make this visualization a fully functional, interactive HTML File containing all necessary rendering libraries (e.g., Plotly, Chart.js, D3.js) and CSS to perfectly replicate the entire visual and functional design upon opening. a downloadable HTML File.]
```

{% endcode %}

### Review of Vulnerabilities in Retest

<figure><img src="/files/N4TNmf1b7BG6JH08DPtd" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Persona & Goal: You are a highly analytical Senior Application Security Remediation Manager. Your primary objective is to generate a clear, strictly actionable report on all outstanding projects and vulnerabilities sourced from the AttackForge MCP database. The report must prioritize required engineering effort and immediate focus areas.
Create a title exactly: “Review of Vulnerabilities in Retest Phase”
Data Source: Use all project and vulnerability data from the AttackForge MCP database.
Required Output Structure
Under this section, produce a single, consolidated markdown table listing every individual vulnerability instance where the Vulnerability Status is “retest” across all in-scope projects.
The table must include the following columns in this exact order:
1.	Vulnerability Name - Set the cell alignment to HORIZONTAL CENTER and VERTICAL CENTER, apply 6-point spacing BEFORE and AFTER the text within the cell, and ensure TEXT WRAPPING is enabled.
2.	Vulnerability Priority (Critical, High, Medium, Low, Informational)
3.	The EXACT, FULL, OFFICIAL NAME of the project linked to the vulnerability - Set the cell alignment to HORIZONTAL CENTER and VERTICAL CENTER, apply 6-point spacing BEFORE and AFTER the text within the cell, and ensure TEXT WRAPPING is enabled.
4.	Affected Assets Count (Total number of assets affected by that specific vulnerability instance) - Set the cell alignment to HORIZONTAL CENTER and VERTICAL CENTER, apply 6-point spacing BEFORE and AFTER the text within the cell, and ensure TEXT WRAPPING is enabled.
Design Requirements:
1.	Page orientation – vertical.
2.	Table Heading in BOLD and WHITE text and dark BLUE background. Set the cell alignment to HORIZONTAL CENTER and VERTICAL CENTER, apply 6-point spacing BEFORE and AFTER the text within the cell, and ensure TEXT WRAPPING is enabled.
3.	Colour the text in the Vulnerability Priority based on cyber security standards.
Requirements:
1.	The table must include only real data from the Attack Forge MCP dataset.
2.	Do not include any placeholder content, boilerplate, summaries, or executive-level narrative.
2. Presentation
The final output must be formatted for easy download and review as a Downloadable PDF.

```

{% endcode %}

### Top 10 Affected Assets Dashboard

<figure><img src="/files/N2JwdIKYtf56L21Ynb4p" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Goal: Generate the Executive Summary Dashboard for the "{{INSERT-PROJECT-NAME}}" project, emphasizing prioritized, actionable insights, and strict adherence to industry-standard risk coloring (Red, Orange, Yellow, Blue, Gray).
Persona: Lead Pentester and Reporting Manager.
Dashboard Components
1.	Metric Cards (Top Row) horizontally stacked:
o	Total Open Vulnerabilities (Count)
o	Total Critical/High Findings (Count)
o	Average CVSSv3 Score (Numeric, to one decimal place)
2.	Top N Affected Assets Pie Chart (Dynamic Rendering)
o	Metric: Show the distribution of the Top 10 assets with the highest cumulative count of Critical, High, Medium, Low & Informational open vulnerabilities.
o	Data Rendering Constraint (Crucial Update): The pie chart must ONLY render slices for assets that have a Critical, High, Medium, Low & Informational open vulnerabilities count within the currently filtered dataset. If the total number of assets with findings is less than 10, display all found assets. Do not display an "Other" or blank section for non-existent categories.
o	Interaction: Must be highly interactive.
>	On Click/Hover: Display a tooltip/modal showing the Asset Name, its Total Critical, High, Medium, Low & Informational open vulnerabilities count greater than or equal to one, and an immediate list (or link to a detailed view) of the associated Critical, High, Medium, Low & Informational open vulnerabilities.
o	Filtering: This chart acts as the Primary Global Filter. Selecting a slice must dynamically update the data in ALL subsequent visualizations to reflect the selection of that specific asset.
```

{% endcode %}

### CVSS Dashboard

<figure><img src="/files/HZm0FIaGeGlLcVxSKJHi" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Count Vulnerabilities](#count-vulnerabilities)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Prompt Persona & Goal: You are a senior-level Cybersecurity Remediation Specialist compiling the Executive Summary section of a formal penetration test report. Your objective is to visually communicate the immediate risk posture to non-technical stakeholders.
Data Source & Requirement: Utilizing the raw vulnerability data extracted from AttackForge MCP Project "{{INSERT-PROJECT-NAME}}", perform the following four mandatory steps:
Step 1: Data Categorization and Standardization (The Foundation) Group all vulnerabilities based strictly on their CVSSv3 Base Score into the following four standard industry severity tiers. Use these exact labels for the categories:
•	Critical (9.0 - 10.0)
•	High (7.0 - 8.9)
•	Medium (4.0 - 6.9)
•	Low (0.1 - 3.9)
Only collect vulnerabilities with a CVSSv3 Base Score assigned to it. Do NOT add vulnerabilities without it.
Step 2: Visualization Code Generation (The Output) Generate a React artifact (NOT an HTML file with external CDN scripts) to create a professional Vertical Bar Chart.
•	Do NOT use external CDN libraries like Plotly CDN - these will be blocked and cause script errors.
•	The chart must plot the total count of findings (Y-axis) for each of the four severity categories (X-axis).
•	Set a professional, corporate color scheme (e.g., bright red for Critical, dark orange for High, yellow for Medium, light blue for Low).
•	Do NOT display count labels on top of the bars - counts should only appear in the tooltip on hover and in the summary section below the chart.
Step 3: Presentation and Formatting (The Report View) Format the final output with the following required elements:
1.	Main Heading (H1): The heading above the chart must be the project name: "{{INSERT-PROJECT-NAME}}" 
2.	Chart Title: The visualization itself must include the title: Vulnerability Count by CVSSv3 Base Score.
3.	Axis Labels: Clearly label the X-axis as Severity Tier and the Y-axis as Total Count of Findings.
4.	Key: Under the chart, include a center-aligned key listing the specific score range for each of the four severity tier labels (e.g., Critical (9.0-10.0)).
Step 4: Enhanced Interactivity (The Functionality) Ensure the React artifact includes functionality where clicking on any individual bar dynamically displays a list of ALL Vulnerability Names, EXACT CVSSv3 Base Scores, and priority contributing to that specific severity count.
```

{% endcode %}

### Unique OWASP Top 10 Vulnerabilities Report

<figure><img src="/files/EvLCiL6MbKbjPDZ0Ahl7" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Find all vulnerabilities that have the tag “OWASP Top 10.” For each matching vulnerability, return the following information:
A brief description explaining what the OWASP Top 10 is.
1.	Filter to show ONLY STATUS “Open” Vulnerabilities. Do NOT include “Closed”
2.	Vulnerability Name – Enable text wrapping; centre-align both vertically and horizontally. Do NOT show Duplicate Vulnerability. This is unique.
3.	Priority – Colour the cell based on cybersecurity standards; centre-align both vertically and horizontally. If multiple associated priority, separate them using a “, ”.
4.	If Vulnerability has more than 1 priority, create separate row each different priority.
5.	Associated Project Name – Enable text wrapping; centre-align both vertically and horizontally. If multiple associated project names, separate them using a “, ”.
6.	Affected Assets – Enable text wrapping; centre-align both vertically and horizontally. If multiple associated assets, separate them using a “, ”.
7.	Do NOT include "Total Vulnerabilities" summary.
8.	IMPORTANT – Accurately count all vulnerabilities and check for duplicates.
*Note if a vulnerability has different priority, break it into a separate row with its associated projects & assets.
Present the results in a table format, sorted strictly from Critical to Informational priority.
Presentation Requirements:
•	Page heading: “OWASP Top 10 Vulnerabilities”, centred.
•	Page orientation: Portrait.
•	Add column numbering for each row.
•	Header row (Row 1): white text with a dark blue background.
•	The Priority column must be centre aligned.
•	Enable text wrapping for all table cells.
•	The final output must be fully formatted and ready for download as a PDF, optimised for clear and effective review.
```

{% endcode %}

### Interactive Executive Project Closeout Scorecard

<figure><img src="/files/c7Pl0VbVoBz50xivCsOe" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/RpUZrNiQ4t8X97eDiyiF" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/1RhuYgdTR74rVqxKGuXD" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/mh2zaeZyzQxZJnGH38eV" alt=""><figcaption></figcaption></figure>

#### Tools Required

* [Count Vulnerabilities](#count-vulnerabilities)
* [Find Vulnerabilities](#find-vulnerabilities)
* [Find Projects](#find-projects)
* [Find Affected Assets](#find-affected-assets)
* [Get Field Structure](#get-field-structure)

#### Prompt

{% code overflow="wrap" %}

```
Objective & Context
Imagine you are a Cyber Security Engineer creating a final, high-stakes Executive Project Closeout Dashboard. This dashboard must summarize the security status and risk of  closed projects to the Executive Leadership Team. Only showing projects with "completed status"

Task: Create an Interactive Executive Project Closeout Scorecard.
The dashboard must be presented in a concise, single-screen view with zero scrolling, utilizing clear, explicit risk color-coding.

Name of Dashboard: Recently Closed Projects Summary

Project Summary Table
Present the all projects in a sortable, prioritized table. Each row must display the following data, with visual emphasis on risk:

- Project Name (clickable, with text wrapping, opens full project detail view)
- Start Date: Show EXACT Start Date (formatted as DD-MM-YYYY)
- End Date: Show EXACT End Date (formatted as DD-MM-YYYY)
- Total Assets: (Count of unique assets involved in the project)

Data: Vulnerability Breakdown
For each project, the total vulnerabilities must be broken down and displayed as separate, countable fields:

- Total Vulnerabilities: (Overall Count)
- Count of Critical Vulns (Must be strictly colored RED #DC2626)
- Count of High Vulns (Must be strictly colored ORANGE #EA580C)
- Count of Medium Vulns (Must be strictly colored BLUE #2563EB)
- Count of Low Vulns (Must be strictly colored GREEN #16A34A)
- Count of Info Vulns (Must be strictly colored GREY #6B7280)

Presentation and Actionability
The dashboard's primary function is to enable immediate risk investigation.

Initial Prioritization: The table must be sorted by the Count of Critical Vulns (highest to lowest) upon initial loading.

Vulnerability-to-Impact Modal: When the user clicks on any individual vulnerability count (e.g., the 'Count of Critical Vulns' number) for a specific project, a modal window must immediately open.
- Modal Content: This modal must show the specific list of vulnerabilities that were clicked, alongside a count of its assigned affected assets (Total Assets assigned to that specific vulnerability type).

Project Name Click: When the user clicks on the Project Name, a new full-screen detail view must be triggered. This view must consist of:

1. Project Title Header:
   - Project name displayed in center
   - Large font (1.75rem), bold (700 weight)
   - Rounded border (12px radius) with background color var(--bg-tertiary)
   - Border: 1px solid var(--border-accent)
   - Padding: 14px 32px
   - "Back to Dashboard" button on the right (position: absolute, right: 32px)

2. Bubble Chart Section - EXACT SPECIFICATIONS:
   
   Container:
   - Height: 500px
   - Background: var(--bg-secondary)
   - Border-radius: 12px
   - Border: 1px solid var(--border-color)
   - Padding: 24px
   - Position: relative (for reset button positioning)
   
   SVG Chart Dimensions:
   - Width: container width or 900px fallback
   - Height: container height or 450px fallback
   - Margins: { top: 30, right: 40, bottom: 65, left: 110 }
   - Chart area: width - margins, height - margins
   
   X-Axis (Discovery Date):
   - Type: Linear scale using JavaScript timestamps (milliseconds)
   - Range: min/max from vulnerability dates with 10% padding on each side
   - Padding calculation: dateRange * 0.1 or 86400000 (1 day) minimum
   - Tick labels: 7 ticks (0 to 6), formatted as "DD MMM" (e.g., "22 Nov")
   - Tick label position: y = height - margin.bottom + 25
   - Axis title: "Discovery Date" at y = height - 10, centered
   
   Y-Axis (Severity Level):
   - Scale: Critical=5, High=4, Medium=3, Low=2, Info=1
   - Range: 0.5 to 5.5 for proper spacing
   - Labels: "Critical", "High", "Medium", "Low", "Info"
   - Label position: x = margin.left - 20, text-anchor: end
   - Axis title: "Severity Level" at x = 20, rotated -90 degrees
   
   Axis Styling:
   - Axis titles: 15px, font-weight 600, fill #c0c0c8
   - Tick labels: 13px, JetBrains Mono font, fill #a0a0b0
   - Grid lines: stroke rgba(255, 255, 255, 0.05), stroke-width 1
   - Axis lines: stroke #2a2a3a, stroke-width 1
   
   Bubble Specifications:
   - Position X: xScale(new Date(vulnerability.created).getTime())
   - Position Y: yScale(severityValue + jitter)
   - Jitter: (Math.random() - 0.5) * 0.4 (range: ±0.2)
   - Radius formula: Math.min(20, Math.max(8, assets * 5 + 6))
     - Minimum radius: 8px
     - Maximum radius: 20px
     - Base: 6px + (assets × 5px)
   - Fill opacity: 0.85
   - Stroke: same color as fill
   - Stroke-width: 1
   
   Bubble Colors (with 0.85 opacity):
   - Critical: #DC2626
   - High: #EA580C
   - Medium: #2563EB
   - Low: #16A34A
   - Info: #6B7280
   
   Hover Tooltip:
   - Position: fixed, top: 50%, left: 50%, transform: translate(-50%, -50%)
   - Background: var(--bg-elevated)
   - Border: 1px solid var(--border-accent)
   - Border-radius: 12px
   - Padding: 16px 20px
   - Min-width: 280px
   - Shows: vulnerability title, discovery date (DD-MM-YYYY), affected assets count
   - Toggle visibility with .active class
   
   Clear Selection Button:
   - Position: absolute, top: 12px, right: 12px
   - Background: var(--bg-tertiary)
   - Border: 1px solid var(--border-color)
   - Border-radius: 6px
   - Padding: 8px 16px
   - Font-size: 0.8rem
   - Only visible when filter is active

3. Chart Legend:
   - Display: flex, justify-content: center, gap: 24px
   - Margin-top: 16px
   - Each item: colored dot (10px × 10px, border-radius: 50%) + label
   - Font-size: 0.8rem, color: var(--text-secondary)

4. Total Vulnerabilities Distribution Section:
   - Title: "Total Vulnerabilities Distribution"
   - Title font: 1.1rem, font-weight 600
   - Container: flex, column, align-items center, gap 16px, margin-top 8px
   
   Distribution Boxes:
   - Container: flex, gap: 12px, justify-content: center
   - Each box: 100px × 80px
   - Border-radius: 8px
   - Border: 2px solid transparent (white when selected)
   - Cursor: pointer
   - Transition: all 0.2s ease
   - Hover: translateY(-3px), box-shadow: 0 6px 20px rgba(0,0,0,0.4)
   - Selected state: border-color white, box-shadow: 0 0 0 2px rgba(255,255,255,0.3)
   
   Box Content:
   - Count: font-size 1.8rem, line-height 1
   - Label: font-size 0.65rem, uppercase, letter-spacing 0.05em, margin-top 6px, opacity 0.9
   
   Box Colors:
   - Critical: var(--critical) #DC2626
   - High: var(--high) #EA580C
   - Medium: var(--medium) #2563EB
   - Low: var(--low) #16A34A
   - Info: var(--info) #6B7280

5. Vulnerability Table:
   - Container: background var(--bg-secondary), border-radius 12px, border 1px solid var(--border-color)
   - flex-shrink: 0 (don't stretch to fill page)
   
   Table Header (fixed):
   - Columns: Vulnerability Name (55%), Priority (15%), Discovered (15%), Affected Assets (15%)
   - Background: var(--bg-tertiary)
   - Padding: 12px 20px
   - Font: 0.7rem, uppercase, letter-spacing 0.05em
   - Color: var(--text-muted)
   
   Table Body:
   - Wrapper: overflow-y auto, max-height 400px
   - Row padding: 14px 20px
   - Row hover: background var(--bg-tertiary)
   - Highlighted row: background var(--bg-elevated), box-shadow inset 3px 0 0 var(--accent)
   - Cursor: pointer
   
   Priority Column:
   - Colored dot (8px × 8px) + label
   - Dot colors match severity colors
   
   Discovered Column:
   - Format: DD-MM-YYYY
   - Font: JetBrains Mono, 0.8rem
   - Text-align: center

6. Interactions:
   - Click distribution box → filters chart and table to that severity only
   - Click same box again → removes filter, shows all
   - Click table row → shows only that vulnerability in chart
   - Click bubble → highlights corresponding table row
   - Clear Selection button → resets all filters and selections
   - Escape key → closes modals/detail view
   - Click outside modal → closes modal

Executive Visual Requirements

Name of Dashboard: Must be centre-aligned and use a bold, prominent font with gradient effect.
- Font-size: 2rem
- Font-weight: 700
- Letter-spacing: -0.02em
- Background: linear-gradient(135deg, #fff 0%, #a0a0b0 100%)
- -webkit-background-clip: text
- -webkit-text-fill-color: transparent

Dark Theme CSS Variables:
- --bg-primary: #0a0a0f
- --bg-secondary: #12121a
- --bg-tertiary: #1a1a24
- --bg-elevated: #22222e
- --text-primary: #f0f0f5
- --text-secondary: #a0a0b0
- --text-muted: #6a6a7a
- --border-color: #2a2a3a
- --border-accent: #3a3a4a
- --accent: #8b5cf6 (purple for highlights)

Typography:
- Primary font: Space Grotesk (from Google Fonts)
- Monospace font: JetBrains Mono (from Google Fonts)

Technical Requirements:
- Single HTML file with embedded CSS and JavaScript
- NO external Chart.js - use pure SVG for bubble chart
- Google Fonts CDN for typography
- Responsive design
- Keyboard support (Escape to close modals)
- Click outside modal to close
- Table container should not stretch beyond content
- Main dashboard table should end at last row, not fill viewport

Query AttackForge for:
- 5 projects with status "Completed"
- All vulnerabilities for each project including: id, title, priority, created date, affected_assets
```

{% endcode %}


# Count Assets

#### Description <a href="#description" id="description"></a>

Returns the number of AttackForge Assets matching the optional `filter`, scoped to the asset library indicated by `asset_library_key`. Use this when you only need a total - call `find_assets` for the records. Requires the Assets module to be enabled; otherwise returns an error.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_assets` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many assets do I have?*
* *How many assets did I create (my library)?*
* *How many production assets are there?*
* *How many uncatalogued assets exist?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter           | Type   | Required | Description                                                                                                                                                                                                                                                                                                                                            |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `asset_library_key` | string | No       | Scope of asset libraries to count from. Built-in keys: `"all"` (every accessible library, also the default), `"my"` (assets created by the calling user), `"uncatalogued"` (assets not in any library). A 24-char hex string targets one specific custom library by its id. **Do NOT** filter on library membership via `filter` - use this parameter. |
| `filter`            | object | No       | A MongoDB-style filter. Supported fields are listed below.                                                                                                                                                                                                                                                                                             |

**Supported `filter` fields:**

| Field                 | Type / Values                                      |
| --------------------- | -------------------------------------------------- |
| `id`                  | `ObjectId('<24 hex>')`                             |
| `created`, `modified` | ISO-8601 datetime                                  |
| `name`                | string                                             |
| `asset_type`          | string                                             |
| `external_asset_id`   | string                                             |
| `custom_fields`       | array of `{ key: string, value: string or array }` |

#### Example Response

```json
{
  "count": 248
}
```


# Count Projects

#### Description <a href="#description" id="description"></a>

This tool can be used to count Projects using a provided filter expression.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_projects` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

```
How many projects do I have in AttackForge?
```

#### Supported Query Fields <a href="#supported-query-fields" id="supported-query-fields"></a>

{% code overflow="wrap" %}

```javascript
id: {
  description: "This is the project id.",
  type: 'string',
  pattern: "ObjectId\\(\\'[0-9a-fA-F]{24}\\'\\)",
}
created: {
  description: 'The timestamp that this project was created.',
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
}
modified: {
  description: 'The timestamp that this project was last modified.',
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
}
name: {
  type: 'string',
}
code: {
  type: 'string',
}
start_date: {
  description: 'The timestamp that this project is expected to commence.',
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
}
end_date: {
  description: 'The timestamp that this project is expected to conclude.',
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
}
status: {
  enum: [
    'Completed',
    'On Hold',
    'Overrun',
    'Retest',
    'Testing',
    'Waiting to Start'
  ]
}
org_code: {
  description: 'This is used to capture the Organizational Code that this project belongs',
  type: 'string',
}
vuln_code: {
  description: 'The Vulnerability Code that is used to generate the alernate id for the project vulnerabilities',
  type: 'string',
}
attack_chains_enabled: {
  description: 'Indicates if attack chains are enabled on this project',
  type: 'boolean'
}
reporting_enabled: {
  description: 'Indicates if reporting is enabled on this project',
  type: 'boolean'
}
retesting_enabled: {
  description: 'Indicates if the retesting workflow is enabled on this project',
  type: 'boolean'
}
summary_enabled: {
  description: 'Indicates if the summary page is enabled on this project',
  type: 'boolean'
}
custom_tags: {
  type: 'array',
  items: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      value: { type: 'string' }
    },
    required: ['name', 'value'],
    additionalProperties: false
  }
}
custom_fields: {
  type: 'array',
  items: {
    type: 'object',
    properties: {
      key: {
        type: 'string',
        pattern: '^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$',
      },
      value: {
        oneOf: [
          {
            type: 'string',
          },
          {
            type: 'array',
          },
        ]
      }
    },
    required: ['key', 'value'],
    additionalProperties: false
}
```

{% endcode %}

#### Example Response <a href="#example-response" id="example-response"></a>

```json
{
	"count": 15
}
```


# Count Project Requests

#### Description <a href="#description" id="description"></a>

Returns the number of AttackForge project requests matching the optional `filter`. Use this when you only need a total - call `find_project_requests` for the records. Results are automatically scoped to project requests the calling user can access (non-admin users only count project requests they have View access to). Returns `0` when project requests are disabled for the tenant (`frontend.enable_project_requests`).

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_project_requests` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many project requests are pending approval?*
* *How many project requests require onsite testing?*
* *How many project requests were submitted this quarter?*
* *How many project requests have been rejected?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                                |
| --------- | ------ | -------- | ---------------------------------------------------------- |
| `filter`  | object | No       | A MongoDB-style filter. Supported fields are listed below. |

**Supported `filter` fields:**

| Field                     | Type / Values                                                                      |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `id`                      | `ObjectId('<24 hex>')`                                                             |
| `created`, `modified`     | ISO-8601 datetime                                                                  |
| `name`, `code`            | string                                                                             |
| `organization_code`       | string                                                                             |
| `status`                  | one of `'Pending Approval'`, `'Approved'`, `'Rejected'`, `'Requested Information'` |
| `desired_start_date`      | ISO-8601 datetime                                                                  |
| `desired_end_date`        | ISO-8601 datetime                                                                  |
| `onsite_testing_required` | one of `'Yes'`, `'No'`                                                             |
| `custom_fields`           | array of `{ key: string, value: string or array }`                                 |

#### Example Response

```json
{
  "count": 7
}
```


# Count Project Scope

#### Description <a href="#description" id="description"></a>

Returns the number of Project Scope items belonging to one AttackForge project. A Project Scope item represents one thing that is within the testing scope of that project (a URL, host, application, etc.). Use this when you only need a total - call `find_project_scope` to retrieve the items themselves. The caller must have at least View access to the project; if not, an access-denied error is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_project_scope` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many scope items are in project X?*
* *How many scope items in project X are linked to an asset?*
* *How many in-scope URLs does the ACME project have?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter    | Type   | Required | Description                                                                                                                               |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id` | string | Yes      | The id (24-hex) of the project whose scope you want to count. Obtain it from `find_projects`. The caller must have View access or higher. |
| `filter`     | object | No       | A MongoDB-style filter. Supported fields are listed below.                                                                                |

**Supported `filter` fields:**

| Field      | Type                   | Description                                                                                                  |
| ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `id`       | `ObjectId('<24 hex>')` | the Project Scope item id                                                                                    |
| `name`     | string                 | the scope item name (URL, host, application, etc.)                                                           |
| `asset_id` | `ObjectId('<24 hex>')` | id of the linked Asset (only set when the Assets module is enabled and the scope item is linked to an Asset) |

#### Example Response

```json
{
  "count": 12
}
```


# Count Project Test Cases

#### Description <a href="#description" id="description"></a>

Returns the number of test cases allocated to one AttackForge project. A project test case is a per-project instance of a library test case - it carries the testing state for that project: `status`, who it is `assigned_to`, which project scope items it covers, and whether it is `locked`. Use this when you only need a total - call `find_project_testcases` to retrieve the test cases themselves.

The caller must have access to the project's test cases; if not, an access-denied error is returned. See Test Cases and Methodology.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_project_testcases` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many test cases are in project X?*
* *How many test cases in project X are still not tested?*
* *How many test cases are assigned to me on this project?*
* *How many test cases in project X are unassigned?*
* *How many test cases have been locked on this project?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter    | Type   | Required | Description                                                                                                                                       |
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id` | string | Yes      | Id (24-hex) of the project whose test cases you want to count. Source: `find_projects`. The caller must have access to this project's test cases. |
| `filter`     | object | No       | A MongoDB-style filter. Supported fields are listed below.                                                                                        |

**Supported `filter` fields:**

| Field                 | Type / Values                                                                  |
| --------------------- | ------------------------------------------------------------------------------ |
| `id`                  | `ObjectId('<24 hex>')` - the project test case id                              |
| `created`, `modified` | ISO-8601 datetime                                                              |
| `status`              | one of `'Tested'`, `'Not Tested'`, `'Testing In Progress'`, `'Not Applicable'` |
| `assigned_to`         | `ObjectId('<24 hex>')` - the user the test case is assigned to                 |
| `locked`              | boolean - supports `$eq`, `$ne` and `$exists` only                             |
| `custom_fields`       | array of `{ key: string, value: string or array }`                             |
| `testcase_id`         | `ObjectId('<24 hex>')` - the parent library test case                          |
| `user_id`             | `ObjectId('<24 hex>')`                                                         |

> Disallowed inside `filter`: `project_id` (use the parameter), and `title`, `details`, `code`, `tags`, `sort_order`, `testsuite_id` - these live on the parent library test case. To narrow by them, call `find_testcases` first and filter on the resulting `testcase_id` values.

#### Example Response

```json
{
  "count": 86
}
```


# Count Project Test Case Notes

#### Description <a href="#description" id="description"></a>

Returns the number of notes recorded against the test cases of one AttackForge project. A note records **progress** working the test case - the running log of what the tester did and found. For reference material useful in performing a test case (background, tips, payloads) use `count_project_testcase_workspace_notes` instead.

Use this when you only need a total - call `find_project_testcase_notes` to retrieve the notes themselves. The caller must have access to the project's test cases; if not, an access-denied error is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_project_testcase_notes` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many testing notes have been recorded on project X?*
* *How many notes are on this test case?*
* *How many notes did I write on project X this week?*
* *How many test case notes mention TLS?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter              | Type   | Required | Description                                                                                                                                                                                                                             |
| ---------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id`           | string | Yes      | Id (24-hex) of the project whose test case notes you want to count. Source: `find_projects`. The caller must have access to this project's test cases.                                                                                  |
| `project_testcase_ids` | array  | No       | Restrict the count to these project test cases (array of 24-hex ids). Source: `find_project_testcases` (its `id` field). Omit to count notes across every test case in the project. Ids that do not belong to this project are ignored. |
| `filter`               | object | No       | A MongoDB-style filter, applied on top of `project_id` and `project_testcase_ids` - a note must satisfy all of them to be counted. Supported fields are listed below.                                                                   |

**Supported `filter` fields:**

| Field                 | Type / Values          |
| --------------------- | ---------------------- |
| `id`                  | `ObjectId('<24 hex>')` |
| `project_testcase_id` | `ObjectId('<24 hex>')` |
| `created`, `modified` | ISO-8601 datetime      |
| `note`                | string                 |
| `user_id`             | `ObjectId('<24 hex>')` |

> Notes are stored as AttackForge richtext (HTML), so a `$regex` on `note` matches the markup as stored, not the rendered text. Keep patterns to plain words to avoid matching tags.

#### Example Response

```json
{
  "count": 31
}
```


# Count Project Test Case Workspace Notes

#### Description <a href="#description" id="description"></a>

Returns the number of workspace notes recorded against the test cases of one AttackForge project. A workspace note is **reference material** useful for performing the test case - background, tips, payloads, or anything else worth having to hand while testing it. It is not a record of testing progress: for that, use `count_project_testcase_notes` instead.

Use this when you only need a total - call `find_project_testcase_workspace_notes` to retrieve the workspace notes themselves. The caller must have access to the project's test cases; if not, an access-denied error is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_project_testcase_workspace_notes` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many workspace notes are on project X?*
* *How many reference notes does this test case have?*
* *How many workspace notes mention payloads?*
* *How many workspace notes were added to this project this month?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter              | Type   | Required | Description                                                                                                                                                                                                                       |
| ---------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id`           | string | Yes      | Id (24-hex) of the project whose test case workspace notes you want to count. Source: `find_projects`. The caller must have access to this project's test cases.                                                                  |
| `project_testcase_ids` | array  | No       | Restrict the count to these project test cases (array of 24-hex ids). Source: `find_project_testcases` (its `id` field). Omit to count across every test case in the project. Ids that do not belong to this project are ignored. |
| `filter`               | object | No       | A MongoDB-style filter, applied on top of `project_id` and `project_testcase_ids` - a workspace note must satisfy all of them to be counted. Supported fields are listed below.                                                   |

**Supported `filter` fields:**

| Field                 | Type / Values          |
| --------------------- | ---------------------- |
| `id`                  | `ObjectId('<24 hex>')` |
| `project_testcase_id` | `ObjectId('<24 hex>')` |
| `created`, `modified` | ISO-8601 datetime      |
| `title`               | string                 |
| `note`                | string                 |
| `user_id`             | `ObjectId('<24 hex>')` |

> Workspace notes are stored as AttackForge richtext (HTML), so a `$regex` on `note` matches the markup as stored, not the rendered text. Keep patterns to plain words to avoid matching tags. `title` is plain text and has no such caveat.

#### Example Response

```json
{
  "count": 9
}
```


# Count Remediation Notes

#### Description <a href="#description" id="description"></a>

Returns the number of remediation notes attached to one or more AttackForge vulnerabilities. A remediation note is a timestamped, authored comment recording remediation progress, guidance, or discussion for a single vulnerability. Use this when you only need a total - call `find_remediation_notes` to retrieve the records.

Supply the vulnerabilities to inspect via `vulnerability_ids`. Results are automatically scoped to notes on vulnerabilities the caller can see: released vulnerabilities require View access to a linked project, while pending (unreleased) vulnerabilities require Edit access. Notes on vulnerabilities the caller cannot see are silently omitted.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_remediation_notes` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many remediation notes are on vulnerability X?*
* *How many remediation notes have been added to these findings this year?*
* *How many remediation notes did I write on this vulnerability?*
* *How many remediation notes mention a patch?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter           | Type   | Required | Description                                                                                                                                                                                                                                                                                   |
| ------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vulnerability_ids` | array  | Yes      | The vulnerabilities whose remediation notes you want to count (array of 24-hex ids). Source: `find_vulnerabilities`. Ids the caller cannot see under the `visibility` rule are silently dropped.                                                                                              |
| `filter`            | object | No       | A MongoDB-style filter. Supported fields are listed below.                                                                                                                                                                                                                                    |
| `visibility`        | string | No       | `'visible'` (default, notes on released vulnerabilities in projects the caller can View), `'pending'` (notes on unreleased vulnerabilities, restricted to projects the caller can Edit), or `'all'`. Notes on pending vulnerabilities in projects the caller can only View are never counted. |

**Supported `filter` fields:**

| Field                 | Type / Values                                           |
| --------------------- | ------------------------------------------------------- |
| `id`                  | `ObjectId('<24 hex>')`                                  |
| `created`, `modified` | ISO-8601 datetime                                       |
| `note`                | string (the note content)                               |
| `author_id`           | `ObjectId('<24 hex>')` - the user who authored the note |
| `vulnerability_id`    | `ObjectId('<24 hex>')` - the parent vulnerability       |

> Do NOT restrict which vulnerabilities are inspected via `filter` - use the `vulnerability_ids` parameter.

> Notes are stored as AttackForge richtext (HTML), so a `$regex` on `note` matches the markup as stored, not the rendered text. Keep patterns to plain words to avoid matching tags. Older notes with no richtext content are not matched.

#### Example Response

```json
{
  "count": 4
}
```


# Count Test Cases

#### Description <a href="#description" id="description"></a>

Returns the number of AttackForge **library** test cases matching the optional `filter`. A library test case is a reusable, taggable unit of testing methodology that belongs to a testsuite - it is NOT a per-project test case (see `count_project_testcases` for those). Use this when you only need a total - call `find_testcases` to retrieve the records.

Scope the count to specific testsuites via `testsuite_ids`. See Test Cases and Methodology.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_testcases` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many test cases are in my methodology library?*
* *How many test cases are in the web application testsuite?*
* *How many test cases are tagged OWASP Top 10?*
* *How many test cases mention injection?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter       | Type   | Required | Description                                                                                                                                                                  |
| --------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `testsuite_ids` | array  | No       | Restrict the count to test cases belonging to these testsuites (array of 24-hex ids). Source: `find_testsuites`. Omit to count across every testsuite the caller can access. |
| `filter`        | object | No       | A MongoDB-style filter. Supported fields are listed below.                                                                                                                   |

**Supported `filter` fields:**

| Field                  | Type / Values                                      |
| ---------------------- | -------------------------------------------------- |
| `id`                   | `ObjectId('<24 hex>')`                             |
| `created`, `modified`  | ISO-8601 datetime                                  |
| `title`                | string                                             |
| `code`                 | string                                             |
| `tags`                 | array of string                                    |
| `sort_order`           | integer                                            |
| `custom_fields`        | array of `{ key: string, value: string or array }` |
| `library_id`           | string                                             |
| `template_testcase_id` | `ObjectId('<24 hex>')`                             |
| `user_id`              | `ObjectId('<24 hex>')`                             |

> Do NOT filter on testsuite membership via `filter` - `testsuite_id` is disallowed there. Use the `testsuite_ids` parameter.

#### Example Response

```json
{
  "count": 412
}
```


# Count Test Suites

#### Description <a href="#description" id="description"></a>

Returns the number of AttackForge testsuites matching the optional `filter`. A testsuite is a reusable methodology - a named, taggable collection of test cases that can be added to a project's testing plan or proposed on a project request. Use this when you only need a total - call `find_testsuites` to retrieve the records.

See Test Cases and Methodology for how testsuites, test cases and project test cases relate.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_testsuites` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *How many testsuites do I have in AttackForge?*
* *How many testsuites are available on project requests?*
* *How many testsuites are tagged OWASP Top 10?*
* *How many testsuites were created this year?*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                                |
| --------- | ------ | -------- | ---------------------------------------------------------- |
| `filter`  | object | No       | A MongoDB-style filter. Supported fields are listed below. |

**Supported `filter` fields:**

| Field                            | Type / Values                                      |
| -------------------------------- | -------------------------------------------------- |
| `id`                             | `ObjectId('<24 hex>')`                             |
| `created`, `modified`            | ISO-8601 datetime                                  |
| `name`                           | string                                             |
| `code`                           | string                                             |
| `is_visible_on_project_requests` | boolean - supports `$eq`, `$ne` and `$exists` only |
| `tags`                           | array of string                                    |
| `user_id`                        | `ObjectId('<24 hex>')`                             |

> A testsuite can be used on project requests unless `is_visible_on_project_requests` is explicitly set to `false` - testsuites where it has never been set are usable. Matching on `true` covers both, so `{ is_visible_on_project_requests: { $eq: true } }` needs no special handling.

#### Example Response

```json
{
  "count": 24
}
```


# Count Vulnerabilities

#### Description <a href="#description" id="description"></a>

This tool can be used to count Vulnerabilities using a provided filter expression.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_vulnerabilities` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

```
How many vulnerabilities do I have in AttackForge?
```

#### Supported Query Fields <a href="#supported-query-fields" id="supported-query-fields"></a>

{% code overflow="wrap" %}

```javascript
id: {
  type: 'string',
  pattern: "ObjectId\\(\\'[0-9a-fA-F]{24}\\'\\)",
  description: "This is the vulnerability id.",
}
created: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The timestamp that this vulnerability was created.'
},
modified: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The timestamp that this vulnerability was last modified.'
}
title: {
  type: 'string',
}
priority: {
  enum: [
    'Critical',
    'High',
    'Medium',
    'Low',
    'Info'
  ]
}
alternate_id: {
  type: 'string',
}
cvssv3_1_score: {
  description: 'CVSSv3.1 score',
  type: 'number'
}
cvssv3_1_vector: {
  description: 'CVSSv3.1 vector string',
  type: 'string'
}
status: {
  enum: ['Closed', 'Open']
}
status_updated: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The status was last updated at this timestamp.'
}
target_remediation_date: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The latest timestamp at which the vulnerability is planned to be remediated.'
}
likelihood_of_exploitation: {
  type: 'integer',
  description: 'Scale of exploitability - 1 is least, 10 is most.'
}
steps_to_reproduce_html: {
  type: 'string',
}
release_date: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The timestamp when the vulnerability was marked as released.'
}
sla: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The timestamp when the vulnerability is expected to be remediated.'
}
tags: {
  type: 'array',
  items: { type: 'string' }
}
custom_tags: {
  type: 'array',
  items: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      value: { type: 'string' }
    },
    required: ['name', 'value'],
    additionalProperties: false
  }
}
custom_fields: {
  type: 'array',
  items: {
    type: 'object',
    properties: {
      key: {
        type: 'string',
        pattern: '^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$',
      },
      value: {
        oneOf: [
          {
            type: 'string',
          },
          {
            type: 'array',
          },
        ]
      }
    },
    required: ['key', 'value'],
    additionalProperties: false
}
is_retest: {
  enum: ['Yes', 'No'],
  description: 'Indicates whether this vulnerability has been marked for retest.'
}
is_zero_day: {
  enum: ['Yes', 'No'],
  description: 'Indicates whether this vulnerability has been categorised as zero day.'
}
writeup_id: {
  type: 'string',
  pattern: "ObjectId\\(\\'[0-9a-fA-F]{24}\\'\\)",
  description: "This is the Writeup id. Example query: { writeup_id: { $eq: ObjectId('65a440c08cade68ca7bc7192') } }"
}
```

{% endcode %}

#### Example Response <a href="#example-response" id="example-response"></a>

```json
{
	"count": 3232
}
```


# Count Writeups

#### Description <a href="#description" id="description"></a>

This tool can be used to count Writeups using a provided filter expression.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `count_writeups` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

```
How many writeups do I have in AttackForge?
```

```
How many writeups are in my main library?
```

#### Supported Query Fields <a href="#supported-query-fields" id="supported-query-fields"></a>

{% code overflow="wrap" %}

```javascript
id: {
  type: 'string',
  pattern: "ObjectId\\(\\'[0-9a-fA-F]{24}\\'\\)",
}
created: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The timestamp that this vulnerability was created.'
}
modified: {
  type: 'string',
  pattern: '^\\d{4,}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$',
  description: 'The timestamp that this vulnerability was last modified.'
}
attack_scenario: {
  type: 'string',
}
description: {
  type: 'string',
}
impact_on_availability: {
  enum: [
    'High',
    'Medium',
    'Low',
    'None',
  ]
}
impact_on_confidentiality: {
  enum: [
    'High',
    'Medium',
    'Low',
    'None',
  ]
}
impact_on_integrity: {
  enum: [
    'High',
    'Medium',
    'Low',
    'None',
  ]
}
import_source_id: {
  type: 'string'
}
import_source: {
  type: 'string'
}
likelihood_of_exploitation: {
  description: 'Scale of exploitability, 1 is least and 10 is most',
  enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
remediation_recommendation: {
  type: 'string'
}
severity: {
  description: '1 is least severe and 10 is most severe',
  enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
title: {
  type: 'string',
}
tags: {
  type: 'array',
  items: { type: 'string' }
}
custom_tags: {
  type: 'array',
  items: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      value: { type: 'string' }
    },
    required: ['name', 'value'],
    additionalProperties: false
  }
}
custom_fields: {
  type: 'array',
  items: {
    type: 'object',
    properties: {
      key: {
        type: 'string',
        pattern: '^[a-zA-Z]([a-zA-Z0-9_]*[a-zA-Z0-9])?$',
      },
      value: {
        oneOf: [
          {
            type: 'string',
          },
          {
            type: 'array',
          },
        ]
      }
    },
    required: ['key', 'value'],
    additionalProperties: false
}
```

{% endcode %}

#### Example Response <a href="#example-response" id="example-response"></a>

```json
{
	"count": 891
}
```


# Create Abuse Case

#### Description <a href="#description" id="description"></a>

Creates a new AttackForge abuse case in an existing project. An abuse case is a test case belonging to **one project only** - it lives in that project's own abuse-case testsuite rather than in the reusable methodology library, so it is created against a project rather than a testsuite. To create a reusable library test case instead, use `create_testcase`.

This is a **write** operation: it persists a new abuse case against the project, creating the project's abuse-case testsuite if it does not have one yet, and allocates it to the project so it appears in `find_project_testcases`.

The shape of `args` is declared inline (see Building args for Write Tools), so no preparatory call is needed. Rich-text fields must use the AttackForge richtext format described in the `richtext_format` block returned by `get_args_schema_for_tool`.

Preconditions: the caller must have Edit access to the project and access to its test cases. On success **both** ids are returned: `testcase_id` for the abuse case itself (patch it with `patch_testcase`) and `project_testcase_id` for its per-project instance (patch that with `patch_project_testcase`).

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `create_abuse_case` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *Add an abuse case to project X for a business logic bypass on the checkout flow.*
* *Create a project-specific test case for abusing the password reset flow.*
* *Add an abuse case to the ACME project with these execution steps.*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                           |
| --------- | ------ | -------- | ----------------------------------------------------- |
| `args`    | object | Yes      | The abuse-case-creation payload - see the keys below. |

**`args` keys:**

| Key              | Type    | Required | Description                                                                                                                                                                                                                                                                                     |
| ---------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id`     | string  | Yes      | Id (24-hex) of the project to create the abuse case in. Source: `find_projects`. The abuse case is placed in that project's abuse-case testsuite, which is created on first use.                                                                                                                |
| `title`          | string  | Yes      | Title of the abuse case.                                                                                                                                                                                                                                                                        |
| `details`        | string  | No       | Details / description. Rich-text - use the AttackForge richtext format.                                                                                                                                                                                                                         |
| `code`           | string  | No       | Arbitrary code associated with this abuse case.                                                                                                                                                                                                                                                 |
| `tags`           | array   | No       | String tags attached to this abuse case.                                                                                                                                                                                                                                                        |
| `sort_order`     | integer | No       | Default sort order within the AttackForge UI. Omit to append at the end of the project's abuse-case testsuite.                                                                                                                                                                                  |
| `execution_flow` | array   | No       | Ordered execution-flow steps, each `{ title, details }`. `details` is rich-text.                                                                                                                                                                                                                |
| `custom_fields`  | array   | No       | Custom field values set on the **project test case** created for this abuse case - discover keys via `get_field_structure(model="project-testcase")`. These are distinct from the custom fields on the abuse case itself, which are set with `patch_testcase` using the returned `testcase_id`. |

#### Example Response

```json
{
  "testcase_id": "6987c31b8e7d46a8089d9b53",
  "project_testcase_id": "6987c31b8e7d46a8089d9b54"
}
```


# Create Project

#### Description <a href="#description" id="description"></a>

Creates a new AttackForge project. This is a **write** operation: it allocates one project credit against the tenant licence and persists a new project record.

A two-step workflow is **required** because the input shape is tenant-specific - see Two-Step Workflow for Write Tools:

1. Call `get_args_schema_for_tool(tool="create_project")` to retrieve the current JSON Schema for `args`.
2. Build `args` to match that schema and call this tool.

Common top-level groups are `details`, `access`, and `notifications`, but the authoritative list comes from the schema. Preconditions: the caller must have permission to create projects and the tenant must be within its project-creation licence limit. On success the new project id is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `create_project` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *Create a new project called "Q3 External Pentest" starting next Monday for two weeks.*
* *Set up a project for the ACME web app engagement with reporting and retesting enabled.*
* *Create a project and add my team to it.*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                                                                          |
| --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `args`    | object | Yes      | The project-creation payload. Build it to satisfy `get_args_schema_for_tool(tool="create_project")`. |

#### Example Response

```json
{
  "id": "685500711d6a44e61f90db4e"
}
```


# Create Project Request

#### Description <a href="#description" id="description"></a>

Creates a new AttackForge project request. This is a **write** operation: it persists a new project request record.

A two-step workflow is **required** because the input shape is tenant-specific - see Two-Step Workflow for Write Tools:

1. Call `get_args_schema_for_tool(tool="create_project_request")` to retrieve the current JSON Schema for `args`.
2. Build `args` to match that schema and call this tool.

Preconditions: the caller must have permission to create project requests, and project requests must be enabled for the tenant (`frontend.enable_project_requests`). On success the new project request id is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `create_project_request` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *Submit a project request for an external pentest of the ACME web app in February.*
* *Raise a project request requiring onsite testing for the data centre.*
* *Create a project request for the ACME org with this scope and justification.*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                                                                                          |
| --------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `args`    | object | Yes      | The project-request-creation payload. Build it to satisfy `get_args_schema_for_tool(tool="create_project_request")`. |

#### Example Response

```json
{
  "id": "6650a4c08cade68ca7bc7192"
}
```


# Create Project Test Case Note

#### Description <a href="#description" id="description"></a>

Adds a note to a test case in an AttackForge project. A note records **progress** working the test case - the running log of what the tester did and found. For reference material useful in performing the test case (background, tips, payloads) use `create_project_testcase_workspace_note` instead.

This is a **write** operation: it persists a new note against the project test case. The shape of `args` is declared inline (see Building args for Write Tools), so no preparatory call is needed. `note` is a rich-text field and must use the AttackForge richtext format described in the `richtext_format` block returned by `get_args_schema_for_tool`.

Files cannot be attached through this tool. Preconditions: the caller must have Edit access to the project and access to its test cases, and the test case must belong to the project. On success the new note id is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `create_project_testcase_note` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *Add a note to this test case saying no injection was found.*
* *Record on the TLS test case that the server still supports TLS 1.0.*
* *Log my testing progress on this test case.*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                     |
| --------- | ------ | -------- | ----------------------------------------------- |
| `args`    | object | Yes      | The note-creation payload - see the keys below. |

**`args` keys:**

| Key                   | Type   | Required | Description                                                                                                                                                                      |
| --------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_id`          | string | Yes      | Id (24-hex) of the project the test case is allocated to. Source: `find_projects`.                                                                                               |
| `project_testcase_id` | string | Yes      | Id (24-hex) of the project test case the note is being added to - the per-project instance, NOT the parent library test case. Source: `find_project_testcases` (its `id` field). |
| `note`                | string | Yes      | The note content - a record of progress working the test case. Rich-text - use the AttackForge richtext format.                                                                  |

#### Example Response

```json
{
  "id": "6650a4c08cade68ca7bc7192"
}
```


# Create Project Test Case Workspace Note

#### Description <a href="#description" id="description"></a>

Adds a workspace note to a test case in an AttackForge project. A workspace note is **reference material** useful for performing the test case - background, tips, payloads, or anything else worth having to hand while testing it. To record testing progress instead, use `create_project_testcase_note`.

This is a **write** operation: it persists a new workspace note against the project test case. The shape of `args` is declared inline (see Building args for Write Tools), so no preparatory call is needed. `note` is a rich-text field and must use the AttackForge richtext format described in the `richtext_format` block returned by `get_args_schema_for_tool`.

Files cannot be attached through this tool. Preconditions: the caller must have Edit access to the project and access to its test cases, and the test case must belong to the project. On success the new workspace note id is returned.

#### How To Enable <a href="#how-to-enable" id="how-to-enable"></a>

1. Go to `Users`
2. Select the user you would like to provide access to this tool
3. Click on `Access > MCP`
4. Click on `Add Tools`
5. Select the tool `create_project_testcase_workspace_note` and click `Add`

#### Example Prompts <a href="#example-prompts" id="example-prompts"></a>

* *Add a workspace note to this test case with the payload list I am using.*
* *Save these setup instructions as reference material on the test case.*
* *Add background notes on the authentication flow to this test case.*

#### Parameters <a href="#parameters" id="parameters"></a>

| Parameter | Type   | Required | Description                                               |
| --------- | ------ | -------- | --------------------------------------------------------- |
| `args`    | object | Yes      | The workspace-note-creation payload - see the keys below. |

**`args` keys:**

| Key                   | Type   | Required | Description                                                                                                                                                                                |
| --------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `project_id`          | string | Yes      | Id (24-hex) of the project the test case is allocated to. Source: `find_projects`.                                                                                                         |
| `project_testcase_id` | string | Yes      | Id (24-hex) of the project test case the workspace note is being added to - the per-project instance, NOT the parent library test case. Source: `find_project_testcases` (its `id` field). |
| `title`               | string | Yes      | The workspace note title. Plain text.                                                                                                                                                      |
| `note`                | string | Yes      | The workspace note content - reference material for performing the test case. Rich-text - use the AttackForge richtext format.                                                             |

#### Example Response

```json
{
  "id": "6650a4c08cade68ca7bc7192"
}
```




---

[Next Page](/llms-full.txt/1)

