Skip to content
academia.sh

Lesson 10 / 18

File and Link Reputation Services

Measuring the combination of multi-engine reputation results: recall falling from 92 percent to 8 percent as the majority threshold rises across 24 items and 12 engines, precision instead rising from 50 percent to 86 percent at the seventh threshold and then returning to 50 percent, the majority decision within a single engine family turning out identical to picking a single engine, 10 of the 24 items never being queryable at all because of the privacy limit, and every query leaving a trace on the other side.

Published Updated

In this lesson

  • Combines multi-engine results as a majority-threshold scan and measures precision and recall separately
  • Shows that dependence within an engine family renders the majority decision ineffective
  • Counts file submission's privacy cost and the trace it leaves on the other side, item by item
Contents

In the previous lesson two items could not be resolved in any analysis environment, and a separate prep, observation, and manual-review budget was spent for every item. There is a second way to resolve an item: ask the same item to a large number of independent engines and combine the results that come back. This route takes seconds, and in return raises a new question — how to combine results that contradict each other.

A multi-engine reputation service asks a large number of detection engines about an item or a link target and shows the flags that come back side by side. The output is not a decision, it is a vote distribution; the decision is made by whoever reads that distribution. What this lesson measures is the quality of that reading.

Counting Votes and Independence

A threshold has to be chosen: how many engines flagging an item counts it as bad. As the threshold drops, more bad items get caught and more clean items get mistakenly flagged; as it rises, the opposite happens. Two numbers measure this trade-off. Precision is how much of what we called bad is actually bad. Recall is how much of what is actually bad we caught.

The majority decision paying off depends on a single condition: engines being wrong independently of each other. Two engines using the same method get the same items wrong; their combined vote says no more than a single vote. In this lesson engines are split into two families and are considered nested within a family: a highly sensitive engine flags every item the less sensitive one flags. There is no such relationship between families.

  • AT29. The engine list, item list, and threshold values are a model; there is no real reputation service, no real file, no real vulnerability identity.
  • AT30. An engine only flags an item if its sensitivity is greater than or equal to the item’s threshold in that family. For bad items the threshold means “the sensitivity needed to catch it”; for clean items it means “the sensitivity needed to flag it falsely.”
  • AT31. Engines are nested within a family; families are independent of each other. In reality there is partial dependence between engines, and this model shows the two extremes together.
  • AT32. There is no randomness; every threshold was written by hand.
  • AT33. An item with an open privacy class can have its file sent. internal and subscriber-data items have no file sent; only a hash is queried, and a hash query only returns a result if the item is known on the service side.
  • AT34. Sending a file to an outside service is publishing it; it is irreversible.
  • AT35. The query does not go to target systems; it does not produce a record on target.
// itibar/engine.mjs — this is a MODEL of a multi-engine reputation service. There is no real
// service, no real file, and no real item; engines are referred to only by their family and
// sensitivity value. Two families look at two separate signals. Engines within a family are
// nested: a highly sensitive engine flags every item the less sensitive one flags.
export const ENGINE = [
  ...[30, 40, 50, 60, 75, 90].map((d, i) => ({ name: `s${i + 1}`, family: 'signature', sensitivity: d })),
  ...[30, 40, 50, 60, 75, 90].map((d, i) => ({ name: `b${i + 1}`, family: 'behavior', sensitivity: d })),
];

// Item: if bad, threshold = lowest sensitivity needed to catch it (per family).
// If clean, threshold = lowest sensitivity needed to flag it falsely. 95 = no engine.
// privacy: open = file can be sent; internal and subscriber-data = cannot be sent, only a hash is queried.
// known: whether the hash query has a match on the service side.
export const ITEM = `
  k01 bad   30 30 open             yes
  k02 bad   40 40 open             yes
  k03 bad   50 40 internal         yes
  k04 bad   60 50 open             no
  k05 bad   50 60 internal         no
  k06 bad   75 60 subscriber-data  yes
  k07 bad   60 75 internal         no
  k08 bad   75 75 open             no
  k09 bad   90 75 internal         yes
  k10 bad   95 60 subscriber-data  no
  k11 bad   95 95 internal         no
  k12 bad   40 50 open             no
  k13 clean 95 95 open             yes
  k14 clean 30 95 internal         yes
  k15 clean 95 30 subscriber-data  yes
  k16 clean 40 95 internal         no
  k17 clean 95 40 open             yes
  k18 clean 50 95 internal         no
  k19 clean 95 50 subscriber-data  yes
  k20 clean 60 95 internal         no
  k21 clean 95 60 open             yes
  k22 clean 75 95 internal         no
  k23 clean 95 75 subscriber-data  no
  k24 clean 30 30 subscriber-data  no
`.trim().split('\n').map((r) => {
  const [name, actual, sig, beh, privacy, known] = r.trim().split(/\s+/);
  return { name, bad: actual === 'bad', threshold: { signature: +sig, behavior: +beh },
    privacy, known: known === 'yes' };
});

export const flags = (m, k) => m.sensitivity >= k.threshold[m.family];
export const flagCount = (k, set = ENGINE) => set.filter((m) => flags(m, k)).length;

// Majority decision: an item is called bad if at least 'threshold' engines in the given set flagged it.
export function majority(threshold, set = ENGINE) {
  const s = { tp: 0, fp: 0, fn: 0, tn: 0 };
  for (const k of ITEM) {
    const calledBad = flagCount(k, set) >= threshold;
    if (calledBad && k.bad) s.tp += 1;
    else if (calledBad) s.fp += 1;
    else if (k.bad) s.fn += 1;
    else s.tn += 1;
  }
  s.precision = s.tp + s.fp ? s.tp / (s.tp + s.fp) : 1;
  s.recall = s.tp / ITEM.filter((k) => k.bad).length;
  return s;
}

export const singleEngine = (m) => majority(1, [m]);

// Privacy limit: a file can only be sent for 'open' items. For the others a hash is queried,
// and a hash query only returns a result if the item is known on the service side.
export const queryable = (k) => k.privacy === 'open' || k.known;
export const publishable = (k) => k.privacy === 'open';

Twenty-Four Items, Twelve Engines

// itibar/measure.mjs — 24 items are run through 12 engines; the majority threshold and engine family are scanned.
import { ENGINE, ITEM, flagCount, majority, singleEngine, queryable, publishable } from './engine.mjs';

const y = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join('').trimEnd());
const percent = (x) => `${(x * 100).toFixed(0)}%`;
const sig = ENGINE.filter((m) => m.family === 'signature');
const beh = ENGINE.filter((m) => m.family === 'behavior');

console.log(`${ITEM.length} items (${ITEM.filter((k) => k.bad).length} bad), ` +
  `${ENGINE.length} engines: ${sig.length} signature + ${beh.length} behavior`);

const A = [-8, 8, 11, 10, 10, 18];
console.log('\n1. flag count per item');
y(A, 'item', 'actual', 'signature', 'behavior', 'total', 'privacy');
for (const k of ITEM)
  y(A, k.name, k.bad ? 'bad' : 'clean', flagCount(k, sig), flagCount(k, beh),
    flagCount(k), k.privacy);

const B = [-11, 8, 8, 8, 11, 8];
console.log('\n2. majority threshold scan (12 engines)');
y(B, 'threshold', 'true+', 'false+', 'false-', 'precision', 'recall');
for (let e = 1; e <= ENGINE.length; e += 1) {
  const s = majority(e);
  y(B, e, s.tp, s.fp, s.fn, percent(s.precision), percent(s.recall));
}

const C = [-34, 8, 8, 8, 11, 8];
console.log('\n3. comparison of decision rules');
y(C, 'decision rule', 'true+', 'false+', 'false-', 'precision', 'recall');
const row = (name, s) => y(C, name, s.tp, s.fp, s.fn, percent(s.precision), percent(s.recall));
const singles = ENGINE.map((m) => ({ m, s: singleEngine(m) })).filter((x) => x.s.tp > 0);
const mostPrecise = singles.reduce((a, b) => (b.s.precision > a.s.precision ? b : a));
row(`most precise single engine (${mostPrecise.m.name})`, mostPrecise.s);
row('signature only, threshold 2', majority(2, sig));
row('behavior only, threshold 2', majority(2, beh));
row('mixed, threshold 3', majority(3));
row('mixed, threshold 5', majority(5));
row('mixed, threshold 7', majority(7));

const identical = (set) => set.every((_, i) => {
  const a = majority(i + 1, set), b = singleEngine(set[set.length - 1 - i]);
  return a.tp === b.tp && a.fp === b.fp;
});
console.log(`  is majority within a single family identical to picking a single engine: ` +
  `signature ${identical(sig) ? 'yes' : 'no'}, behavior ${identical(beh) ? 'yes' : 'no'}`);

console.log('\n4. privacy limit and trace');
const open = ITEM.filter(publishable), queried = ITEM.filter(queryable);
console.log(`  items whose file can be sent (publishable)  ${open.length} / ${ITEM.length}`);
console.log(`  items matched via hash query                ${queried.length - open.length}`);
console.log(`  items that cannot be queried in any form      ${ITEM.length - queried.length}`);
console.log(`    of these, actually bad                     ` +
  `${ITEM.filter((k) => k.bad && !queryable(k)).length}`);
console.log(`  unsendable because they carry subscriber data ` +
  `${ITEM.filter((k) => k.privacy === 'subscriber-data').length}`);
console.log(`  submissions leaving a trace on the other side ${queried.length}`);
console.log(`  records produced on target systems             0`);

console.log(`\nbad items no engine flags                 ` +
  `${ITEM.filter((k) => k.bad && flagCount(k) === 0).length}`);
console.log(`clean items every engine flags             ` +
  `${ITEM.filter((k) => !k.bad && flagCount(k) === ENGINE.length).length}`);
console.log(`bad items a single family is blind to      ` +
  `${ITEM.filter((k) => k.bad && (flagCount(k, sig) === 0 || flagCount(k, beh) === 0)).length}`);
24 items (12 bad), 12 engines: 6 signature + 6 behavior

1. flag count per item
item      actual  signature  behavior     total           privacy
k01          bad          6         6        12              open
k02          bad          5         5        10              open
k03          bad          4         5         9          internal
k04          bad          3         4         7              open
k05          bad          4         3         7          internal
k06          bad          2         3         5   subscriber-data
k07          bad          3         2         5          internal
k08          bad          2         2         4              open
k09          bad          1         2         3          internal
k10          bad          0         3         3   subscriber-data
k11          bad          0         0         0          internal
k12          bad          5         4         9              open
k13        clean          0         0         0              open
k14        clean          6         0         6          internal
k15        clean          0         6         6   subscriber-data
k16        clean          5         0         5          internal
k17        clean          0         5         5              open
k18        clean          4         0         4          internal
k19        clean          0         4         4   subscriber-data
k20        clean          3         0         3          internal
k21        clean          0         3         3              open
k22        clean          2         0         2          internal
k23        clean          0         2         2   subscriber-data
k24        clean          6         6        12   subscriber-data

2. majority threshold scan (12 engines)
threshold     true+  false+  false-  precision  recall
1                11      11       1        50%     92%
2                11      11       1        50%     92%
3                11       9       1        55%     92%
4                 9       7       3        56%     75%
5                 8       5       4        62%     67%
6                 6       3       6        67%     50%
7                 6       1       6        86%     50%
8                 4       1       8        80%     33%
9                 4       1       8        80%     33%
10                2       1      10        67%     17%
11                1       1      11        50%      8%
12                1       1      11        50%      8%

3. comparison of decision rules
decision rule                        true+  false+  false-  precision  recall
most precise single engine (b5)         11       6       1        65%     92%
signature only, threshold 2              9       6       3        60%     75%
behavior only, threshold 2              11       6       1        65%     92%
mixed, threshold 3                      11       9       1        55%     92%
mixed, threshold 5                       8       5       4        62%     67%
mixed, threshold 7                       6       1       6        86%     50%
  is majority within a single family identical to picking a single engine: signature yes, behavior yes

4. privacy limit and trace
  items whose file can be sent (publishable)  8 / 24
  items matched via hash query                6
  items that cannot be queried in any form      10
    of these, actually bad                     4
  unsendable because they carry subscriber data 6
  submissions leaving a trace on the other side 14
  records produced on target systems             0

bad items no engine flags                 1
clean items every engine flags             1
bad items a single family is blind to      2

What a Threshold Buys

The second table tries every threshold in sequence and shows that no single row raises both precision and recall at once. If one engine flagging is enough, recall goes up to 92 percent and precision stays at 50 percent: half of the 22 items called bad are clean. If seven engines are required, precision goes up to 86 percent and recall drops to 50 percent: six bad items are missed.

Two items stand at the two ends of this curve, and both are independent of the threshold. k11 is not flagged by any engine; whichever threshold is chosen, it slips through. k24 is flagged by every engine; whichever threshold is chosen, it stays a false positive. These two items are an error floor that counting votes cannot fix, and they have to be handled in the report not with the line “the reputation query could not decide” but by another method.

Choosing a threshold is therefore not a technical decision but a business decision: if the cost of a false positive is an hour of investigation, the threshold is pulled down; if it is recalling the field team, it is pulled up. The same table makes two different organizations pick two different rows.

When Majority Adds Something

The last row of the third table is this lesson’s structural finding: within a single family, the majority decision is identical to picking a single engine. Requiring two of the six signature engines to flag an item gives the exact same result as listening to the signature family’s fifth engine alone — the same true positives, the same false positives. The code checks this at all six thresholds and prints yes for both families. The reason is this: counting votes among nested engines is nothing more than picking a point on the sensitivity axis. The vote of twelve engines looking at the same method is the vote of a single engine.

The mixed set behaves differently, but its gain is conditional. At 92 percent recall, the mixed threshold of 3 is worse than the best single engine: it produces 9 false positives against the same 11 true positives, where the single engine produces 6. By contrast, the mixed threshold of 7 reaches a precision no single engine can reach — 86 percent against the best single engine’s 65 percent. The gain comes from the two families being wrong on different items: two bad items that one family is blind to are flagged by the other family, and the false positives one family produces do not find a vote in the other family.

The rule that follows from this: what should be counted when reading a multi-engine result is not the number of engines, it is the number of different methods. If six of twelve flags come from the same family, those six flags are one flag.

The Item That Cannot Be Sent

The fourth block gives the cost, and most of that cost is not technical. Only eight of the twenty-four items can have their file sent. Six items cannot be sent under any condition because they carry subscriber data; uploading a file to an outside service is publishing it, and it is irreversible. For the remaining items only a hash can be queried, but a hash query only answers if the item is known on the service side. The result: ten items cannot be queried in any form, and four of these are actually bad.

This is another instance of the same shape as the scope limit from earlier lessons. There, a request could not be sent to an out-of-scope target; here, the privacy class cannot leave the organization. Both produce an unverifiable candidate, and both are written in the report with the line “the method could not be applied to this item.”

The trace column on the defense side is again zero in this lesson: queries do not go to target systems. The trace lands elsewhere. Each of the fourteen queries enters the service’s own records, and someone able to watch those records learns which item was investigated and when. The second of the two trace types separated at the end of the previous lesson takes its most concrete form here: querying an item can tell whoever is using it that it is being examined, and that information cannot be taken back.

Summary

  • As the majority threshold rises, recall falls from 92 percent to 8 percent; precision is not monotonic — it rises from 50 percent to 86 percent at the seventh threshold, then returns to 50 percent at the twelfth. No threshold raises both at once, and the choice is a business decision.
  • Two items are independent of the threshold: one is a bad item no engine flags, the other a clean item every engine flags. These two are an error floor that counting votes cannot fix.
  • Within a single family the majority decision is identical to picking a single engine; counting votes among nested engines is nothing more than picking a point on the sensitivity axis.
  • The mixed set’s gain is conditional: at the same recall it produces more false positives than a single engine, but at a high threshold it reaches 86 percent precision that no single engine reaches. What should be counted is the number of different methods, not the number of engines.
  • Eight of twenty-four items can have their file sent; six cannot be sent under any condition because they carry subscriber data, and ten items cannot be queried in any form. Four of these are actually bad.
  • Records produced on target are zero; fourteen queries land in the service’s own records and can tell the other side it is being examined.

Next Step

Five tool classes have now hit the same limit. Traffic fields left inference, the analyzer dropped to metadata in an encrypted flow, the intercepting proxy had to leave two candidates unresolved because of scope, the sandbox could not resolve two items, the reputation service could not even query ten items. Every time, the same gap remained: the last step of showing that a candidate is really open was never taken.

The next lesson examines the tool class that takes up that last step — exploitation frameworks — only for validation purposes and within their limits. Three things are measured: what a framework proves, what it does not prove, and under what condition it damages the target. The second point especially gets emphasis, because a failed validation attempt does not prove the vulnerability does not exist, and losing this distinction is the report’s most expensive mistake.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close