---
title: 'Open-Source Intelligence'
source: 'https://academia.sh/en/courses/penetration-testing/open-source-intelligence'
course: 'Security Tooling and Penetration Testing'
language: en
updated: '2026-09-26T03:02:55+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts the attack surface candidates extracted from publicly available sources and separates them into evidence, lead, and false positive'
    - "Measures each source type's validation-step cost and the number of records it leaves on the target side"
    - 'Compares the return of adding a source against its effect on accuracy'
---

# Open-Source Intelligence

From a publicly available item to an attack surface candidate: 29 items yielding 26 mentions and 20 unique candidates, 9 of which are evidence and 6 false positives (30%), the most accurate source producing 83% evidence and the least productive 0%, 12 of 45 validation steps returning zero evidence, the evidence count flattening after four sources while accuracy drops from 69% to 45%, and only 9 of a 13-item inventory being visible from outside.

The previous course counted, on paper, which link could be broken, and closed with a question:
only someone authorized, actually trying, can show whether a defense truly holds. This course is
that attempt itself. Its first step is knowing **what to look at** — and that list is not handed
over by the organization itself; it is drawn out of items that are already sitting out in the
open.

**Open-source intelligence** is the extraction, from publicly available items, of assets that may
belong to the target. The critical word here is **may**. This course's unit of measure is not the
number of findings, it is the number of **evidenced findings**, and the distinction starts at the
very first step: a name appearing in a list does not show that the name belongs to the
organization.

## Candidate, Evidence, and False Positive

This assessment is **authorized**: the scope owner has granted written authorization and defined
which assets may be tested. Performing the same steps outside written authorization and a defined
scope is both illegal and unprofessional.

**RE1:** each source type carries four numbers — how many items it takes, how many records it
leaves on the target side, how many alerts it triggers, and how many validation steps it requires
per candidate. **RE2:** a candidate appearing in a source is called a *mention*; the same
candidate can appear in multiple sources, so the mention count is larger than the candidate
count. **RE3:** the inventory the scope owner has approved is the referee of this measurement; a
candidate in the inventory counts as **evidence**, one not in the inventory but seen to serve the
organization counts as a **lead**, and one that does not overlap with the inventory at all counts
as a **false positive**.

```js
// recon/source.mjs — a fictional measurement network's set of publicly available items (a
// model): no real domain name, no real public address, no real organization. No randomness.

// RE1: a source type carries these four numbers — items taken, records it leaves on the
// target, alerts it triggers, and validation steps required per candidate. A source sitting
// on a third party produces no record on the target; an item taken from the organization's
// own endpoint leaves an access record on every fetch.
export const SOURCE = `
  certificate-record   4   0  0  1
  document-metadata    3   0  0  2
  job-posting          2   0  0  2
  public-code-repo     3   0  0  2
  archived-copy        5   0  0  3
  corporate-website   12  12  0  1`
  .trim().split("\n").map((r) => {
    const [type, items, record, alert, step] = r.trim().split(/\s+/);
    return { type, items: +items, record: +record, alert: +alert, step: +step };
  });

// RE2: each row is one candidate appearing in one source; the same candidate can appear in
// multiple sources.
export const MENTION = `
  certificate-record   subscriber-portal
  certificate-record   field-app
  certificate-record   test-portal
  certificate-record   shared-interface
  certificate-record   meter-update-endpoint
  certificate-record   old-report-endpoint
  document-metadata    report-endpoint
  document-metadata    shared-interface
  document-metadata    branch-vpn-endpoint
  document-metadata    old-report-endpoint
  job-posting          work-order-endpoint
  job-posting          branch-vpn-endpoint
  job-posting          training-environment
  public-code-repo     collector-gateway
  public-code-repo     cloud-storage-bucket
  public-code-repo     test-portal
  public-code-repo     spare-domain
  archived-copy        old-subscriber-portal
  archived-copy        old-field-app
  archived-copy        support-portal
  archived-copy        dealer-portal
  corporate-website    subscriber-portal
  corporate-website    report-endpoint
  corporate-website    marketing-page
  corporate-website    identity-provider-endpoint
  corporate-website    payment-provider-endpoint`
  .trim().split("\n").map((r) => {
    const [source, candidate] = r.trim().split(/\s+/);
    return { source, candidate };
  });

// RE3: the fictional inventory is, in this assessment, the real asset list the scope owner
// approved, and it is the referee of the measurement. A candidate not in the inventory is a
// false positive.
export const INVENTORY = [
  "subscriber-portal", "field-app", "branch-vpn-endpoint", "collector-gateway", "report-endpoint",
  "test-portal", "shared-interface", "work-order-endpoint", "meter-update-endpoint",
  "internal-network-management-endpoint", "backup-endpoint", "measurement-store", "identity-store",
];

// Items whose ownership cannot be proven from outside, sitting on a third party but serving
// the organization.
export const LEAD = ["old-subscriber-portal", "marketing-page", "cloud-storage-bucket",
  "identity-provider-endpoint", "training-environment"];

export const CANDIDATE = [...new Set(MENTION.map((a) => a.candidate))];
export const classify = (a) =>
  INVENTORY.includes(a) ? "evidence" : LEAD.includes(a) ? "lead" : "false-positive";
```

```js
// recon/candidate.mjs — from a publicly available item to an attack surface candidate: how
// many candidates come out, how many are evidence.
import { SOURCE, MENTION, CANDIDATE, INVENTORY, classify } from "./source.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const count = (l, s) => l.filter((a) => classify(a) === s).length;
const ITEMS = SOURCE.reduce((t, k) => t + k.items, 0);

console.log(`${SOURCE.length} source types, ${ITEMS} publicly available items, ${MENTION.length} mentions, ` +
  `${CANDIDATE.length} unique candidates; inventory ${INVENTORY.length} items`);

const A = [-19, 7, 6, 7, 7, 8, 9];
console.log("\n1. candidates and accuracy per source type");
write(A, "source type", "items", "ment", "evid", "leads", "falseP", "evid %");
for (const k of SOURCE) {
  const l = MENTION.filter((a) => a.source === k.type).map((a) => a.candidate);
  write(A, k.type, k.items, l.length, count(l, "evidence"), count(l, "lead"), count(l, "false-positive"),
    ((count(l, "evidence") / l.length) * 100).toFixed(0));
}
write(A, "TOTAL (unique)", ITEMS, CANDIDATE.length, count(CANDIDATE, "evidence"), count(CANDIDATE, "lead"),
  count(CANDIDATE, "false-positive"), ((count(CANDIDATE, "evidence") / CANDIDATE.length) * 100).toFixed(0));

const B = [-19, 9, 7, 8, 12, 12];
console.log("\n2. the cost of validation and the trace left on the defense side");
write(B, "source type", "records", "alerts", "steps", "steps/evid", "rec/evid");
let stepTotal = 0;
for (const k of SOURCE) {
  const l = MENTION.filter((a) => a.source === k.type).map((a) => a.candidate);
  const kn = count(l, "evidence"), st = l.length * k.step;
  stepTotal += st;
  write(B, k.type, k.record, k.alert, st, kn ? (st / kn).toFixed(2) : "-",
    kn ? (k.record / kn).toFixed(2) : "-");
}
const TRACE = SOURCE.reduce((t, k) => t + k.record, 0);
write(B, "TOTAL", TRACE, SOURCE.reduce((t, k) => t + k.alert, 0), stepTotal,
  (stepTotal / count(CANDIDATE, "evidence")).toFixed(2), (TRACE / count(CANDIDATE, "evidence")).toFixed(2));

const C = [-19, 10, 12, 13];
console.log("\n3. if one source type is dropped");
write(C, "dropped source", "evid left", "missed", "missed %");
const EVID = CANDIDATE.filter((a) => classify(a) === "evidence");
for (const k of SOURCE) {
  const remaining = [...new Set(MENTION.filter((a) => a.source !== k.type).map((a) => a.candidate))]
    .filter((a) => classify(a) === "evidence");
  write(C, k.type, remaining.length, EVID.length - remaining.length,
    (((EVID.length - remaining.length) / EVID.length) * 100).toFixed(1));
}

const D = [-19, 7, 7, 9, 9, 9];
console.log("\n4. as sources are added (from most to least accurate)");
write(D, "added source", "cand", "evid", "falseP", "evid %", "trace");
const sorted = [...SOURCE].sort((x, y) => {
  const o = (k) => { const l = MENTION.filter((a) => a.source === k.type).map((a) => a.candidate);
    return count(l, "evidence") / l.length; };
  return o(y) - o(x);
});
const accumulated = [];
let traceTotal = 0;
for (const k of sorted) {
  accumulated.push(k.type); traceTotal += k.record;
  const l = [...new Set(MENTION.filter((a) => accumulated.includes(a.source)).map((a) => a.candidate))];
  write(D, k.type, l.length, count(l, "evidence"), count(l, "false-positive"),
    ((count(l, "evidence") / l.length) * 100).toFixed(0), traceTotal);
}

const visible = INVENTORY.filter((e) => MENTION.some((a) => a.candidate === e)).length;
console.log(`\nexternally invisible inventory items: ` +
  INVENTORY.filter((e) => !MENTION.some((a) => a.candidate === e)).join(", "));
console.log(`${visible}/${INVENTORY.length} inventory items are visible ` +
  `(${((visible / INVENTORY.length) * 100).toFixed(1)}%); ` +
  `${count(CANDIDATE, "false-positive")}/${CANDIDATE.length} candidates are false positives ` +
  `(${((count(CANDIDATE, "false-positive") / CANDIDATE.length) * 100).toFixed(1)}%)`);
```

```
6 source types, 29 publicly available items, 26 mentions, 20 unique candidates; inventory 13 items

1. candidates and accuracy per source type
source type          items  ment   evid  leads  falseP   evid %
certificate-record       4     6      5      0       1       83
document-metadata        3     4      3      0       1       75
job-posting              2     3      2      1       0       67
public-code-repo         3     4      2      1       1       50
archived-copy            5     4      0      1       3        0
corporate-website       12     5      2      2       1       40
TOTAL (unique)          29    20      9      5       6       45

2. the cost of validation and the trace left on the defense side
source type          records alerts   steps  steps/evid    rec/evid
certificate-record         0      0       6        1.20        0.00
document-metadata          0      0       8        2.67        0.00
job-posting                0      0       6        3.00        0.00
public-code-repo           0      0       8        4.00        0.00
archived-copy              0      0      12           -           -
corporate-website         12      0       5        2.50        6.00
TOTAL                     12      0      45        5.00        1.33

3. if one source type is dropped
dropped source      evid left      missed     missed %
certificate-record          7           2         22.2
document-metadata           9           0          0.0
job-posting                 8           1         11.1
public-code-repo            8           1         11.1
archived-copy               9           0          0.0
corporate-website           9           0          0.0

4. as sources are added (from most to least accurate)
added source          cand   evid   falseP   evid %    trace
certificate-record       6      5        1       83        0
document-metadata        8      7        1       88        0
job-posting             10      8        1       80        0
public-code-repo        13      9        2       69        0
corporate-website       16      9        3       56       12
archived-copy           20      9        6       45       12

externally invisible inventory items: internal-network-management-endpoint, backup-endpoint, measurement-store, identity-store
9/13 inventory items are visible (69.2%); 6/20 candidates are false positives (30.0%)
```

The numbers belong to the **measurement** class; their inputs are RE1–RE3.

## Twenty Candidates, Nine Evidence

Twenty-nine publicly available items produced twenty-six mentions; once mentions were
deduplicated, **20 candidates** remained. Of these twenty candidates, **9 are evidence**, 5 are
leads, and **6 are false positives**. 30% of the raw list does not belong to the organization, and
if it went into a report without validation, it would produce six items with no one on the other
end to fix them.

The first table's real information is the accuracy gap between source types. Certificate record
produces evidence in five of six mentions — **83%**. Archived copy produces four mentions and
**none of them is evidence**; three are outright false positives. Both fall under the heading
"publicly available source," and both enter the candidate list the same way, but one is nearly
pure evidence and the other is nearly pure noise.

The difference comes from the source's **nature**. A certificate record shows that a certificate
was issued for a name, and the party who can request that is limited; an archived copy shows only
that something once appeared on a page, and something that was true in the past does not have to
be true today. The `old-field-app` item that came out of the archive once belonged to the
organization, was handed off, and is now the responsibility of another party. The
`support-portal` and `dealer-portal` items sitting in the same list, meanwhile, entered it only
because of **name similarity**.

## The Step That Leaves a Trace and the One That Does not

The second table opens the column that will carry through this course: how many **records** a
step produces on the defense side. **Five of the six** source types leave zero records on the
target, because the item sits in a third party's hands; there is no trace of these steps in the
target's log at all. Only the twelve items taken from the organization's own endpoint produce
twelve access records.

That these twelve records produce **0 alerts** is the table's second finding. Fetching a portal's
twelve pages in sequence is indistinguishable from what a normal visitor would do; on the defense
side, these rows sit inside the noise. From the blue team's perspective, the conclusion is this:
most of the reconnaissance phase happens where the defense cannot see it, and the visible part
stays below the threshold. **Detecting this phase is not a matter of alerts, but a matter of
retrospective correlation.**

The same table's `step` column gives the cost. Forty-five validation steps were spent, and nine
pieces of evidence came out of them: **5.00 steps per evidence item**. The distribution is not
even. Certificate record spends 1.20 steps per evidence item; public code repo spends 4.00 steps;
archived copy **spends 12 steps and produces no evidence at all**. A reconnaissance effort that
orders sources by habit rather than accuracy sinks a quarter of its budget into a source with zero
return.

## Two Sides of Adding a Source

The third table measures each source's **indispensability**. When three of the six sources are
dropped, no evidence is lost at all: document metadata, archived copy, and the organization's own
website share their evidence with other sources. Dropping certificate record loses **2 pieces of
evidence** (22.2%), and job posting and public code repo each take away one piece of evidence. So
reaching all nine pieces of evidence does not require six sources — but the fact that three of them
are individually dispensable does not mean the three of them can be dropped **together**; the next
table shows that four are enough.

The fourth table shows this cumulatively, and it is the lesson's sharpest finding. When sources
are added one by one in order of accuracy, the evidence count moves 5 → 7 → 8 → **9** and **stops
growing entirely** after the fourth source. The candidate count, by contrast, climbs from 13 to 16
to 20, and false positives rise from 2 to 6. Accuracy drops from **69% to 45%** over those same
two steps.

The last two sources do this: they add seven new candidates, bring **zero new evidence**, produce
four new false positives, and leave **12 records** on the defense side. The reconnaissance-phase
assumption that "more sources means better coverage" breaks down in this table. Adding a source
stops increasing coverage past a certain point; what it increases is the **elimination burden**.

## The Lead Class Stops at the Scope Boundary

Five of the twenty candidates are neither evidence nor false positives. The old subscriber
portal, marketing page, cloud storage bucket, identity provider endpoint, and training
environment **serve** the organization but do not sit in the organization's inventory; all of them
run on third-party infrastructure. These five items are a quarter of the candidate list, and
under a binary elimination rule — "evidence if it is in the inventory, noise if it is not" — they
fall on the wrong side.

The reason this class is kept separate is not measurement, it is **authorization**. An item may
well serve the organization, but acting on it means touching infrastructure that belongs to the
third party who owns that item, and the written authorization granted does not cover that party.
Each of the five lead items is an item that **touches the scope boundary**, and none of them can
be tested without the scope owner's approval. That is also where it differs from a false positive:
a false positive **drops out** of the list, while a lead stays on the list and **waits for a
decision**.

Numerically, it comes to this: nine pieces of evidence can be acted on directly, six false
positives get eliminated, and five items move to a scope conversation. Reconnaissance's output
is not a single list, it is **three separate lists**; a report that collapses the three into one
either works on an item it should not, or drops an item it should have worked on.

## Four Items Invisible from Outside

The last line gives the measurement's upper bound. There are thirteen items in the inventory,
and open-source intelligence sees **nine of them** — **69.2%**. The internal network management
endpoint, backup endpoint, measurement store, and identity store do not appear in any publicly
available item.

These four items say two things by their absence. First, open-source intelligence's coverage is
limited to the organization's **outward-facing side**; assets that sit internally and leave no
external signature do not enter the list at this stage. Second, the list produced at this stage
is not an **inventory**, it is a projection of the inventory as seen from outside. In an assessment
that cannot be compared against the inventory the scope owner provides, this 69.2% is never known
at all; the only thing that is known is twenty candidates, and that nine of those twenty are
evidence can only be said while a referee exists.

## Summary

- This course's unit of measure is not the candidate, it is the evidence: 29 publicly available
  items produced 26 mentions and 20 unique candidates; 9 are evidence, 5 are leads, 6 are false
  positives (30.0%).
- The accuracy gap between source types is large: certificate record produces 83% evidence while
  archived copy produces 0%, and three of its four mentions are false positives.
- Five of the six source types leave zero records on the target; the 12 items taken from the
  organization's own endpoint produce 12 access records and **0 alerts** — this phase stays below
  the threshold.
- The validation cost is 5.00 steps per evidence item and is not evenly distributed: certificate
  record spends 1.20 steps, archived copy spends 12 steps and produces no evidence at all.
- Evidence count flattens at 9 after four sources; the last two sources add 7 candidates, 0
  evidence, 4 false positives, and 12 records, dropping accuracy from 69% to 45%.
- Only 9 of the inventory's 13 items are visible from outside (69.2%); four items do not appear in
  any publicly available source.

## Next Step

What separated nine of the twenty candidates as evidence and six as false positives in this
lesson was a referee that came from outside: the inventory the scope owner approved. In a real
assessment, that inventory is the assessment's **output**, not its input. So the question is
this: without a referee, how is it shown that a candidate belongs to the organization?

For some of them, it can be shown. Domain name and registration systems carry verifiable traces
of who controls a name; some records can only be written by the name's owner, while others point
to a third party hosting the name and prove nothing about ownership. The next lesson treats
records as **inventory evidence**: the same twenty candidates are run through registration
lookup, and it is counted how many have ownership that can be proven, how many stay at the lead
level only, and how many records this lookup leaves on the target side.
