Database schema discovery
Every scan maps your application's database schema from the ORM models and migrations in the repository. You get each table (or collection) with its columns, keys, indexes and relations, which columns hold sensitive data, and which endpoints read or write the table. Vulkro reads the code only: it never connects to a database.
The schema is the code-side twin of Salesforce objects and fields. It also feeds the data-flow map: when a flow touches a table, its data classes come from that table's classified columns.
Where you see it
vulkro scan --format json: the top-levelschemaobject (left out when no schema was found). The--format ndjsonsummary line carries the schema counts.- The local console: it serves the schema of a saved scan through its
local API (
GET /api/scans/<id>/schema, or?table=<id>for one table). - Vulkro Cloud: each code repository has a Database area with an overview, Tables, Columns (filterable by sensitivity) and a Relationships diagram of the tables and the keys between them. Every table and column opens its own record.
There is no separate vulkro schema command: the schema is part of every
scan.
What it reads
| Source | What Vulkro reads |
|---|---|
| Prisma | schema.prisma |
| Drizzle | pgTable / mysqlTable / sqliteTable, including column helpers imported from another file |
| Mongoose | schemas, including NestJS @Schema classes |
| TypeORM, MikroORM | entity classes |
| Sequelize | define, Model.init, sequelize-typescript, queryInterface migrations |
| Knex | migrations |
| SQL DDL | CREATE TABLE, ALTER TABLE ... ADD, CREATE [UNIQUE] INDEX, CREATE TYPE ... AS ENUM, and DROP TABLE / RENAME TO applied in migration order |
| Django | models |
| SQLAlchemy | declarative models, Table(...), SQLModel |
| Alembic | migrations |
| Rails | db/schema.rb and ActiveRecord migrations, plus encrypts / has_secure_password in models |
| JPA / Hibernate | @Entity classes in Java and Kotlin |
| GORM, ent | Go structs and ent schemas |
| EF Core | DbSet<T> and the entity classes |
| Laravel | migrations, plus $casts |
When a table appears both as a model and in migrations, the two views merge
into one entry. In a monorepo, two packages that each own a users table
stay separate: a table's id is <package dir>#<name>, or just <name> at
the repository root.
What each table carries
- Columns: name, database name, type, nullability, primary key, unique, default, enum values, and the table and column a foreign key points to.
- Sensitivity per column:
none,pii,health,financial,secretorcredential, matched per whole word against the same field-name catalogue the PII rules and the data-flow map use (never by substring). - Protection:
hashedorencrypted, when the code shows it. - Relations and indexes as declared in the models and migrations.
- Hints: the soft-delete column, the tenant column, the owner column, timestamps, and which columns are hashed or encrypted.
- Endpoints that read or write the table (
read,writeorread_write), with their method, path, handler and whether they need a sign-in.
A trimmed example of the JSON:
"schema": {
"tables": [{
"id": "apps/api#users",
"name": "users", "model": "User", "kind": "table",
"file": "apps/api/src/db/schema.ts", "line": 24,
"columns": [{
"name": "passwordHash", "db_name": "password_hash", "type": "text",
"primary_key": false, "unique": false,
"sensitivity": "credential", "protection": "hashed"
}],
"hints": { "tenant_column": "orgId", "hashed_columns": ["passwordHash"] },
"endpoints": [{ "method": "POST", "path": "/signup", "access": "write", "auth": "unprotected" }]
}],
"stats": { "tables": 75, "columns": 686, "sensitive_columns": 8 }
}
Schema rules
Four rules check the schema itself. Each one is explained in full by
vulkro explain <rule>.
| Rule | Severity | Fires when |
|---|---|---|
schema-plaintext-password-column (CWE-256) | High | A login password column is written by code, and no write site, assigned value or model hook hashes it. |
schema-plaintext-secret-column (CWE-312) | Medium | A token, API key or secret column is written raw, with no hash or encryption on any write site. |
schema-login-identifier-not-unique (CWE-287) | Medium | A table that stores a password has an email or username that no model or migration makes unique, and accounts are looked up by it. |
schema-tenant-column-missing (CWE-639) | Medium | Most tables carry a tenant column, but one holding sensitive data has neither one nor a key to a tenant-scoped table. |
Two schema facts add context to findings that already exist, instead of creating new ones. They never change a finding's severity or confidence:
- A missing-authentication finding whose endpoint reaches a table holding
credentials, secrets, financial or health data is marked
schema-sensitive-table-unauthenticated. - A mass-assignment finding whose endpoint writes role, admin, balance,
owner or tenant columns is marked
schema-privileged-columns.
Limits
- Vulkro reads models and migrations as code. It does not connect to a database, so a column added by hand in production and never written down in the repository is not seen.
- Sensitivity is a name-based classification. A column called
notesthat happens to hold health data is not flagged as health data.