Data inventory
- Lesson 10
- 50 min
- Module 4: Privacy and data governance
Lesson 10 of 18 · AI Security, Privacy and Responsible DesignData inventory
1. Map the system and its risks
2. Prompt injection and untrusted content
3. Secure tools and external access
4. Privacy and data governance
5. Responsible behaviour and human oversight
6. Test, monitor and respond
What you will be able to do
Before you can protect, shorten or delete a customer’s data, you must know where every copy of it is. This lesson builds that list for one customer of Larkfield.
Before you startWhat to know and to have ready
What you need to know
- The data-flow map and the trust boundary of the assistant, from Larkfield’s boundary, as a file.
- The audit log,
redact()and the rule “no message text in the log”, from A useful, non-sensitive audit event. - Tenants, memberships and roles, from the authentication course’s Users and organizations.
What you need on your computer
- The project at the end of Module 3, with its virtual environment active.
python -m pytestprints41 passed. If your project differs, copy the snapshotend-of-m03from the labs repository and start from there. - The labs repository
nextia-learning-labsnext to the project folder.
Tested with Python 3.12.13 on macOS. The PowerShell commands for Windows were not tested.
Moving house
Section titled “Moving house”You move to a new flat. You want your post to stop going to the old address. So you make a list: the bank, the doctor, the gym, the phone company, a magazine. You tell each one. Three months later, a letter still arrives at the old address. It is from a shop where you once bought a sofa. You forgot that they had your address.
You cannot change or remove what you have not listed. The shop’s copy was not hidden. It was not on your list.
The problem: “delete my data” is not one delete
Section titled “The problem: “delete my data” is not one delete”Priti Shah is a Larkfield customer, C-50533. Like all the data in this course, she is made up. Her cedar planter arrived with a cracked side panel, and she wrote three tickets about it. Later she writes again: “Please tell me what you keep about me, and delete it.”
Amira’s first idea is the customers table. Priti has one row there: her name, email, phone and city. But the assistant has already handled Priti’s three tickets. Here are the audit log’s events after three runs with the weak start design. This is our run, shortened. You make the same three runs later in this lesson, in Measure: the weak start keeps more:
usr-sam assistant run {'case': 'ATK-15', 'customer': 'C-50533', 'message': 'Damage report for my planter attached (order LK-640436).', ...}usr-sam send_email executed {'order_id': None}usr-sam assistant run {'case': 'TASK-24', 'customer': 'C-50533', 'message': 'How long do I have to report a damaged item?', ...}usr-sam assistant run {'case': 'TASK-02', 'customer': 'C-50533', 'message': "My cedar planter (order LK-640436) came with one side panel cracked right through. The box was crushed. ...", ...}usr-sam create_return_label executed {'order_id': 'LK-640436'}usr-sam send_email executed {'order_id': None}Priti’s own words are in the log. They are also in the tickets, in the stored conversations, and in every prompt that went to the model provider. If Amira deletes the customer row, most of Priti’s data stays. The threat model of Module 1 named this risk T8. It says: “The log and the stored conversations keep every message and contact detail forever.” This module fixes it, and the fix starts with a list.
The idea: a data inventory
Section titled “The idea: a data inventory”Personal data is any information about a person who can be identified. A name and an email address identify Priti directly. Her customer ID, C-50533, identifies her through a link: anyone with the database can look her up. Security people often say PII, personally identifiable information, for the same idea.
A data inventory is the list of every place where personal data lives. Like your moving-house list, it is only useful if it is complete. For each place, it answers five questions:
- What data is there, and how many copies?
- Why is it there? This is its purpose. A copy with no purpose should not exist.
- Who processes it for the shop? A processor is another company that handles the data on the shop’s behalf. The model provider is one, because every prompt goes to it.
- Where is it? In the app’s database, in a file area, at a provider, or in a backup.
- Who can read it, and in which role: Larkfield’s members, the security team, operations or the provider.
The inventory also says, for each copy, how it can be retired: deleted, anonymized, kept on purpose, or out of the app’s reach. The next lesson acts on that column.
A small example: one customer, every value
Section titled “A small example: one customer, every value”Start with a fresh practice database, before the assistant has run. Priti’s data is in five places:
| Place | Copies | Holds message text or contact details |
|---|---|---|
| customer record | 1 | yes: name, email, phone, city |
| orders | 1 | no: items and totals, linked by her ID |
| tickets | 3 | yes: her three messages |
| attached files | 2 | yes: two damage reports |
| backups | 1 | yes: a copy of the whole database |
That is 8 copies in 5 places, and 7 hold content. Now the assistant runs once on each of her three tickets. ATK-15 is a damage report with a planted comment, TASK-24 a question about deadlines, TASK-02 the cracked planter. Each run makes new copies. The picture shows where they go.
Text description of the diagram
Priti Shah (customer C-50533) writes three tickets about one cedar planter. Her data then lives in two areas.
Inside Larkfield’s support app, a delete in the app can reach five places:
- Customer record, 1 copy: name, email, phone, city. Why: to contact her. Who reads it: Larkfield’s members.
- Tickets, 3, and attached files, 2: her messages and damage reports. Why: to answer her. Who reads them: Larkfield’s members and the assistant’s tools.
- Orders, 1: items and totals. Why: the shop’s accounts. Who reads it: Larkfield’s members and the get_order tool.
- Stored runs, 3, with full storage: her message, the reply draft, her name and email. Why: to look back at a run. Who reads them: anyone with database access.
- Audit log, 3 events: who did what and when, IDs only, no message text. Why: security. Who reads it: the security team.
A dashed box notes what these three runs did not create: approvals, emails to her address, refunds and return labels. Another run can add them.
Outside the app, a delete in the app cannot reach two places:
- Model provider, 3 runs: the prompt with her messages, in each of 14 model calls. Why: to write the reply draft. Who reads it: the provider, under its terms.
- Backups, 1: a copy of the whole database. Why: recovery after a failure. Who reads it: operations.
After the three runs: 17 copies in 8 places, 13 with her message text or contact details.
Three runs added nine copies in three new places: three stored runs, three audit events and three prompts at the model provider. People forget the model provider most often. In these three runs, the assistant made 14 model calls, and each call sent Priti’s message again.
In your practice runs, the mock replays recorded decisions, so nothing leaves your computer. The inventory counts the copy that a live run sends. The recordings themselves were real calls to Azure with this made-up data, so those prompts did go to a provider once.
The inventory in code
Section titled “The inventory in code”The project gets a small module for this, support_assistant/inventory.py. Its function copies() looks in every place that it knows, for one customer, and adds one row per place:
add("database: tickets", "the customer's messages", len(tickets), True, "deleted")...add("audit log", "who did what, when" + (", with the message text" if raw else ", no message text"), len(events), raw, "kept: append-only")add("model provider", "the prompts sent in each run, outside this system", len(runs), True, "not reached")...add("backups", "a copy of the database, kept for recovery", 1, True, "not reached")Each row has five values: where, what, how many, whether it holds content, and what a delete does there. A copy holds content when it has the customer’s message text or contact details. A copy without content can still point to her through an ID.
Two places are outside the database, and the code says so honestly. The model provider gets one copy per run. The code counts the runs from the audit log, because it cannot look inside the provider. The backup is one copy of the whole database, whatever is in it today.
The inventory command prints the rows. It is a teaching tool, not a legal record: it finds only the places that it knows about.
Guided practice: add the inventory
Section titled “Guided practice: add the inventory”-
Copy the new module and its test from the snapshot. On macOS or Linux:
Terminal window cp ../nextia-learning-labs/C24/snapshots/end-of-m04/support_assistant/inventory.py support_assistant/cp ../nextia-learning-labs/C24/snapshots/end-of-m04/tests/test_inventory.py tests/On Windows, in PowerShell:
Terminal window Copy-Item ..\nextia-learning-labs\C24\snapshots\end-of-m04\support_assistant\inventory.py support_assistant\Copy-Item ..\nextia-learning-labs\C24\snapshots\end-of-m04\tests\test_inventory.py tests\inventory.pyalso has a functioncompare(). It needs code from the next lesson, so do not call it yet. -
Open
support_assistant/__main__.py. Above the linedef cmd_changes(args):, add the command:def cmd_inventory(args):from .inventory import summarys = summary(World(), args.customer)print(f"Customer {s['customer']}: {s['total_copies']} copies in {s['places']} places "f"({s['content_copies']} with message text or contact details)")for r in s["rows"]:print(f" {r['where']}: {r['count']} ({r['what']}) -> {r['reach']}")if s["not_reached_by_a_delete"]:print("A delete in this app cannot reach: " + ", ".join(s["not_reached_by_a_delete"])) -
In the same file, above the line that adds the
changescommand, add:p = sub.add_parser("inventory"); p.add_argument("customer"); p.set_defaults(fn=cmd_inventory) -
Run the tests:
Terminal window python -m pytestIt prints
44 passed: the 41 tests of Module 3 and 3 new ones. -
Make the practice database fresh, and list Priti’s data:
Terminal window python -m support_assistant resetpython -m support_assistant inventory C-50533Customer C-50533: 8 copies in 5 places (7 with message text or contact details)database: customers: 1 (name, e-mail, phone, city) -> anonymizeddatabase: orders: 1 (items and totals) -> kept for the accountsdatabase: tickets: 3 (the customer's messages) -> deletedfiles: attachments: 2 (files the customer attached) -> deletedbackups: 1 (a copy of the database, kept for recovery) -> not reachedA delete in this app cannot reach: backups -
Run the assistant on Priti’s three tickets with the
securedesign:Terminal window python -m support_assistant run ATK-15 --design securepython -m support_assistant run TASK-24 --design securepython -m support_assistant run TASK-02 --design secure -
List her data again:
Terminal window python -m support_assistant inventory C-50533Customer C-50533: 17 copies in 8 places (13 with message text or contact details)database: customers: 1 (name, e-mail, phone, city) -> anonymizeddatabase: orders: 1 (items and totals) -> kept for the accountsdatabase: tickets: 3 (the customer's messages) -> deleteddatabase: conversations: 3 (stored runs with the message, the draft, the name and e-mail) -> deletedaudit log: 3 (who did what, when, no message text) -> kept: append-onlymodel provider: 3 (the prompts sent in each run, outside this system) -> not reachedfiles: attachments: 2 (files the customer attached) -> deletedbackups: 1 (a copy of the database, kept for recovery) -> not reachedA delete in this app cannot reach: model provider, backups -
Look at the stored runs. Save this code as
conversation_show.pyin the project folder, and runpython conversation_show.py:conversation_show.py from support_assistant.systems import Worldfor row in World().conversations("C-50533"):print({k: v for k, v in row.items() if k not in ("run_id", "tenant")})The first line starts like this:
{'case_id': 'ATK-15', 'ticket_id': 'T-24113', 'customer_id': 'C-50533', 'customer_name': 'Priti Shah', 'customer_email': 'priti.shah@mail.example', 'request': 'Please help me handle ticket T-24113.', 'ticket_text': 'Damage report for my planter attached (order LK-640436).', 'answer': 'The customer reports a small scratch ...
The secure design keeps the audit log clean, as Module 3 made it. But it still stores every conversation in full, with her name and email, and with no end date. The next lesson closes that gap.
Measure: the weak start keeps more
Section titled “Measure: the weak start keeps more”Now do the same three runs with the weak start design, on a fresh practice database:
python -m support_assistant resetpython -m support_assistant run ATK-15 --design startpython -m support_assistant run TASK-24 --design startpython -m support_assistant run TASK-02 --design startpython -m support_assistant inventory C-50533TASK-02 prints a note: one recorded decision was made for a slightly different request. The ATK-15 run sent an email first, so your database differs from the recording. The mock replayed the decision and checked it against your state.
- all copies
- copies with message text or contact details
Text description of the chart
A column chart with three pairs of columns. Fresh database: 8 copies, 7 with content. After three runs with the secure design: 17 copies, 13 with content. After the same three runs with the weak start design: 21 copies, 19 with content.
The weak start has 21 copies in 9 places, and 19 hold content. Two differences explain most of it. Its audit log has 6 events, and each run event holds Priti’s message, because the log is raw. It also created a return label at once, which adds a row in return_labels.
The two emails that it sent are the surprise. Print the emails table. Save this code as emails_show.py in the project folder, and run python emails_show.py:
from support_assistant.systems import World
for row in World().changes()["emails"]: print(row["email_id"], "|", row["to_address"], "|", row["subject"])SE-0001 | customer email not provided | We’re sorry about your cedar planterSE-0002 | customer for order LK-640436 | Your damaged cedar planterBoth emails are about Priti’s planter. Neither went to her address, so the inventory does not list them: it looks for emails by her address. An inventory finds only the copies that carry a key it knows.
These counts depend on what the model did, so they are a snapshot of one model, one prompt and one date. Another model would make other copies. Models change fast; the method lasts.
The inventory as a table
Section titled “The inventory as a table”The command prints counts. A team also needs the reasons. Kwame asks Amira for one table that the whole team can read. This is the privacy-oriented data-flow inventory of the support assistant:
| Data | Where | Purpose | Processor | Who can read it | How it is retired |
|---|---|---|---|---|---|
| Name, email, phone, city | customers table |
contact the customer | the host | Larkfield’s members | anonymized on request |
| Messages, attached files | tickets table, file area |
answer the customer | the host | members, the assistant’s tools | deleted on request |
| Orders, refunds, labels | database tables | the shop’s accounts | the host | members, get_order |
kept: how long is a legal question |
| Stored runs | conversations table |
look back at a run | the host | anyone with database access | the next lesson: less content, an end date |
| Audit events | audit_log table |
security: who did what | the host | the security team | kept, append-only, no content |
| Prompts | model provider | write the reply draft | the model provider | the provider, under its terms | not reachable from the app |
| Everything | backups | recovery | the backup storage | operations | when the backup expires |
The “Processor” column is where people forget the model provider. It is a company outside Larkfield, and it receives the customer’s words in every call. The third lesson of this module asks what the contract with it says.
When to make an inventory, and what it costs
Section titled “When to make an inventory, and what it costs”Make it before the first real customer. Update it whenever the system gets a new place to write. A new table, tool, cache, search index or provider is a new row. In RAG: Building AI That Uses Your Data, the vector index holds copies of documents. If it indexed tickets, it would be a row here too.
You can skip the detail for data that identifies nobody, such as Larkfield’s policy documents. They are not personal data.
The costs are real:
- It goes out of date. The code of
copies()lists places by hand, so a developer who adds a table must add a row.test_inventory.pychecks the known places, not the unknown ones. - It finds copies by their keys. A copy without the customer’s ID or address, like the two emails above, is invisible to it.
- It counts; it does not judge. “Kept for the accounts” is a decision that the shop must justify. The inventory only makes it visible.
Common mistake
The inventory says 21 copies, but there are more
Symptom: with the weak start design, the inventory lists 21 copies of Priti’s data. But the emails table holds two more emails about her damaged planter.
Cause: the inventory finds emails by the customer’s address. The model wrote the emails to “customer email not provided” and “customer for order LK-640436”. The copies exist, but no key links them to her.
Diagnose: search for her data by content, not only by ID. Print the emails table, and look for her order number, her product or her words. The audit log helps too: each send_email event has the run ID, and the run event names the customer.
Fix: stop the copy at the source. Module 2’s control sends an email only to the address on file, so under secure every email carries her address. Then make each write record the customer that it belongs to. In the next lesson, you test a delete by searching every table for her words.
Your turn: another customer
Section titled “Your turn: another customer”Run TASK-05 with the secure design on a fresh practice database. Its order, LK-640218, belongs to the customer C-50311. Before you run the inventory for that customer, predict: which place appears that Priti did not have, and why?
Show the answer
python -m support_assistant resetpython -m support_assistant run TASK-05 --design securepython -m support_assistant inventory C-50311Customer C-50311: 11 copies in 8 places (5 with message text or contact details) database: customers: 1 (name, e-mail, phone, city) -> anonymized database: orders: 1 (items and totals) -> kept for the accounts database: tickets: 1 (the customer's messages) -> deleted database: conversations: 1 (stored runs with the message, the draft, the name and e-mail) -> deleted database: approvals: 1 (proposals and their evidence) -> deleted audit log: 4 (who did what, when, no message text) -> kept: append-only model provider: 1 (the prompts sent in each run, outside this system) -> not reached backups: 1 (a copy of the database, kept for recovery) -> not reachedA delete in this app cannot reach: model provider, backupsThe new place is approvals. The assistant proposed a return label, and the proposal waits for a person as AP-0001. Its evidence names the customer’s ticket. The audit log has 4 events: the run, the proposed label, the refused refund and the queued approval. Add a row to your inventory table: the purpose is “a person approves a change”, and the readers are the owners and staff who approve.
Knowledge check
Section titled “Knowledge check”Check your understanding
After three runs with the secure design, the inventory shows 3 copies at the model provider. In your practice run, nothing left your computer. Why does the inventory still count them?
Show the answer
The inventory describes the system as it runs for real. In a live run, every model call sends the prompt, with the customer’s message, to the provider. The mock only replays recorded decisions for the course. The provider is a processor outside the app, so the inventory must show it, even when a practice run sends nothing.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Name the five questions that a data inventory answers for each place. Then answer them for the stored runs (the conversations table) with full storage.
Show the answer
What, why, who processes it, where, and who can read it. For the stored runs, what: the customer’s message, the reply draft, her name and email (3 copies for Priti). Why: to look back at what the assistant did. Who processes it: the host of the app. Where: the conversations table in the database. Who can read it: anyone with database access. A delete in the app removes them, but today they never expire.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Tomás adds a cache that keeps the last 100 reply drafts, for speed. What must change in the inventory, and why?
Show the answer
The cache is a new place that holds content: the drafts quote customers’ messages and names. It needs a new row with its purpose (speed), its processor, who can read it and how it is retired. copies() must look there too, or a delete will miss it. If the cache cannot be searched by customer, it needs a short life, so that its copies expire on their own.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- Personal data is information about a person who can be identified, directly or through an ID. PII is the same idea.
- A data inventory lists every place, with what, why (purpose), who processes it (processor), where, and who can read it (role). It also says how each copy is retired.
- Priti’s data: 8 copies in 5 places in a fresh database. After three runs: 17 in 8 with
secure, 21 in 9 with the weak start. - The model provider and the backups hold copies that a delete in the app cannot reach.
- An inventory finds copies by their keys. A copy without a key, like an email to a made-up address, is invisible to it.
- The project has the
inventorycommand:44 passed.