mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 19:53:11 +08:00
- Consolidate common migrations into dedicated common app module - Remove asset search materialized view migration (0002) and simplify migration structure - Reorganize target detail page with new overview and settings sub-routes - Add target overview component displaying key asset information - Add target settings component for configuration management - Enhance scan history UI with improved data table and column definitions - Update scheduled scan dialog with better form handling - Refactor target service with improved API integration - Update scan hooks (use-scans, use-scheduled-scans) with better state management - Add internationalization strings for new target management features - Update Docker initialization and startup scripts for new app structure - Bump Django to 5.2.7 and update dependencies in requirements.txt - Add WeChat group contact information to README - Improve UI tabs component with better accessibility and styling
346 lines
31 KiB
Python
346 lines
31 KiB
Python
# Generated by Django 5.2.7 on 2026-01-06 00:55
|
||
|
||
import django.contrib.postgres.fields
|
||
import django.contrib.postgres.indexes
|
||
import django.core.validators
|
||
import django.db.models.deletion
|
||
from django.db import migrations, models
|
||
|
||
|
||
class Migration(migrations.Migration):
|
||
|
||
initial = True
|
||
|
||
dependencies = [
|
||
('scan', '0001_initial'),
|
||
('targets', '0001_initial'),
|
||
]
|
||
|
||
operations = [
|
||
migrations.CreateModel(
|
||
name='AssetStatistics',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('total_targets', models.IntegerField(default=0, help_text='目标总数')),
|
||
('total_subdomains', models.IntegerField(default=0, help_text='子域名总数')),
|
||
('total_ips', models.IntegerField(default=0, help_text='IP地址总数')),
|
||
('total_endpoints', models.IntegerField(default=0, help_text='端点总数')),
|
||
('total_websites', models.IntegerField(default=0, help_text='网站总数')),
|
||
('total_vulns', models.IntegerField(default=0, help_text='漏洞总数')),
|
||
('total_assets', models.IntegerField(default=0, help_text='总资产数(子域名+IP+端点+网站)')),
|
||
('prev_targets', models.IntegerField(default=0, help_text='上次目标总数')),
|
||
('prev_subdomains', models.IntegerField(default=0, help_text='上次子域名总数')),
|
||
('prev_ips', models.IntegerField(default=0, help_text='上次IP地址总数')),
|
||
('prev_endpoints', models.IntegerField(default=0, help_text='上次端点总数')),
|
||
('prev_websites', models.IntegerField(default=0, help_text='上次网站总数')),
|
||
('prev_vulns', models.IntegerField(default=0, help_text='上次漏洞总数')),
|
||
('prev_assets', models.IntegerField(default=0, help_text='上次总资产数')),
|
||
('updated_at', models.DateTimeField(auto_now=True, help_text='最后更新时间')),
|
||
],
|
||
options={
|
||
'verbose_name': '资产统计',
|
||
'verbose_name_plural': '资产统计',
|
||
'db_table': 'asset_statistics',
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='StatisticsHistory',
|
||
fields=[
|
||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||
('date', models.DateField(help_text='统计日期', unique=True)),
|
||
('total_targets', models.IntegerField(default=0, help_text='目标总数')),
|
||
('total_subdomains', models.IntegerField(default=0, help_text='子域名总数')),
|
||
('total_ips', models.IntegerField(default=0, help_text='IP地址总数')),
|
||
('total_endpoints', models.IntegerField(default=0, help_text='端点总数')),
|
||
('total_websites', models.IntegerField(default=0, help_text='网站总数')),
|
||
('total_vulns', models.IntegerField(default=0, help_text='漏洞总数')),
|
||
('total_assets', models.IntegerField(default=0, help_text='总资产数')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('updated_at', models.DateTimeField(auto_now=True, help_text='更新时间')),
|
||
],
|
||
options={
|
||
'verbose_name': '统计历史',
|
||
'verbose_name_plural': '统计历史',
|
||
'db_table': 'statistics_history',
|
||
'ordering': ['-date'],
|
||
'indexes': [models.Index(fields=['date'], name='statistics__date_1d29cd_idx')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='Directory',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.CharField(help_text='完整请求 URL', max_length=2000)),
|
||
('status', models.IntegerField(blank=True, help_text='HTTP 响应状态码', null=True)),
|
||
('content_length', models.BigIntegerField(blank=True, help_text='响应体字节大小(Content-Length 或实际长度)', null=True)),
|
||
('words', models.IntegerField(blank=True, help_text='响应体中单词数量(按空格分割)', null=True)),
|
||
('lines', models.IntegerField(blank=True, help_text='响应体行数(按换行符分割)', null=True)),
|
||
('content_type', models.CharField(blank=True, default='', help_text='响应头 Content-Type 值', max_length=200)),
|
||
('duration', models.BigIntegerField(blank=True, help_text='请求耗时(单位:纳秒)', null=True)),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('target', models.ForeignKey(help_text='所属的扫描目标', on_delete=django.db.models.deletion.CASCADE, related_name='directories', to='targets.target')),
|
||
],
|
||
options={
|
||
'verbose_name': '目录',
|
||
'verbose_name_plural': '目录',
|
||
'db_table': 'directory',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['-created_at'], name='directory_created_2cef03_idx'), models.Index(fields=['target'], name='directory_target__e310c8_idx'), models.Index(fields=['url'], name='directory_url_ba40cd_idx'), models.Index(fields=['status'], name='directory_status_40bbe6_idx'), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='directory_url_trgm_idx', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('target', 'url'), name='unique_directory_url_target')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='DirectorySnapshot',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.CharField(help_text='目录URL', max_length=2000)),
|
||
('status', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
|
||
('content_length', models.BigIntegerField(blank=True, help_text='内容长度', null=True)),
|
||
('words', models.IntegerField(blank=True, help_text='响应体中单词数量(按空格分割)', null=True)),
|
||
('lines', models.IntegerField(blank=True, help_text='响应体行数(按换行符分割)', null=True)),
|
||
('content_type', models.CharField(blank=True, default='', help_text='响应头 Content-Type 值', max_length=200)),
|
||
('duration', models.BigIntegerField(blank=True, help_text='请求耗时(单位:纳秒)', null=True)),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='directory_snapshots', to='scan.scan')),
|
||
],
|
||
options={
|
||
'verbose_name': '目录快照',
|
||
'verbose_name_plural': '目录快照',
|
||
'db_table': 'directory_snapshot',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['scan'], name='directory_s_scan_id_c45900_idx'), models.Index(fields=['url'], name='directory_s_url_b4b72b_idx'), models.Index(fields=['status'], name='directory_s_status_e9f57e_idx'), models.Index(fields=['content_type'], name='directory_s_content_45e864_idx'), models.Index(fields=['-created_at'], name='directory_s_created_eb9d27_idx'), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='dir_snap_url_trgm', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_directory_per_scan_snapshot')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='Endpoint',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.TextField(help_text='最终访问的完整URL')),
|
||
('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
|
||
('location', models.TextField(blank=True, default='', help_text='重定向地址(HTTP 3xx 响应头 Location)')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('title', models.TextField(blank=True, default='', help_text='网页标题(HTML <title> 标签内容)')),
|
||
('webserver', models.TextField(blank=True, default='', help_text='服务器类型(HTTP 响应头 Server 值)')),
|
||
('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
|
||
('content_type', models.TextField(blank=True, default='', help_text='响应类型(HTTP Content-Type 响应头)')),
|
||
('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈(服务器/框架/语言等)', size=None)),
|
||
('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
|
||
('content_length', models.IntegerField(blank=True, help_text='响应体大小(单位字节)', null=True)),
|
||
('vhost', models.BooleanField(blank=True, help_text='是否支持虚拟主机', null=True)),
|
||
('matched_gf_patterns', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='匹配的GF模式列表,用于识别敏感端点(如api, debug, config等)', size=None)),
|
||
('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
|
||
('target', models.ForeignKey(help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)', on_delete=django.db.models.deletion.CASCADE, related_name='endpoints', to='targets.target')),
|
||
],
|
||
options={
|
||
'verbose_name': '端点',
|
||
'verbose_name_plural': '端点',
|
||
'db_table': 'endpoint',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['-created_at'], name='endpoint_created_44fe9c_idx'), models.Index(fields=['target'], name='endpoint_target__7f9065_idx'), models.Index(fields=['url'], name='endpoint_url_30f66e_idx'), models.Index(fields=['host'], name='endpoint_host_5b4cc8_idx'), models.Index(fields=['status_code'], name='endpoint_status__5d4fdd_idx'), models.Index(fields=['title'], name='endpoint_title_29e26c_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='endpoint_tech_2bfa7c_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='endpoint_resp_headers_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='endpoint_url_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='endpoint_title_trgm_idx', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('url', 'target'), name='unique_endpoint_url_target')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='EndpointSnapshot',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.TextField(help_text='端点URL')),
|
||
('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
|
||
('title', models.TextField(blank=True, default='', help_text='页面标题')),
|
||
('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
|
||
('content_length', models.IntegerField(blank=True, help_text='内容长度', null=True)),
|
||
('location', models.TextField(blank=True, default='', help_text='重定向位置')),
|
||
('webserver', models.TextField(blank=True, default='', help_text='Web服务器')),
|
||
('content_type', models.TextField(blank=True, default='', help_text='内容类型')),
|
||
('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈', size=None)),
|
||
('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
|
||
('vhost', models.BooleanField(blank=True, help_text='虚拟主机标志', null=True)),
|
||
('matched_gf_patterns', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='匹配的GF模式列表', size=None)),
|
||
('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='endpoint_snapshots', to='scan.scan')),
|
||
],
|
||
options={
|
||
'verbose_name': '端点快照',
|
||
'verbose_name_plural': '端点快照',
|
||
'db_table': 'endpoint_snapshot',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['scan'], name='endpoint_sn_scan_id_6ac9a7_idx'), models.Index(fields=['url'], name='endpoint_sn_url_205160_idx'), models.Index(fields=['host'], name='endpoint_sn_host_577bfd_idx'), models.Index(fields=['title'], name='endpoint_sn_title_516a05_idx'), models.Index(fields=['status_code'], name='endpoint_sn_status__83efb0_idx'), models.Index(fields=['webserver'], name='endpoint_sn_webserv_66be83_idx'), models.Index(fields=['-created_at'], name='endpoint_sn_created_21fb5b_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='endpoint_sn_tech_0d0752_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='ep_snap_resp_hdr_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='ep_snap_url_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='ep_snap_title_trgm', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_endpoint_per_scan_snapshot')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='HostPortMapping',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('host', models.CharField(help_text='主机名(域名或IP)', max_length=1000)),
|
||
('ip', models.GenericIPAddressField(help_text='IP地址')),
|
||
('port', models.IntegerField(help_text='端口号(1-65535)', validators=[django.core.validators.MinValueValidator(1, message='端口号必须大于等于1'), django.core.validators.MaxValueValidator(65535, message='端口号必须小于等于65535')])),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('target', models.ForeignKey(help_text='所属的扫描目标', on_delete=django.db.models.deletion.CASCADE, related_name='host_port_mappings', to='targets.target')),
|
||
],
|
||
options={
|
||
'verbose_name': '主机端口映射',
|
||
'verbose_name_plural': '主机端口映射',
|
||
'db_table': 'host_port_mapping',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['target'], name='host_port_m_target__943e9b_idx'), models.Index(fields=['host'], name='host_port_m_host_f78363_idx'), models.Index(fields=['ip'], name='host_port_m_ip_2e6f02_idx'), models.Index(fields=['port'], name='host_port_m_port_9fb9ff_idx'), models.Index(fields=['host', 'ip'], name='host_port_m_host_3ce245_idx'), models.Index(fields=['-created_at'], name='host_port_m_created_11cd22_idx')],
|
||
'constraints': [models.UniqueConstraint(fields=('target', 'host', 'ip', 'port'), name='unique_target_host_ip_port')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='HostPortMappingSnapshot',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('host', models.CharField(help_text='主机名(域名或IP)', max_length=1000)),
|
||
('ip', models.GenericIPAddressField(help_text='IP地址')),
|
||
('port', models.IntegerField(help_text='端口号(1-65535)', validators=[django.core.validators.MinValueValidator(1, message='端口号必须大于等于1'), django.core.validators.MaxValueValidator(65535, message='端口号必须小于等于65535')])),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('scan', models.ForeignKey(help_text='所属的扫描任务(主关联)', on_delete=django.db.models.deletion.CASCADE, related_name='host_port_mapping_snapshots', to='scan.scan')),
|
||
],
|
||
options={
|
||
'verbose_name': '主机端口映射快照',
|
||
'verbose_name_plural': '主机端口映射快照',
|
||
'db_table': 'host_port_mapping_snapshot',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['scan'], name='host_port_m_scan_id_50ba0b_idx'), models.Index(fields=['host'], name='host_port_m_host_e99054_idx'), models.Index(fields=['ip'], name='host_port_m_ip_54818c_idx'), models.Index(fields=['port'], name='host_port_m_port_ed7b48_idx'), models.Index(fields=['host', 'ip'], name='host_port_m_host_8a463a_idx'), models.Index(fields=['scan', 'host'], name='host_port_m_scan_id_426fdb_idx'), models.Index(fields=['-created_at'], name='host_port_m_created_fb28b8_idx')],
|
||
'constraints': [models.UniqueConstraint(fields=('scan', 'host', 'ip', 'port'), name='unique_scan_host_ip_port_snapshot')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='Subdomain',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('name', models.CharField(help_text='子域名名称', max_length=1000)),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('target', models.ForeignKey(help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)', on_delete=django.db.models.deletion.CASCADE, related_name='subdomains', to='targets.target')),
|
||
],
|
||
options={
|
||
'verbose_name': '子域名',
|
||
'verbose_name_plural': '子域名',
|
||
'db_table': 'subdomain',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['-created_at'], name='subdomain_created_e187a8_idx'), models.Index(fields=['name', 'target'], name='subdomain_name_60e1d0_idx'), models.Index(fields=['target'], name='subdomain_target__e409f0_idx'), models.Index(fields=['name'], name='subdomain_name_d40ba7_idx'), django.contrib.postgres.indexes.GinIndex(fields=['name'], name='subdomain_name_trgm_idx', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('name', 'target'), name='unique_subdomain_name_target')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='SubdomainSnapshot',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('name', models.CharField(help_text='子域名名称', max_length=1000)),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_snapshots', to='scan.scan')),
|
||
],
|
||
options={
|
||
'verbose_name': '子域名快照',
|
||
'verbose_name_plural': '子域名快照',
|
||
'db_table': 'subdomain_snapshot',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['scan'], name='subdomain_s_scan_id_68c253_idx'), models.Index(fields=['name'], name='subdomain_s_name_2da42b_idx'), models.Index(fields=['-created_at'], name='subdomain_s_created_d2b48e_idx'), django.contrib.postgres.indexes.GinIndex(fields=['name'], name='subdomain_snap_name_trgm', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('scan', 'name'), name='unique_subdomain_per_scan_snapshot')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='Vulnerability',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.CharField(help_text='漏洞所在的URL', max_length=2000)),
|
||
('vuln_type', models.CharField(help_text='漏洞类型(如 xss, sqli)', max_length=100)),
|
||
('severity', models.CharField(choices=[('unknown', '未知'), ('info', '信息'), ('low', '低'), ('medium', '中'), ('high', '高'), ('critical', '危急')], default='unknown', help_text='严重性(未知/信息/低/中/高/危急)', max_length=20)),
|
||
('source', models.CharField(blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)', max_length=50)),
|
||
('cvss_score', models.DecimalField(blank=True, decimal_places=1, help_text='CVSS 评分(0.0-10.0)', max_digits=3, null=True)),
|
||
('description', models.TextField(blank=True, default='', help_text='漏洞描述')),
|
||
('raw_output', models.JSONField(blank=True, default=dict, help_text='工具原始输出')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('target', models.ForeignKey(help_text='所属的扫描目标', on_delete=django.db.models.deletion.CASCADE, related_name='vulnerabilities', to='targets.target')),
|
||
],
|
||
options={
|
||
'verbose_name': '漏洞',
|
||
'verbose_name_plural': '漏洞',
|
||
'db_table': 'vulnerability',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['target'], name='vulnerabili_target__755a02_idx'), models.Index(fields=['vuln_type'], name='vulnerabili_vuln_ty_3010cd_idx'), models.Index(fields=['severity'], name='vulnerabili_severit_1a798b_idx'), models.Index(fields=['source'], name='vulnerabili_source_7c7552_idx'), models.Index(fields=['url'], name='vulnerabili_url_4dcc4d_idx'), models.Index(fields=['-created_at'], name='vulnerabili_created_e25ff7_idx')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='VulnerabilitySnapshot',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.CharField(help_text='漏洞所在的URL', max_length=2000)),
|
||
('vuln_type', models.CharField(help_text='漏洞类型(如 xss, sqli)', max_length=100)),
|
||
('severity', models.CharField(choices=[('unknown', '未知'), ('info', '信息'), ('low', '低'), ('medium', '中'), ('high', '高'), ('critical', '危急')], default='unknown', help_text='严重性(未知/信息/低/中/高/危急)', max_length=20)),
|
||
('source', models.CharField(blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)', max_length=50)),
|
||
('cvss_score', models.DecimalField(blank=True, decimal_places=1, help_text='CVSS 评分(0.0-10.0)', max_digits=3, null=True)),
|
||
('description', models.TextField(blank=True, default='', help_text='漏洞描述')),
|
||
('raw_output', models.JSONField(blank=True, default=dict, help_text='工具原始输出')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='vulnerability_snapshots', to='scan.scan')),
|
||
],
|
||
options={
|
||
'verbose_name': '漏洞快照',
|
||
'verbose_name_plural': '漏洞快照',
|
||
'db_table': 'vulnerability_snapshot',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['scan'], name='vulnerabili_scan_id_7b81c9_idx'), models.Index(fields=['url'], name='vulnerabili_url_11a707_idx'), models.Index(fields=['vuln_type'], name='vulnerabili_vuln_ty_6b90ee_idx'), models.Index(fields=['severity'], name='vulnerabili_severit_4eae0d_idx'), models.Index(fields=['source'], name='vulnerabili_source_968b1f_idx'), models.Index(fields=['-created_at'], name='vulnerabili_created_53a12e_idx')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='WebSite',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.TextField(help_text='最终访问的完整URL')),
|
||
('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
|
||
('location', models.TextField(blank=True, default='', help_text='重定向地址(HTTP 3xx 响应头 Location)')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('title', models.TextField(blank=True, default='', help_text='网页标题(HTML <title> 标签内容)')),
|
||
('webserver', models.TextField(blank=True, default='', help_text='服务器类型(HTTP 响应头 Server 值)')),
|
||
('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
|
||
('content_type', models.TextField(blank=True, default='', help_text='响应类型(HTTP Content-Type 响应头)')),
|
||
('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈(服务器/框架/语言等)', size=None)),
|
||
('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
|
||
('content_length', models.IntegerField(blank=True, help_text='响应体大小(单位字节)', null=True)),
|
||
('vhost', models.BooleanField(blank=True, help_text='是否支持虚拟主机', null=True)),
|
||
('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
|
||
('target', models.ForeignKey(help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)', on_delete=django.db.models.deletion.CASCADE, related_name='websites', to='targets.target')),
|
||
],
|
||
options={
|
||
'verbose_name': '站点',
|
||
'verbose_name_plural': '站点',
|
||
'db_table': 'website',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['-created_at'], name='website_created_c9cfd2_idx'), models.Index(fields=['url'], name='website_url_b18883_idx'), models.Index(fields=['host'], name='website_host_996b50_idx'), models.Index(fields=['target'], name='website_target__2a353b_idx'), models.Index(fields=['title'], name='website_title_c2775b_idx'), models.Index(fields=['status_code'], name='website_status__51663d_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='website_tech_e3f0cb_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='website_resp_headers_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='website_url_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='website_title_trgm_idx', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('url', 'target'), name='unique_website_url_target')],
|
||
},
|
||
),
|
||
migrations.CreateModel(
|
||
name='WebsiteSnapshot',
|
||
fields=[
|
||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||
('url', models.TextField(help_text='站点URL')),
|
||
('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
|
||
('title', models.TextField(blank=True, default='', help_text='页面标题')),
|
||
('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
|
||
('content_length', models.BigIntegerField(blank=True, help_text='内容长度', null=True)),
|
||
('location', models.TextField(blank=True, default='', help_text='重定向位置')),
|
||
('webserver', models.TextField(blank=True, default='', help_text='Web服务器')),
|
||
('content_type', models.TextField(blank=True, default='', help_text='内容类型')),
|
||
('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈', size=None)),
|
||
('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
|
||
('vhost', models.BooleanField(blank=True, help_text='虚拟主机标志', null=True)),
|
||
('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
|
||
('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
|
||
('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='website_snapshots', to='scan.scan')),
|
||
],
|
||
options={
|
||
'verbose_name': '网站快照',
|
||
'verbose_name_plural': '网站快照',
|
||
'db_table': 'website_snapshot',
|
||
'ordering': ['-created_at'],
|
||
'indexes': [models.Index(fields=['scan'], name='website_sna_scan_id_26b6dc_idx'), models.Index(fields=['url'], name='website_sna_url_801a70_idx'), models.Index(fields=['host'], name='website_sna_host_348fe1_idx'), models.Index(fields=['title'], name='website_sna_title_b1a5ee_idx'), models.Index(fields=['-created_at'], name='website_sna_created_2c149a_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='website_sna_tech_3d6d2f_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='ws_snap_resp_hdr_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='ws_snap_url_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='ws_snap_title_trgm', opclasses=['gin_trgm_ops'])],
|
||
'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_website_per_scan_snapshot')],
|
||
},
|
||
),
|
||
]
|