Skip to content
Stribog

Security

All writing

Vulnerability Assessment and Penetration Testing: The Report

Vulnerability assessment and penetration testing end in a report. Test its scope against live cluster state, recompute severity locally, drive the retest.

Stribog13 min read

A PDF lands in a shared drive named VAPT_Report_FY26_Final_v3.pdf and reaches the platform team with the word "actions?" attached. Almost everything written for this search stops a step earlier: choosing a firm, telling the exercises apart. This is the step after, where the report becomes an infrastructure input.

Two Exercises, Two Kinds of Evidence

Start with what each half is entitled to claim; the report mixes them and the severity table will not. NIST SP 800-115 defines the harder one: penetration testing is "security testing in which assessors mimic real-world attacks to identify methods for circumventing the security features of an application, system, or network." The assessor reaches the thing. Scanning it treats differently: network-based scanning "generally covers surface vulnerabilities, and is unable to address the overall risk level of a scanned network", and because scanners "can have a high false positive error rate", an expert "should interpret the results".

PCI SSC's Penetration Testing Guidance draws the line from the other side. Its comparison table's *How* row calls a scan "typically a variety of automated tools combined with manual verification of identified issues", and a penetration test "a manual process that may include the use of vulnerability scanning or other automated tools". One is automation with a human checking output; the other, a human wielding automation.

The evidence classes fall out of that: an assessment finding is a signature match plus a human's judgement that it is real, a test finding is a demonstrated path. Whether your regime hands you one report or two needs answering first.

The Report Has a Table of Contents Someone Else Wrote

Under PCI SSC's guidance the pentest report has a suggested outline whose order is informative. "Executive Summary" opens it; "Statement of Scope" comes second — "a detailed definition of the scope of the network and systems tested as part of the engagement." Methodology follows, then the section nobody reads. "Statement of Limitations" documents "any restrictions imposed on testing such as designated testing hours, bandwidth restrictions" — what the engagement did not cover, written by the only people who know.

That guidance also publishes a report evaluation tool aimed at the reader, not the writer — "intended for entities that receive a penetration test report and need to interpret and evaluate the completeness of the report." Its checklist asks one question worth putting first: "is there sufficient evidence that the individuals are organizationally independent from the management of the environment being tested?"

SEBI's Cybersecurity and Cyber Resilience Framework (CSCRF, Version 1.0, August 2024) specifies one combined VAPT report. Its Annexure-A table of contents runs Auditor's Declaration, Executive Summary, Scope of Audit, Tools used, "Exclusions, if any", then a Summary separating "Details of Vulnerability Assessment findings" from "Details of Penetration Testing findings" into their own numbered subsections, then Detailed Report and Risk Rating Description. The table of contents numbers those 6.1 and 6.2; specimen tables later in the annexure number them 6.3 and 6.4. Check for the split, not the number.

CERT-In's Comprehensive Cyber Security Audit Policy Guidelines (Version 1.0, dated 25.07.2025) — which SEBI regulated entities are directed to follow by circular SEBI/HO/ITD-1/ITD_CSC_EXT/P/CIR/2025/119 of 28 August 2025 — move the format question before the engagement. Among the items that "must be communicated clearly by the auditing organization to the auditee organization before the commencement of the Audit" sit the "Format of the Reports", the "Assets covered in the scope", and "Handling & retention of auditee data".

The two regimes specify different artefacts. Merging them produces a checklist wrong under both — hunting for Annexure-A subsection numbers in a PCI penetration test report, or applying PCI's scan cadence to a SEBI VAPT.

Does the Scope Statement Still Describe What You Expose?

Every format above obliges the tester to define scope. None obliges that definition to still be true on the day you read it. Only CERT-In's guidelines attach a vintage, and the modal chain matters. The auditing organisation "may advise auditee organization to explicitly mention the date up to which the scope / asset inventory has been updated and this date must be reflected in the audit reports." A *may advise* triggers a *must* on the report, not a standing duty on every auditee. Where taken, the report carries an as-of date — and that is testable.

So test it, narrowly. Inventory, artefact hashes and the evidence appendix belong to the engagement itself. The returned report raises a smaller question: what does the cluster declare it exposes today, and is all of it named in the scope statement?

bash
#!/usr/bin/env bash
# Does the report's scope statement still describe what this cluster
# declares it exposes? Exits 3 on exposure the report never named.
set -euo pipefail

USAGE='usage: scope-check.sh <kube-ctx> <report-scope.txt> [declared-out]'
CONTEXT="${1:?$USAGE}"; SCOPE_FILE="${2:?$USAGE}"
DECLARED_OUT="${3:-}" # exposure list for rerank.sh
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
kc() { kubectl --context "$CONTEXT" "$@"; }

# Ingress rule and TLS hosts.
kc get ingress -A -o json | jq -r '
  .items[]
  | ((.spec.rules // []) | map(.host // empty))
    + ((.spec.tls // []) | map(.hosts // []) | add // [])
  | unique[] | "ingress-host " + .
' >> "$WORK/declared"

# LoadBalancer addresses and every allocated node port.
kc get svc -A -o json | jq -r '
  .items[]
  | select(.spec.type == "LoadBalancer" or .spec.type == "NodePort")
  | .metadata.namespace as $ns | .metadata.name as $n
  | (((.status.loadBalancer.ingress // [])
      | map("lb-address " + (.ip // .hostname // empty)))
     + ((.spec.ports // [])
        | map(select(.nodePort != null))
        | map("nodeport " + $ns + "/" + $n + ":" + (.nodePort|tostring))))[]
' >> "$WORK/declared"

# Node addresses the listen set is drawn from.
kc get nodes -o json | jq -r '
  .items[] | (.status.addresses // [])[]
  | select(.type == "ExternalIP" or .type == "InternalIP")
  | "node-" + (.type | ascii_downcase) + " " + .address
' >> "$WORK/declared"

sort -u "$WORK/declared" -o "$WORK/declared"
if [[ -n "$DECLARED_OUT" ]]; then
  cp "$WORK/declared" "$DECLARED_OUT" # outlives the trap
fi
grep -Ev '^[[:space:]]*(#|$)' "$SCOPE_FILE" | sort -u > "$WORK/named" || true

printf 'as-of %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
comm -13 "$WORK/declared" "$WORK/named" | sed 's/^/  gone: /'
comm -23 "$WORK/declared" "$WORK/named" > "$WORK/unnamed"

if [[ -s "$WORK/unnamed" ]]; then
  sed 's/^/  UNTESTED (not passed): /' "$WORK/unnamed"
  exit 3
fi
scope-check.sh — Stribog's proposed reachability check, run against the report on your desk. The scope file is the report's asset list transcribed one entry per line in the same `kind identity` form the script emits, so the diff is mechanical rather than a reading exercise.

Then read what it does not tell you. A Service of type NodePort means "every node in the cluster configures itself to listen on that assigned port", from a range "specified by --service-node-port-range flag (default: 30000-32767)". Whether those addresses are reachable from outside is a network-path fact the Service object does not settle, and kube-proxy's --nodeport-addresses narrows the listen set. Host-network pods, externalIPs set outside Service objects, Gateway API HTTPRoute hostnames, and anything fronted by infrastructure the cluster does not describe never appear.

So the output is a hypothesis to narrow with routing and firewall facts you hold, and the value is in the diff. An entry the scope statement names and the cluster no longer has closes as not applicable. An entry the cluster declares and the scope statement never names is not a passed test — it is surface nobody looked at.

Reading the Severity Column You Were Sent

The severity column arrives with a model attached, and which model depends on where the report came from. Under PCI's split, ranking lives on the scan side: the *Reports* row says a scan reports "potential risks posed by known vulnerabilities, ranked in accordance with NVD/CVSS base scores" — while noting internal scans may instead be ranked by the entity's own risk-ranking process, so do not assume the NVD scale. The penetration test half reads differently: each vulnerability "verified", with "specific methods how and to what extent it may be exploited".

A CERT-In-governed report arrives with more attached. The 2025 guidelines require auditors "to implement both CVSS and Exploit Prediction Scoring System (EPSS) frameworks within their audit reports", categorising observations on CVSS "for severity" and supplementing them with EPSS "to assess the likelihood of real-world exploitation." That is an obligation on the auditor, not a gap for the operator to fill — and EPSS measures a different quantity: the probability "that a published CVE will be exploited in the wild in the next 30 days".

Either way the environmental metrics are the operator's: nobody outside the estate can supply them. Under v3.1 — still the version most reports arrive in — it moves both ways. A score falls once Modified Attack Vector stops being Network for a service reachable only from an operator VPN segment. It rises on the ingress path fronting every regulated workload once Confidentiality and Integrity Requirements are stated High rather than left at that Medium-equivalent default, since "the Modified Confidentiality impact (MC) metric has increased weight if the Confidentiality Requirement (CR) is High" — except where the modified impact metrics are already all High, pinning the sub-score at its ceiling. Under v4.0 only the fall is available: High is already the default.

Which makes one discipline matter more than the arithmetic: record the version alongside the vector. The strings do not interconvert, so a register that mixes versions compares numbers never on one scale. Recompute in whichever version arrived.

yaml
# findings/2026-VAPT-014.yaml
id: 2026-VAPT-014
origin: penetration-test
report_section: "6.2 Details of Penetration Testing findings"

asset:
  declared_as: "ingress-host api.internal.example.com"
  named_in_scope_statement: true
  scope_statement_as_of: "2026-07-31"

severity:
  as_reported:
    cvss_version: "3.1"
    vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
    base_score: 9.1
  recomputed:
    cvss_version: "3.1"
    vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N/MAV:A/CR:H/IR:H/AR:L"
    environmental_score: 8.8
    rationale: >-
      Operator VPN segment only, so Modified Attack Vector
      Adjacent; CR and IR stated High rather than v3.1's
      Medium-equivalent default; AR Low. Exploitability falls,
      impact rises: 9.1 base, 8.8 environmental.
  epss: { probability: 0.042, fetched: "2026-09-11" }

evidence:
  artefact: "evidence/2026-VAPT-014-session.tar.zst"
  sha256: "3b1f0c7e9a5d2b8461f7c03e5a9d17b4c26e8f0a3d5b7c91e04a6f28d3b5c7e1"
  auditor_destruction_due: "2026-10-31" # agreed pre-engagement

remediation:
  owner: platform-team
  clock: vapt-observation-closure
  clock_basis: >-
    CSCRF v1.0 Table 19 — 3 months from report submission, graded.
    Not a patch finding, so the 11 June 2025 FAQ does not route
    it onto PR.MA.S3.
  due: "2026-11-15"

retest:
  requested: "2026-11-18"
  outcome: null # closed | open | superseded
One row of the findings register — Stribog's proposed schema, not a standard. One file per finding, in the platform repository, reviewed like any other change. The recomputed figure is the v3.1 environmental score for the vector beside it — check it in FIRST's calculator.

Telling a Scanner Finding From a Tester Finding

Before a severity enters that register, ask what evidence the finding owes. An assessment finding owes raw tool output and an interpretation step, and NIST SP 800-115 says why that step cannot be automated: assessors may validate "by using a second automated tool and comparing the results", but "these comparison tools can often produce similar results—including the same false positives". Two scanners agreeing is often the same signature database consulted twice.

A test finding owes a path. PCI's guidance defines the evidence class broadly — "screenshots, raw tool output (i.e., NMAP, burp suite, Nessus, TCPDump Wireshark, etc.), acquired dumps in case of exploitation (i.e., database files, logs, configuration files etc.)". The report narrative summarises that material; it does not replace it. A finding called exploited with nothing attached showing it is an assessment finding wearing a stronger label. A penetration-testing subsection that reads like a second scanner export is a finding about the engagement, not the estate.

Solely tools-based testing should be discouraged as tool-based audits may focus primarily on automated processes and may overlook non-automated or manual components of the IT infrastructure.
CERT-In, Comprehensive Cyber Security Audit Policy Guidelines, Version 1.0 (25.07.2025)

The Deliverable Is Not Done Until the Retest Is

Remediation runs on a clock, and under CSCRF on more than one. CSCRF v1.0 (August 2024) sets the default in Table 19: closure of findings identified during VAPT activity "within 3 months of submission of VAPT report", on "a graded approach (based on the criticality of observations)". Section 4.3.4 adds a second: "any open vulnerabilities after 3 months of VAPT activity shall be approved by IT Committee for REs and shall be closed before start of next VAPT exercise". They do not start together: one counts from report submission, the other from the activity. Collapse them and the wrong deadline reaches the committee that signs for it.

A third timeline governs a subset. SEBI's CSCRF FAQs of 11 June 2025 clarify that vulnerabilities "identified due to non-implementation of patches and falling under 'high' severity would be validated for non-compliances against the patch management timelines (1 week)", pointing at standard PR.MA.S3, whose table sets ceilings at both the primary data centre and the DR site: one week high, two weeks moderate, one month low. Other observations "shall be validated for non-closure against the VAPT observation closure timelines (3 months)". That mapping is the FAQ's, read with PR.MA.S3: it clarifies how existing timelines apply rather than ranking alongside Table 19. Separately, revalidation "shall be completed within 5 months of completion of VAPT."

None of which is India-specific. PCI's guidance states the general case: "remediation efforts extending for a long period after the initial test may require a new testing engagement to be performed to ensure accurate results of the most current environment are reported", decided "after a risk analysis of how much change has occurred". A retest against a topology that moved underneath it is a smaller, undeclared engagement on the old report's letterhead.

Which is where the register pays for itself. The join keeps findings whose declared exposure the cluster still emits and sorts them by recomputed severity — a narrow filter, not a verdict on whether an asset exists.

bash
#!/usr/bin/env bash
# Drop findings whose asset is gone, flag assets the scope statement
# never named, sort by recomputed score.
set -euo pipefail


REGISTER_DIR="${1:?usage: rerank.sh <findings-dir> <declared-exposure.txt>}"
DECLARED="${2:?usage: rerank.sh <findings-dir> <declared-exposure.txt>}"

yq -o=json -I0 '.' "$REGISTER_DIR"/*.yaml \
  | jq -rs --rawfile declared "$DECLARED" '
      ($declared | split("\n") | map(select(length > 0))) as $live
      | map(.asset.declared_as as $a
            | . + { live: ($live | index($a)) != null,
                    untested: (.asset.named_in_scope_statement != true) })
      | map(select(.live))
      | sort_by(-(.severity.recomputed.environmental_score
                  // .severity.as_reported.base_score))
      | .[]
      | [ (if .untested then "UNTESTED" else .remediation.clock end),
          .id, .remediation.due, .asset.declared_as,
          (.severity.recomputed.environmental_score | tostring),
          (.severity.as_reported.base_score | tostring) ]
      | @tsv
    ' \
  | column -t -s "$(printf '\t')"
rerank.sh — joins the findings register to the exposure list `scope-check.sh ctx scope.txt declared.txt` wrote, so the remediation queue is ordered by your environment rather than the report's defaults. It ranks only findings whose `asset.declared_as` matches a line the script emits, so a ClusterIP-only database or internal app is dropped and must be carried separately.
The report is accurate as of a date. The cluster is not. Everything here is an attempt to keep those two facts from being confused with each other.

Evidence: What You Keep, What They Keep, What Leaves

PCI's guidance puts the evidence arrangement before the testing: "procedures for retention and destruction of evidence" should "be documented for all parties involved prior to commencing the penetration test", and where a third party tests, "contract language should be reviewed to confirm these procedures are clear." Given an evidence class covering acquired dumps of database files, logs and configuration files, a firm closing an engagement holds some of your most sensitive material with no default expiry.

CERT-In's guidelines make it a pre-engagement duty: "Handling & retention of auditee data" sits in that same pre-commencement list, alongside the "Requirement to share audit metadata & reports with CERT-In". The report leaves the building by design.

What reaches the regulator is narrower than what you hold. SEBI's August 2025 circular says regulated entities "shall submit the summary of VAPT and cyber audit reports strictly as per the format mentioned in CSCRF", and that "at no point of time, REs shall submit the explicit vulnerabilities unless and otherwise asked for the details by SEBI." For firms inside India's BFSI data boundary the shape is familiar: the evidence you retain, the summary the regulator receives, the material the testing firm destroys on an agreed date. Put that date in the register row, not an email thread.

Exit Ramp: Own the Findings Register

Testing firms change — by procurement, by rotation, or because the regime narrows the pool. CSCRF is explicit that "unless otherwise specified, all audits mentioned in CSCRF have to be conducted by CERT-In empanelled IS auditing organization": the roster is the constraint, not the relationship. When the firm changes, everything held only in their portal leaves with it — finding history, evidence links, retest outcomes, the reasoning behind accepting a finding.

A register in your own repository inverts that dependency. Each finding is a file, reviewed like any other change, carrying the incoming vector and its version, the recomputed one, the evidence digest, the governing clock and who approved what. It is not a second inventory: it anchors findings to assets and keeps the diff across engagements, which no single report contains. The next firm inherits that history in a format they did not define and cannot withhold. The same rows answer a third-party risk register question under DORA or a procurement question about secure-by-design practice without an export from someone else's tooling.

The Long Game: An Annual Test Against a Continuously Measured Surface

NIST SP 800-115 observed in September 2008 that "because of its high cost and potential impact, penetration testing of an organization's network and systems on an annual basis may be sufficient", recommending "regularly scheduled network and vulnerability scanning, interspersed with periodic penetration testing". That cost of skilled human testing still shapes what most regimes assume. What changed is the cost of the other half: a cluster emitting its declared exposure on every change makes the annual engagement a sample of a surface you already measure.

That reframes the report as a dated, adversarial reading of infrastructure that describes itself the rest of the year — the posture that makes supply-chain provenance and resilience evidence continuous rather than episodic.

Regimes keep arriving. CSCRF is at Version 1.0, CERT-In's audit guidelines Version 1.0 of a 2025 document, PCI's report structure a 2017 information supplement. Clause numbers churn and checklists built on them age badly. The question underneath does not move: on the day it was written, did the report describe the system you run — and can you prove what changed since?

§FAQ/Common questions

Frequently asked

What is the difference between a vulnerability assessment and a penetration test?

They differ in method and in what the result is entitled to claim. NIST SP 800-115 defines penetration testing as security testing in which assessors mimic real-world attacks to identify methods for circumventing an application's, system's or network's security features, while describing network-based vulnerability scanning as covering surface vulnerabilities only, unable to address a network's overall risk level, and carrying a high false-positive rate that requires expert interpretation. PCI SSC's Penetration Testing Guidance puts the same line in its comparison table: a scan is typically automated tools combined with manual verification, a penetration test a manual process that may use automated tools. An assessment finding is a signature match plus human judgement; a test finding is a demonstrated path.

What should a VAPT report contain?

That depends on the regime, and the two most operators meet are not interchangeable. PCI SSC's guidance suggests a report outline opening with an Executive Summary, then a Statement of Scope defining the network and systems tested, then Methodology and a Statement of Limitations recording restrictions such as designated testing hours or bandwidth limits. SEBI's CSCRF v1.0 Annexure-A instead mandates a table of contents for a single combined VAPT report — Auditor's Declaration, Executive Summary, Scope of Audit, Tools used, Exclusions, a Summary of the VAPT Report splitting vulnerability assessment findings from penetration testing findings into separate numbered subsections, a Detailed Report, and a Risk Rating Description.

How do you check whether a VAPT report's scope statement is still accurate?

Treat it as an assertion to test rather than prose to skim. CERT-In's 2025 guidelines say the auditing organisation may advise the auditee to state explicitly the date up to which the scope or asset inventory has been updated, and that this date must then be reflected in the audit reports — so a well-formed report carries a vintage. Derive what the cluster declares it exposes today from live objects (Ingress rules and TLS hosts, LoadBalancer addresses, allocated node ports, node addresses) and diff that against the asset list the statement of scope names. Anything exposed now and absent from the scope statement is untested surface, not a passed test. The derived list reads exposure as cluster objects declare it, not as an attacker would find it, so narrow it with routing and firewall facts first.

Should you accept the CVSS score in the report as it arrived?

Not as the final ranking, and which way it is wrong depends on the version. The CVSS v4.0 specification combines base metric values with defaults assuming the highest severity for the Threat and Environmental groups, and an unstated Confidentiality, Integrity or Availability Requirement is Not Defined, equivalent to High — so a bare v4.0 score is a worst case for some environment rather than yours. CVSS v3.1 defaults those same requirements to the equivalent of Medium, so a bare v3.1 score can move up as well as down once you state them. Either way only the operator can supply the environmental values. Record the version alongside the vector and recompute in it. Exploitation probability is not the gap: CERT-In's 2025 guidelines already require auditors to put both CVSS and EPSS in the report.

How long do you have to close findings from a VAPT?

Under SEBI's CSCRF v1.0 (August 2024), Table 19 sets the default at within three months of submission of the VAPT report, following a graded approach based on the criticality of the observations, and section 4.3.4 requires anything still open after three months of the VAPT activity to be approved by the IT Committee and closed before the next VAPT exercise starts — two clocks that do not begin together. The CSCRF FAQs of 11 June 2025 clarify that a vulnerability arising from an unimplemented high-severity patch is validated against PR.MA.S3's one-week patch timeline instead. Revalidation must be completed within five months of completion of the VAPT. PCI SSC adds that remediation extending for a long period after the initial test may require an entirely new engagement rather than a retest.

vulnerability assessment and penetration testingvapt report formatpenetration testing and vulnerability assessmentvapt report scope statementvapt report evaluation checklistsebi cscrf vapt report annexure-a

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.