AI governance for engineers · Lesson 6 of 11 · about 35 minutes

Building an AI inventory

Every framework and law in this track starts with the same question: what AI do you have? This lesson builds an inventory for Fernhill, a made-up small company, with real scripts and their real output. A scan finds 25 signals in its code; they turn out to be six systems: one in production, two retired, one never deployed, one for experiments, and one that exists only as a request from HR. Then a checker that catches an entry nobody owns, what a code scan can't see, and the inventory mistake that hides the riskiest system.

By the end you'll know

  • Why an AI inventory comes first, and what each framework asks for
  • How to find AI use in code and configuration, and why that isn't enough
  • How to turn files into systems, and what to record for each
  • How to check an inventory automatically, so it stays honest

Before you start

  • You need: Lessons 1 to 5 help, but aren't required. Comfort with a shell, and Python 3 for the checker (standard library only).
  • The example: Fernhill is made up: a small company with six projects under ~/Apps, built for this lesson. The scripts and every line of output below are real; the company, its people and its projects aren't.
  • Maps to: the IAPP's AIGP (Domain III, governing AI development and deployment); NIST AI RMF GOVERN 1.6; ISO/IEC 42001 clause 4.3 and Annex A.4.

The short version

  • You can't govern what you haven't listed. NIST, ISO/IEC 42001, the EU AI Act and OMB memo M-25-21 all start here.
  • Find it three ways: search code and configuration for signs of AI, check what's actually running, and ask people, because code finds neither bought software nor plans.
  • List systems, not files. Nine files that call a model became five systems; a conversation added a sixth.
  • Record what decides risk: owner, model, where it runs, what data goes in, what the output is used for, whether it decides about people, and a review date.
  • Check it with code, so gaps fail loudly. Fernhill's first check failed on an entry with no owner, last looked at a year ago.
  • The riskiest system on the list doesn't exist yet. That's the cheapest time to govern it.

Why the inventory comes first

Every rule in the last five lessons assumes you know what AI you have:

SourceWhat it asks
NIST AI RMF, GOVERN 1.6"Mechanisms are in place to inventory AI systems", resourced according to risk
ISO/IEC 42001Clause 4.3 sets the scope of the management system; Annex A.4 asks you to document the resources each AI system uses (data, tools, models, people)
EU AI ActNo list by name, but you can't classify a system's tier, or know if you're a provider or a deployer, without one
OMB M-25-21US federal agencies publish an AI use case inventory every year, and flag high-impact uses

An inventory isn't paperwork for its own sake. It's the list every other control hangs from: which systems need an impact assessment, which need human oversight, which send data to someone else, which nobody owns any more.

From 25 signals to one system in production. Select the diagram to enlarge it.

Step 1: find it in code

The scan script searches every project under ~/Apps for seven kinds of signal. Each is a regular expression, checked against code and configuration files only:

BASH
# signal type <TAB> regex
SIGNALS='sdk-hosted	^[[:space:]]*(import|from)[[:space:]]+(anthropic|openai)
sdk-local	^[[:space:]]*(import|from)[[:space:]]+ollama
endpoint-local	(localhost|127\.0\.0\.1):11434|OLLAMA_(HOST|URL)
endpoint-hosted	api\.(anthropic|openai)\.com
api-key	(ANTHROPIC|OPENAI)_API_KEY
model-name	(qwen[0-9.]*:[0-9]+b|llama[0-9.]*:[0-9]+b|claude-[a-z]+-[0-9]|gpt-[0-9])
embeddings	SentenceTransformer|chromadb'

Each line is a name, a tab, and a pattern: imports of hosted or local model libraries, the address of a local model server (Ollama, on port 11434) or a hosted provider's API, the name of an API key variable, a model name, or an embeddings library (the building block of retrieval). Add patterns for whatever providers your organization uses.

BASH
while IFS=$'\t' read -r sig rx; do
  grep -rIlE "${EXCL[@]}" "${INCL[@]}" "$rx" "$ROOT" 2>/dev/null | while read -r f; do
    rel="${f#"$ROOT"/}"
    printf '%s\t%s\t%s\n' "${rel%%/*}" "$rel" "$sig" >> "$OUT"
  done
done <<< "$SIGNALS"

For each signal, grep -rIlE lists the files that match: -r searches folders, -I skips binary files, -l prints only file names, -E allows the pattern syntax above. EXCL skips dependencies, virtual environments and build output (node_modules, .venv, .git and so on); INCL limits the search to code and config (.py, .js, .ts, .yaml, .toml, .sh). Each hit becomes a line of scan.tsv: project, file, signal. The rest of the script summarizes that file. Run on Fernhill:

(Your machine)

BASH
chmod +x scan_ai.sh
./scan_ai.sh

chmod +x makes the script runnable; the second line runs it. It reads files only, and writes nothing outside ~/Apps/ai-inventory-lab.

TEXT
== 1. Scanning ~/Apps for AI signals (code and config only)
   25 signal hits written to ~/Apps/ai-inventory-lab/scan.tsv

== 2. Signals by project
   experiments               3 files: endpoint-local model-name
   invoice-reader            4 files: sdk-hosted endpoint-local model-name
   lead-scorer-poc           1 files: sdk-hosted model-name
   marketing-site            1 files: endpoint-local model-name
   support-bot               3 files: sdk-hosted api-key model-name embeddings

Five of Fernhill's six projects have signals; billing-api has none. Next, the files that actually call a model, leaving out tests and paths known not to be AI use (the script takes a list; here, the marketing site):

TEXT
== 3. Files that call a model (not tests, not known non-uses)
   experiments/ask.py
   experiments/rag.py
   experiments/summarize.py
   invoice-reader/archive/compare_models.py
   invoice-reader/archive/read_invoices_v1.py
   invoice-reader/archive/read_invoices_v2.py
   invoice-reader/read_invoices.py
   lead-scorer-poc/score.py
   support-bot/app.py

Which models the code depends on, and what's running on the machine doing the scan:

TEXT
== 4. Model names written in code (unique)
         4 qwen2.5:3b
         3 qwen2.5:7b
         3 gpt-4o
         2 gpt-4o-mini
         1 qwen2.5:14b

== 5. What's running on this machine
   Ollama is not answering on localhost:11434
   AI API keys set in this shell (names only, never values):
     (none)

Section 5 prints the names of API key variables, never their values. And when each file last changed:

TEXT
== 6. Last change to each model-calling file
   2026-10-29  invoice-reader/read_invoices.py
   2026-10-15  experiments/summarize.py
   2026-10-15  experiments/rag.py
   2026-10-15  experiments/ask.py
   2026-06-11  invoice-reader/archive/read_invoices_v2.py
   2026-06-11  invoice-reader/archive/read_invoices_v1.py
   2026-06-11  invoice-reader/archive/compare_models.py
   2026-04-02  support-bot/app.py
   2025-11-20  lead-scorer-poc/score.py

Three things stand out:

  • No model is running here. Fernhill's invoice reader runs nightly on an ops server, not on the machine that holds the code. A scan sees code, not where it runs.
  • The marketing site's hit isn't AI use. It's a page that shows an example request, and makes none. A signal is a lead, not a finding.
  • Dates separate the living from the dead. One invoice reader file changed in October; three sit in archive/, untouched since June. The lead scorer hasn't changed in a year.

Step 2: list systems, not files

A system is something with one purpose, one owner and one life: it gets deployed, changed and retired as a unit. Nine files became five systems, and a conversation with HR added a sixth:

IDSystemFilesWhy it's one system
AI-01Invoice reader1The current version, the one that runs nightly
AI-02Invoice reader, earlier versions3Hosted-model versions and the comparison that chose the local model; none runs
AI-03Support bot1One app, with its test and Kubernetes manifest
AI-04Lead scorer (proof of concept)1A one-off experiment from 2025
AI-05Experiments3Learning scripts with one purpose and no users
AI-06Applicant screening helper0Planned; HR asked for it. Found by asking, not scanning

The last row is the important one. It's the only system that would make findings about people, the only one that's high-risk under the EU AI Act (Lesson 4), and the only one covered by hiring rules like Illinois's and New York City's (Lesson 5). A code scan would never find it, because it doesn't exist yet. Planned uses belong in the inventory, because that's when governance is cheapest.

Step 3: describe each system

The inventory is a CSV file, so it opens in any spreadsheet and is easy to check with code. Each system gets fifteen fields:

FieldWhy it's there
id, name, purposeWhat it is, in a sentence someone outside the team understands
statusactive, planned, inactive, retired or practice: what needs attention now
ownerOne accountable person. "The team" isn't an owner.
where_it_runsThe machine or cluster, and how often
model, hostingWhat it depends on; local, hosted, or mixed
data_inWhat goes in, especially personal or financial data, and where it's sent
output_useWhat happens to the result; this, more than the model, sets the risk
decides_about_peopleThe single most useful risk flag in every framework
human_oversightWho checks, and at what point
eu_tierLesson 4's classification
code_pathWhere the code is, so retired systems can be checked
last_reviewedWhen someone last confirmed the entry is true

Two of Fernhill's rows, for the system in production and the one that's planned:

TEXT
AI-01,Invoice reader,active,Reads supplier invoices and proposes the total and due date,
  Priya (finance ops),"Ops server, nightly at 02:15",qwen2.5:7b,local,
  Supplier invoices (include bank details),Proposal a clerk approves before payment,no,
  A clerk approves every invoice,minimal,invoice-reader/read_invoices.py,2026-11-07
AI-06,Applicant screening helper,planned,Summarizes each applicant's evidence against the
  job's criteria for HR,Dana (HR),Not built,not chosen,hosted,
  Résumés and job descriptions (personal data),Summaries HR uses to shortlist,yes,
  HR reviews every summary; the hiring manager decides,high (Annex III employment),
  not built,2026-11-07

(Each row is one line in the file; they're wrapped here to fit.)

The support bot's status was the hard one. The code and a Kubernetes manifest were there, but nobody was sure whether it had ever been deployed, and an inventory built from memory is how forgotten systems keep running. So the team checked instead of guessing:

WhereCommandWhat it shows
Containersdocker ps -aContainers running or stopped on a machine
Kuberneteskubectl get deploy,cronjob -ADeployments and scheduled jobs in every namespace
Scheduled jobscrontab -l, systemctl list-timersWhat runs on a schedule on a server
Model serverscurl localhost:11434/api/tagsWhich models a local Ollama server has installed

Nothing named support-bot was running anywhere. A manifest in a repository shows a system could be deployed, not that it is: status inactive. If it ever is deployed, it's a chatbot, so Article 50's transparency duty applies (Lesson 4), and its row says so.

Step 4: check the inventory

The checker reads the CSV and applies seven rules. It's standard-library Python, so it runs anywhere Python 3 does:

PYTHON
#!/usr/bin/env python3
"""Check an AI inventory (CSV) for gaps. Standard library only.

Usage: check_inventory.py ai-inventory.csv ~/Apps
Exit code 1 if any problem is found, so it can run in CI.
"""
import csv
import os
import sys
from datetime import date, datetime

REQUIRED = ["id", "name", "status", "purpose", "owner", "where_it_runs", "model",
            "hosting", "data_in", "output_use", "decides_about_people",
            "human_oversight", "eu_tier", "code_path", "last_reviewed"]
STATUSES = {"active", "planned", "inactive", "retired", "practice"}
REVIEW_DAYS = 90

def main(inv_path, root):
    rows = list(csv.DictReader(open(inv_path, newline="", encoding="utf-8")))
    problems, notes = [], []
    print(f"{len(rows)} entries in {os.path.basename(inv_path)}\n")
    print(f"{'id':6} {'status':9} {'hosting':8} {'people?':8} {'name':42} EU tier")
    for r in rows:
        print(f"{r['id']:6} {r['status']:9} {r['hosting']:8} {r['decides_about_people']:8} "
              f"{r['name'][:42]:42} {r['eu_tier']}")
        rid = r["id"]
        # 1. every field filled
        for f in REQUIRED:
            if not (r.get(f) or "").strip() or r[f].strip().upper() == "CHECK":
                problems.append(f"{rid}: '{f}' is empty or still CHECK")
        # 2. status is one we know
        if r["status"] not in STATUSES:
            problems.append(f"{rid}: status '{r['status']}' is not one of {sorted(STATUSES)}")
        # 3. reviewed recently
        try:
            age = (date.today() - datetime.strptime(r["last_reviewed"], "%Y-%m-%d").date()).days
            if age > REVIEW_DAYS:
                problems.append(f"{rid}: last reviewed {age} days ago (limit {REVIEW_DAYS})")
        except ValueError:
            problems.append(f"{rid}: last_reviewed '{r['last_reviewed']}' is not YYYY-MM-DD")
        # 4. retired means the code is gone, or there's a reason it stays
        path = os.path.join(root, r["code_path"].rstrip("/"))
        if r["status"] == "retired" and os.path.exists(path):
            notes.append(f"{rid}: retired, but its code is still at {r['code_path']}")
        # 5. decisions about people need named oversight and an assessment first
        if r["decides_about_people"] == "yes":
            if r["status"] == "planned":
                notes.append(f"{rid}: decides about people; do an impact assessment "
                             "and check hiring rules before it's built")
            if r["human_oversight"] in ("", "n/a"):
                problems.append(f"{rid}: decides about people with no human oversight named")
        # 6. high-risk can't be quietly active
        if r["eu_tier"].startswith("high") and r["status"] == "active":
            problems.append(f"{rid}: high-risk and active; needs its assessment on file")
        # 7. hosted models: where does the data go?
        if r["hosting"] in ("hosted", "mixed") and r["status"] in ("active", "planned"):
            notes.append(f"{rid}: sends '{r['data_in']}' to a hosted model; "
                         "check the provider's data terms")

    print()
    for n in notes:
        print("NOTE    " + n)
    for p in problems:
        print("PROBLEM " + p)
    print(f"\n{len(problems)} problems, {len(notes)} notes")
    return 1 if problems else 0

if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit(__doc__)
    sys.exit(main(sys.argv[1], os.path.expanduser(sys.argv[2])))

How it works:

  • csv.DictReader turns each row into a dictionary keyed by the header, so r["owner"] is that row's owner.
  • Rules 1 to 3 are about the entry itself: nothing blank or left as CHECK, a known status, and a review in the last 90 days.
  • Rules 4 to 7 are about risk: retired systems whose code still exists (they can be run again by accident), anything that decides about people, high-risk systems running without an assessment, and data sent to a hosted provider.
  • Problems fail the run; notes don't. Problems mean the inventory is wrong or stale. Notes are decisions for a person.
  • The exit code is 1 on any problem, so the checker can run in CI on every change to the inventory, like a test.

Fernhill's first run:

(Your machine)

BASH
python3 check_inventory.py ai-inventory.csv ~/Apps
TEXT
6 entries in ai-inventory.csv

id     status    hosting  people?  name                                       EU tier
AI-01  active    local    no       Invoice reader                             minimal
AI-02  retired   mixed    no       Invoice reader (earlier versions)          minimal
AI-03  inactive  hosted   no       Support bot                                transparency (chatbot) if deployed
AI-04  inactive  hosted   no       Lead scorer (proof of concept)             minimal
AI-05  practice  local    no       Experiments                                minimal
AI-06  planned   hosted   yes      Applicant screening helper                 high (Annex III employment)

NOTE    AI-02: retired, but its code is still at invoice-reader/archive/
NOTE    AI-06: decides about people; do an impact assessment and check hiring rules before it's built
NOTE    AI-06: sends 'Résumés and job descriptions (personal data)' to a hosted model; check the provider's data terms
PROBLEM AI-04: 'owner' is empty or still CHECK
PROBLEM AI-04: last reviewed 352 days ago (limit 90)

2 problems, 3 notes

The lead scorer had no owner (the person who wrote it had moved on), and nobody had looked at it in almost a year. That's the most common inventory finding there is. Sales ops took ownership, confirmed nobody used it, and marked it retired. The second run:

TEXT
6 entries in ai-inventory.csv

id     status    hosting  people?  name                                       EU tier
AI-01  active    local    no       Invoice reader                             minimal
AI-02  retired   mixed    no       Invoice reader (earlier versions)          minimal
AI-03  inactive  hosted   no       Support bot                                transparency (chatbot) if deployed
AI-04  retired   hosted   no       Lead scorer (proof of concept)             minimal
AI-05  practice  local    no       Experiments                                minimal
AI-06  planned   hosted   yes      Applicant screening helper                 high (Annex III employment)

NOTE    AI-02: retired, but its code is still at invoice-reader/archive/
NOTE    AI-04: retired, but its code is still at lead-scorer-poc/
NOTE    AI-06: decides about people; do an impact assessment and check hiring rules before it's built
NOTE    AI-06: sends 'Résumés and job descriptions (personal data)' to a hosted model; check the provider's data terms

0 problems, 4 notes

Zero problems. Four notes, each a decision for a person:

  • AI-02 and AI-04: retired code that still exists can be run again by accident. Delete it, or keep it on purpose and write down why. The invoice reader's archive is a fair keep: it's the record of why the local model was chosen.
  • AI-06: before any code is written, the screening helper needs an impact assessment, a decision on which hiring rules apply, and a look at what the hosted provider does with résumés. The later lessons on testing, oversight and vendors pick each of these up.

In real systems

  • Code is one source of four. The others are procurement (AI inside bought software: a CRM's lead scoring, an HR system's screening), expense reports and single sign-on logs (people signing up for AI tools on their own, sometimes called shadow AI), and asking teams directly. Most organizations find more AI in what they bought than in what they built.
  • Network and cloud logs help. Outbound calls to model providers' API domains, and model endpoints in cloud accounts, show use the code search missed.
  • Own each entry. An inventory nobody reviews drifts within a quarter. A review date and a check that fails when it passes are the cheapest fix.
  • Agencies publish theirs. US federal AI use case inventories are public, and a good model of the fields that matter: purpose, stage, whether it's high-impact, and what's been done about it.
  • Keep it boring. A spreadsheet that people update beats a governance platform nobody opens. Start with the fifteen fields and a checker.

Spot the problem

Work out what's wrong before you read the explanation.

"We inventoried the repos." A company's AI governance lead runs a code scan like the one above across all 400 of its repositories, builds a tidy inventory of 31 systems, all minimal risk, and reports to the board that the company has no high-risk AI. Six months later, an applicant's complaint reveals that the HR team has been using a vendor's résumé screening feature, switched on in the applicant tracking system the company already paid for. What's wrong? The inventory only looked where AI is built, not where it's bought or switched on. The screening feature never appeared in any repository; it was a setting in purchased software, and it's the one system that was high-risk under the EU AI Act and covered by New York City's audit law. Three fixes: add procurement and contract review to the inventory process, asking every vendor whether their product uses AI and for what; ask the teams that make decisions about people (HR, lending, support) what tools they use, not just engineering; and don't report "no high-risk AI" without saying which sources were checked.

Keep this

StepDo
Find in codeModel libraries, endpoints, key names, model names, embeddings; skip dependencies
Find at runtimeContainers, clusters, scheduled jobs, model servers, network logs
Find what's boughtVendor products with AI features; contracts; expense reports
Find what's plannedAsk: the cheapest time to govern a system is before it's built
GroupOne system per purpose and owner; tests and demos aren't uses
DescribeThe fifteen fields; above all, output use and whether it decides about people
CheckRules that fail on gaps; notes for decisions; run in CI
Keep currentReview date on every entry; retire with the code, or say why it stays

Where you are now

You can find AI use in code and at runtime, turn files into systems, describe each with the fields that set its risk, and check the inventory automatically. You've also seen what a scan misses. The next lesson takes the inventory's two riskiest fields, data_in and output_use, and looks at the data: what goes into prompts, what comes out, and what to keep out of both.

Check yourself

  1. Why does an AI inventory come before the other controls?

  2. Fernhill's marketing site matched the scan. Why isn't it an entry?

  3. Which system did the code scan miss entirely?

  4. Which field does most to set a system's risk?

  5. The support bot has a Kubernetes manifest. Is it deployed?

  6. A company scanned all its repositories and found no high-risk AI. What's the gap?

Key terms

  • AI inventory: a list of an organization's AI systems, with what each does, who owns it and what risk it carries.
  • AI system (in an inventory): something with one purpose and one owner, deployed and retired as a unit, however many files it has.
  • Signal: evidence in code or configuration that AI may be used, such as an import, an endpoint or a key name. A lead, not a finding.
  • Shadow AI: AI tools people use without the organization knowing, often signed up for individually.
  • Output use: what happens to a system's result; the main driver of its risk.
  • Use case inventory: the US federal term for an agency's public AI inventory.

Go deeper