Files
rengine/shared/models/target_seed.py
T
Yogesh OjhaandClaude Opus 5 c41f0c860e feat(targets): store the assets you already know and seed every scan with them
A target carries a list of host names, addresses and URLs an operator already
holds. Every scan of that target starts from them, so a list earned elsewhere
no longer has to be re-pasted or re-discovered.

Each pasted line is classified by its own shape and scope-checked against the
target; a refusal names the line and the reason. Seeds enter a run through the
asset_seed stage, which resolves the names it has no answer for and materialises
what they resolve to. Stages the engine has switched off still do not run.

Seeds no longer force a focused run: scope reads the new seed_only, which only
rescan, watch and connector runs set, so a seeded census scan still covers its
target. Seed count is a material run facet, so Compare states it before any
delta it would otherwise explain.

Two stages share the subdomains table for a run: discovery's clear no longer
drops rows asset_seed wrote, and the run's seeded hosts join its merge so the
imported source survives a name discovery also finds.

Closes #1538

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYZfaNjq3Ym8GPvWYuHisS
2026-09-13 07:21:10 +05:30

54 lines
1.5 KiB
Python

import uuid
from datetime import datetime
from pydantic import BaseModel
from pydantic import Field as PydanticField
from sqlmodel import Field, SQLModel, UniqueConstraint
from shared.definitions.rescan import MAX_RUN_ASSETS, SeedKind
from shared.utils.datetime import utc_now
MAX_SEED_VALUE_LEN = 2000
class TargetSeed(SQLModel, table=True):
__tablename__ = "target_seeds"
__table_args__ = (
UniqueConstraint("target_id", "value", name="uq_target_seed_value"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
target_id: uuid.UUID = Field(
foreign_key="targets.id", index=True, ondelete="CASCADE"
)
project_id: uuid.UUID = Field(foreign_key="projects.id", index=True)
kind: str = Field(default=SeedKind.HOST.value, max_length=16)
value: str = Field(max_length=MAX_SEED_VALUE_LEN)
created_at: datetime = Field(default_factory=utc_now)
class TargetSeedRead(BaseModel):
id: uuid.UUID
kind: str
value: str
created_at: datetime
class TargetSeedWrite(BaseModel):
"""Seed lines as typed or pasted. Each line is classified against the target."""
values: list[str] = PydanticField(default_factory=list, max_length=MAX_RUN_ASSETS)
replace: bool = False
class TargetSeedRejection(BaseModel):
value: str
reason: str
class TargetSeedResult(BaseModel):
total: int
added: int
removed: int
rejected: list[TargetSeedRejection] = PydanticField(default_factory=list)