You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
974 lines
34 KiB
974 lines
34 KiB
const pageSize = 10;
|
|
let ipAssets = [], currentIpPage = 1;
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
// 初始加载
|
|
loadIpAssets();
|
|
loadUrlAssets();
|
|
});
|
|
|
|
// 页面卸载时断开连接
|
|
window.addEventListener("beforeunload", function() {
|
|
ipAssets = [];
|
|
urlAssets = [];
|
|
currentIpPage = 1;
|
|
});
|
|
|
|
|
|
|
|
//---------------------------IP-Assets Tab----------------------------
|
|
async function exportIpAssets(){
|
|
// 构造 CSV 文本
|
|
let rows = [
|
|
['序号', '资产IP', '所属用户', '风险', '最新检测时间','端口数','关联域名'].map(fmtCell).join(',')
|
|
];
|
|
ipAssets.forEach((row, i) => {
|
|
rows.push([
|
|
(i + 1).toString(),
|
|
row[0].toString(),
|
|
row[1] || '',
|
|
row[2] || '',
|
|
row[3] || '',
|
|
row[4] || '',
|
|
row[5] || '',
|
|
].map(fmtCell).join(','));
|
|
});
|
|
|
|
const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM
|
|
|
|
downloadCSV(csv, `ip_assets.csv`);
|
|
}
|
|
|
|
async function loadIpAssets(page=1) {
|
|
const IP = document.getElementById("ipFilter").value.trim();
|
|
const user = document.getElementById("userFilter").value.trim();
|
|
const safe_rank = document.getElementById("riskFilter").value;
|
|
try {
|
|
const data = await postJSON("/api/assets/getipassets",{IP,user,safe_rank})
|
|
ipAssets = data.ip_assets || [];
|
|
renderIpTable(page); //刷新表格
|
|
} catch (error) {
|
|
console.error("查询任务记录出错:", error);
|
|
alert("查询失败!");
|
|
}
|
|
}
|
|
|
|
//刷新IP-assets表格
|
|
function renderIpTable(page) {
|
|
currentIpPage = page; //查询数据了,从第一页显示
|
|
const start = (currentIpPage-1)*pageSize;
|
|
const slice = ipAssets.slice(start, start+pageSize);
|
|
const tbody = document.querySelector('#ipTable tbody');
|
|
tbody.innerHTML = '';
|
|
for (let i=0; i<slice.length; i++) {
|
|
const item = slice[i];
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+i+1}</td>
|
|
<td>${item[0]}</td>
|
|
<td>${item[1]}</td>
|
|
<td>${item[2]}</td>
|
|
<td>${item[3]}</td>
|
|
<td>${item[4]}</td>
|
|
<td>${item[5]}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-info asset-op-btn" onclick="showBasicInfo('${item[0]}','${item[1]}','${item[2]}','${item[3]}')">基本信息</button>
|
|
<button class="btn btn-sm btn-info asset-op-btn" data-ip="${item[0]}" onclick="showPortData('${item[0]}')">端口数据</button>
|
|
<button class="btn btn-sm btn-info asset-op-btn" onclick="showVulData('${item[0]}')">漏洞数据</button>
|
|
<button class="btn btn-sm btn-info asset-op-btn" onclick="showRelatedDomain('${item[0]}')">关联域名</button>
|
|
<button class="btn btn-sm btn-danger asset-op-btn" onclick="confirmDeleteIp('${item[0]}')">删除</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
}
|
|
// 补足空行
|
|
for (let i=slice.length; i<pageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="9"> </td>';
|
|
tbody.appendChild(tr);
|
|
}
|
|
// 更新分页
|
|
document.getElementById('ipPrev').dataset.page = currentIpPage>1?currentIpPage-1:1;
|
|
document.getElementById('ipNext').dataset.page = ipAssets.length>currentIpPage*pageSize?currentIpPage+1:currentIpPage;
|
|
}
|
|
|
|
// 分页按钮
|
|
document.getElementById('ipPrev').addEventListener('click', e => { e.preventDefault(); renderIpTable(+e.target.dataset.page); });
|
|
document.getElementById('ipNext').addEventListener('click', e => { e.preventDefault(); renderIpTable(+e.target.dataset.page); });
|
|
// 查询
|
|
document.getElementById('ipSearchBtn').addEventListener('click', () => loadIpAssets(1));
|
|
//导出
|
|
document.getElementById("ipExportBtn").addEventListener('click',() => exportIpAssets())
|
|
|
|
//*********基本信息modal***********
|
|
const ipModalEl = document.getElementById('ipBasicInfoModal');
|
|
const ownerDrawerEl = document.getElementById('ownerDrawer');
|
|
const infoIpEl = document.getElementById('info_ip');
|
|
const infoRiskEl = document.getElementById('info_risk');
|
|
const infoOwnerEl = document.getElementById('info_owner');
|
|
const infoScanTimeEl = document.getElementById('info_scanTime');
|
|
const contactNameEl = document.getElementById('contactName');
|
|
const contactPhoneEl = document.getElementById('contactPhone');
|
|
const btnChooseOwner = document.getElementById('btnChooseOwner');
|
|
const btnSaveIpInfo = document.getElementById('saveIpInfo');
|
|
const ownerSearchEl = document.getElementById('ownerSearchKeyword');
|
|
const btnSearchOwner = document.getElementById('btnSearchOwner');
|
|
const btnDelOwner = document.getElementById("btnDelOwner");
|
|
const ownerTableBody = document.getElementById('ownerTableBody');
|
|
const ipinofModal = new bootstrap.Modal(ipModalEl);
|
|
const ownerDrawer = new bootstrap.Offcanvas(ownerDrawerEl);
|
|
let selectedOwnerId = null; // 当前选定的 user_id
|
|
let owner_list = null; // 资产所属单位列表
|
|
let curModal; //选择所属用户界面的共享参数,用于区分时IP窗口进入,还是URL窗口进入
|
|
// 5. Drawer 操作
|
|
btnChooseOwner.addEventListener('click', () => {
|
|
curModal = "ip"
|
|
ownerDrawer.show();
|
|
loadOwners(ownerSearchEl.value.trim());
|
|
});
|
|
|
|
//删除所属用户的关系
|
|
btnDelOwner.addEventListener('click',() =>{
|
|
selectedOwnerId = null;
|
|
infoOwnerEl.textContent = '';
|
|
contactNameEl.textContent = '';
|
|
contactPhoneEl.textContent = '';
|
|
});
|
|
|
|
//选择一个所属用户
|
|
ownerTableBody.addEventListener('click', e => {
|
|
const btn = e.target.closest('button[data-id]');
|
|
if (!btn) return;
|
|
selectedOwnerId = btn.dataset.id;
|
|
if(curModal === "ip"){
|
|
infoOwnerEl.textContent = btn.dataset.name;
|
|
contactNameEl.textContent = btn.dataset.telluname;
|
|
contactPhoneEl.textContent = btn.dataset.tellnum;
|
|
}else{
|
|
ownerEl.textContent = btn.dataset.name;
|
|
contactEl.textContent = btn.dataset.telluname;
|
|
tellEl.textContent = btn.dataset.tellnum;
|
|
}
|
|
ownerDrawer.hide();
|
|
});
|
|
|
|
//5.1--搜索按钮
|
|
btnSearchOwner.addEventListener('click', () => loadOwners(ownerSearchEl.value.trim()));
|
|
|
|
async function loadOwners(keyword='') {
|
|
try {
|
|
const res = await fetch('/api/assets/getassetsuser', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ keyword })
|
|
});
|
|
owner_list = (await res.json()).user_list;
|
|
ownerTableBody.innerHTML = '';
|
|
//ID,uname,tellnum,tell_username
|
|
owner_list.forEach((u, idx) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${idx + 1}</td>
|
|
<td>${u[1]}</td>
|
|
<td><button class="btn btn-link p-0" data-id="${u[0]}" data-name="${u[1]}"
|
|
data-tellnum="${u[2]}" data-telluname="${u[3]}">选择</button></td>`;
|
|
ownerTableBody.appendChild(tr);
|
|
});
|
|
} catch (e) { showToast('加载用户列表失败', 'danger'); }
|
|
}
|
|
|
|
// 6. 保存修改
|
|
btnSaveIpInfo.addEventListener('click', async () => saveIpInfo());
|
|
|
|
// 7. hidden 清理
|
|
ipModalEl.addEventListener('hidden.bs.modal', () => {
|
|
selectedOwnerId = null;
|
|
document.getElementById('ipInfoForm').reset();
|
|
['info_ip','info_risk','info_owner','info_scanTime'].forEach(id => {
|
|
document.getElementById(id).textContent = '';
|
|
});
|
|
ownerDrawer.hide();
|
|
});
|
|
|
|
//显示基本信息
|
|
async function showBasicInfo(IP,owner,risk_rank,update_time) {
|
|
try {
|
|
infoIpEl.textContent = IP;
|
|
infoRiskEl.textContent = risk_rank;
|
|
infoOwnerEl.textContent = owner;
|
|
infoScanTimeEl.textContent = update_time;
|
|
if(owner == 'null'){
|
|
contactNameEl.textContent = '';
|
|
contactPhoneEl.textContent = '';
|
|
}
|
|
else { //au.ID,au.tellnum,au.tell_username
|
|
try{
|
|
const data = await postJSON('/api/assets/getipinfo', {IP});
|
|
ip_info = data.ip_info;
|
|
} catch (error) {
|
|
ip_info = []
|
|
console.error("获取IP资产基本信息失败!",error);
|
|
}
|
|
contactNameEl.textContent = ip_info[2] || '';
|
|
contactPhoneEl.textContent = ip_info[1] || '';
|
|
selectedOwnerId = ip_info[0] || 0;
|
|
}
|
|
ipinofModal.show();
|
|
} catch (e) {
|
|
showToast(`请求基本信息失败:${e.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
//保存所属用户的基本信息
|
|
async function saveIpInfo(){
|
|
const payload = {
|
|
cur_ip: infoIpEl.textContent,
|
|
owner_id: selectedOwnerId
|
|
};
|
|
try {
|
|
const ret = await postJSON('/api/assets/updateipinfo', payload);
|
|
bsuccess = ret.bsuccess;
|
|
if (bsuccess) {
|
|
showToast('修改成功', 'success');
|
|
//bootstrap.Modal.getInstance(ipModalEl).hide();
|
|
loadIpAssets(currentIpPage) //刷新整个数据。。
|
|
} else {
|
|
showToast(ret.message || '修改失败', 'danger');
|
|
}
|
|
} catch (e) {
|
|
showToast(`提交失败:${e.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
//***********端口数据**************
|
|
const latestTableBody = document.querySelector('#latestPortTable tbody');
|
|
const latestPrevBtn = document.getElementById('latestPrev');
|
|
const latestNextBtn = document.getElementById('latestNext');
|
|
|
|
const historyTableHead = document.querySelector('#historyPortTable thead');
|
|
const historyTableBody = document.querySelector('#historyPortTable tbody');
|
|
const hisPortPrevBtn = document.getElementById('hisPortPrev');
|
|
const hisPortNextBtn = document.getElementById('hisPortNext');
|
|
|
|
const portModalEl = document.getElementById('portDataModal');
|
|
const portModal = new bootstrap.Modal(portModalEl);
|
|
let latestData = [], historyData = {};
|
|
let latestPage = 1, latestPageSize = 10;
|
|
|
|
async function showPortData(IP){
|
|
// 把 IP 暂存到 modal 元素上
|
|
portModalEl.dataset.ip = IP;
|
|
portModal.show()
|
|
}
|
|
|
|
// 打开 Modal 时加载
|
|
portModalEl.addEventListener('show.bs.modal', async ev => {
|
|
const ip = portModalEl.dataset.ip;
|
|
if (!ip) return;
|
|
|
|
// 1. 拉最新数据
|
|
try{
|
|
const latestJson = await postJSON('/api/assets/getportlatest', {ip});
|
|
latestData = latestJson.port_data || [];
|
|
} catch (error) {
|
|
console.error("拉取最新的端口数据失败!",error)
|
|
}
|
|
renderLatest();
|
|
|
|
// 2. 拉历史数据
|
|
try{
|
|
const histJson = await postJSON('/api/assets/getporthistory', {ip});
|
|
historyData = histJson || [];
|
|
} catch (error) {
|
|
console.error("拉取历史的端口数据失败!",error)
|
|
}
|
|
renderHistory();
|
|
});
|
|
|
|
// 渲染最新数据和分页
|
|
function renderLatest() {
|
|
latestTableBody.innerHTML = '';
|
|
const start = (latestPage-1)*latestPageSize;
|
|
const slice = latestData.slice(start, start+latestPageSize);
|
|
slice.forEach((row,i) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+i+1}</td>
|
|
<td>${row[0]}</td>
|
|
<td>${row[1]}</td>
|
|
<td>${row[2]}</td>
|
|
<td>${row[3]}</td>
|
|
`;
|
|
latestTableBody.appendChild(tr);
|
|
});
|
|
// 补空行
|
|
for (let i = slice.length; i < latestPageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="5"> </td>';
|
|
latestTableBody.appendChild(tr);
|
|
}
|
|
// 更新按钮
|
|
latestPrevBtn.dataset.page = latestPage>1?latestPage-1:1;
|
|
latestNextBtn.dataset.page = latestData.length>latestPage*latestPageSize?latestPage+1:latestPage;
|
|
}
|
|
latestPrevBtn.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
latestPage = +e.target.dataset.page;
|
|
renderLatest();
|
|
});
|
|
latestNextBtn.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
latestPage = +e.target.dataset.page;
|
|
renderLatest();
|
|
});
|
|
|
|
// 渲染历史数据
|
|
function renderHistory() {
|
|
historyTableHead.innerHTML = '';
|
|
historyTableBody.innerHTML = '';
|
|
|
|
// 1) 表头:批次时间
|
|
const th = document.createElement('tr');
|
|
th.innerHTML = `<th style="width:80px">端口 / 时间</th>` +
|
|
historyData.times.map(t => `<th style="width:100px">${t}</th>`).join('');
|
|
historyTableHead.appendChild(th);
|
|
imaxcount = historyData.times.length;
|
|
|
|
// 2) 每个端口一行
|
|
Object.entries(historyData.entries).forEach(([port, info]) => {
|
|
const tr = document.createElement('tr');
|
|
// 第一列:端口号
|
|
let rowHtml = `<td>${port}</td>`;
|
|
// 后面的各批次状态i
|
|
let idx = 0;
|
|
for(let i=1;i <= imaxcount;i++){
|
|
if(info.scancount[idx] == i){
|
|
// 若 changed 为 1,则背景淡红
|
|
const bg = info.changed[idx] ? 'background-color:#ffecec;' : '';
|
|
// 鼠标移上时显示服务/版本/协议
|
|
const title =
|
|
`服务: ${info.service[idx]}\n` +
|
|
`版本: ${info.version[idx]}`;
|
|
rowHtml += `<td style="${bg}" title="${title}">${info.statuses[idx]}</td>`;
|
|
idx ++;
|
|
} else {//序号不对则加个空单元格
|
|
rowHtml += `<td style="background-color: lightsteelblue"></td>`;
|
|
}
|
|
}
|
|
tr.innerHTML = rowHtml;
|
|
historyTableBody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// 导出按钮
|
|
document.getElementById('exportPortData').onclick = () => {
|
|
// 找到当前激活的 tab-pane
|
|
const activePane = document.querySelector('#portTabContent .tab-pane.active').id;
|
|
|
|
if (activePane === 'latestPane') {
|
|
// 导出最新数据:可以直接把 latestData 当前页 slice 成 CSV
|
|
exportLatest();
|
|
} else {
|
|
// 导出历史数据
|
|
exportHistory();
|
|
}
|
|
};
|
|
|
|
function exportLatest() {
|
|
// 构造 CSV 文本
|
|
let rows = [
|
|
['序号', '端口号', '服务', '版本号', '端口状态'].map(fmtCell).join(',')
|
|
];
|
|
latestData.forEach((row, i) => {
|
|
rows.push([
|
|
(i + 1).toString(),
|
|
row[0].toString(),
|
|
row[1] || '',
|
|
row[2] || '',
|
|
row[3] || ''
|
|
].map(fmtCell).join(','));
|
|
});
|
|
|
|
const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM
|
|
|
|
downloadCSV(csv, `latest_ports_${portModalEl.dataset.ip}.csv`);
|
|
}
|
|
|
|
function exportHistory() {
|
|
const thead = historyTableHead.querySelector('tr');
|
|
const tbody = historyTableBody.querySelectorAll('tr');
|
|
|
|
// 1. 构造表头 CSV
|
|
const headerCells = Array.from(thead.children).map(th => fmtCell(th.textContent.trim()));
|
|
const headerLine = headerCells.join(',');
|
|
|
|
// 2. 构造表体 CSV
|
|
const bodyLines = Array.from(tbody).map(tr => {
|
|
const cells = Array.from(tr.children).map(td => fmtCell(td.textContent.trim()));
|
|
return cells.join(',');
|
|
});
|
|
|
|
// 3. 拼接,加 BOM
|
|
const csvContent = '\uFEFF' + [headerLine, ...bodyLines].join('\r\n');
|
|
|
|
// 4. 发起下载
|
|
downloadCSV(csvContent, `history_ports_${portModalEl.dataset.ip}.csv`);
|
|
|
|
}
|
|
|
|
//***********漏洞数据**************
|
|
const vulModalEl = document.getElementById('vulDataModal');
|
|
const nodeNameEl = document.getElementById("vulNodeName");
|
|
const vulTypeEl = document.getElementById("vulType");
|
|
const vulLevelEl = document.getElementById("vulLevel");
|
|
|
|
const vulModal = new bootstrap.Modal(vulModalEl);
|
|
let allVuls = [];
|
|
let currentVulPage = 1;
|
|
async function showVulData(IP){
|
|
vulModalEl.dataset.ip = IP;
|
|
nodeNameEl.value = "";
|
|
vulTypeEl.value = "";
|
|
vulLevelEl.value = "";
|
|
vulModal.show();
|
|
}
|
|
vulModalEl.addEventListener('show.bs.modal',async ev =>{
|
|
searchVulnerabilities();
|
|
});
|
|
|
|
async function searchVulnerabilities(page = 1) {
|
|
const ip = vulModalEl.dataset.ip;
|
|
if (!ip) return;
|
|
const nodeName = nodeNameEl.value.trim();
|
|
const vulType = vulTypeEl.value.trim();
|
|
const vulLevel = vulLevelEl.value;
|
|
//拉取该IP最新完成检测的漏洞数据
|
|
try{
|
|
const vuldata = await postJSON('/api/assets/getvuldata', {ip,nodeName,vulType,vulLevel});
|
|
allVuls = vuldata.vuls || [];
|
|
console.log("拉取漏洞数据")
|
|
} catch (error) {
|
|
console.error("拉取漏洞数据失败!",error)
|
|
}
|
|
renderVulPage(1);
|
|
}
|
|
|
|
function renderVulPage(page) {
|
|
currentVulPage = page;
|
|
const start = (page - 1) * pageSize;
|
|
const end = start + pageSize;
|
|
const pageData = allVuls.slice(start, end);
|
|
|
|
const tbody = document.querySelector("#vulTable tbody");
|
|
renderTableRows(tbody, pageData);
|
|
document.getElementById("vulTable").scrollIntoView({ behavior: "smooth" });
|
|
|
|
// 更新分页按钮
|
|
document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1;
|
|
document.getElementById("vulNext").dataset.page = (end < allVuls.length) ? page + 1 : page;
|
|
}
|
|
|
|
//使用该函数,需要数据的顺序与表格列的数据一致
|
|
function renderTableRows(tbody, rowsData) {
|
|
tbody.innerHTML = "";
|
|
// 遍历数据行,生成 <tr>
|
|
rowsData.forEach((row, index) => {
|
|
const tr = document.createElement("tr");
|
|
// 这里假设 row 为对象,包含各个字段;下标从1开始显示序号
|
|
for (const cellData of Object.values(row)) {
|
|
const td = document.createElement("td");
|
|
td.textContent = cellData;
|
|
tr.appendChild(td);
|
|
}
|
|
tbody.appendChild(tr);
|
|
});
|
|
// 补足空行
|
|
const rowCount = rowsData.length;
|
|
for (let i = rowCount; i < pageSize; i++) {
|
|
const tr = document.createElement("tr");
|
|
for (let j = 0; j < tbody.parentElement.querySelectorAll("th").length; j++) {
|
|
const td = document.createElement("td");
|
|
td.innerHTML = " ";
|
|
tr.appendChild(td);
|
|
}
|
|
tbody.appendChild(tr);
|
|
}
|
|
}
|
|
|
|
//导出漏洞数据
|
|
async function ExportVuls(){
|
|
// Check if in secure context
|
|
if (!window.isSecureContext) {
|
|
console.error("Page is not in a secure context. Please load the page over HTTPS.");
|
|
alert("无法导出文件:请使用 HTTPS 访问页面。");
|
|
return;
|
|
}
|
|
|
|
// 如果 dataArr 为空,返回提示
|
|
if (!allVuls || allVuls.length === 0) {
|
|
alert("没有数据可导出");
|
|
return;
|
|
}
|
|
|
|
// 在 CSV 的开头加上 BOM,用于 Excel 识别 UTF-8 编码
|
|
let csv = "\uFEFF" + "序号,节点路径,漏洞类型,漏洞级别,漏洞说明\n"; // 添加 BOM 防乱码
|
|
allVuls.forEach((item, i) => {
|
|
csv +=
|
|
[
|
|
escapeCsvField(item[0] || ""),
|
|
escapeCsvField(item[1] || ""),
|
|
escapeCsvField(item[2] || ""),
|
|
escapeCsvField(item[3] || ""),
|
|
escapeCsvField(item[4] || ""),
|
|
].join(",") + "\n";
|
|
});
|
|
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = "漏洞数据.csv";
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// 绑定漏洞数据查询按钮事件
|
|
document.getElementById("vulSearchBtn").addEventListener("click", () => {
|
|
searchVulnerabilities();
|
|
});
|
|
//导出按钮
|
|
document.getElementById("vulExportBtn").addEventListener("click",()=>ExportVuls());
|
|
// 绑定漏洞数据分页点击事件
|
|
document.getElementById("vulPrev").addEventListener("click", (e) => {
|
|
const page = parseInt(e.target.dataset.page, 10);
|
|
renderVulPage(page);
|
|
});
|
|
document.getElementById("vulNext").addEventListener("click", (e) => {
|
|
const page = parseInt(e.target.dataset.page, 10);
|
|
renderVulPage(page);
|
|
});
|
|
|
|
//***********关联域名**************
|
|
let latestUrls = [], historyUrls = [];
|
|
let urllatestPage = 1, urlhistoryPage = 1;
|
|
const urlModalEl = document.getElementById('urlDataModal');
|
|
const urlModal = new bootstrap.Modal(urlModalEl);
|
|
|
|
// table bodies & pagination
|
|
const latestTbody = document.querySelector('#latestUrlTable tbody');
|
|
const latestPrev = document.getElementById('latestUrlPrev');
|
|
const latestNext = document.getElementById('latestUrlNext');
|
|
|
|
const historyTbody = document.querySelector('#historyUrlTable tbody');
|
|
const historyPrev = document.getElementById('historyUrlPrev');
|
|
const historyNext = document.getElementById('historyUrlNext');
|
|
|
|
// 打开 Modal 时加载
|
|
urlModalEl.addEventListener('show.bs.modal', async ev => {
|
|
const ip = urlModalEl.dataset.ip;
|
|
if (!ip) return;
|
|
|
|
//拉 最新关联
|
|
try{
|
|
const latestJson = await postJSON('/api/assets/geturllatest', {ip});
|
|
latestUrls = latestJson.ip_url_data || [];
|
|
} catch (error) {
|
|
console.error("拉取最新关联的url数据失败!",error)
|
|
}
|
|
|
|
urllatestPage = 1;
|
|
urlrenderLatest();
|
|
|
|
//拉 历史关联
|
|
try{
|
|
const hisJson = await postJSON('/api/assets/geturlhistory', {ip});
|
|
historyUrls = hisJson.ip_url_data || [];
|
|
} catch (error) {
|
|
console.error("拉取历史的关联URL数据失败!",error)
|
|
}
|
|
urlhistoryPage = 1;
|
|
urlrenderHistory();
|
|
});
|
|
|
|
function urlrenderLatest() {
|
|
latestTbody.innerHTML = '';
|
|
const start = (urllatestPage - 1) * pageSize;
|
|
const slice = latestUrls.slice(start, start + pageSize);
|
|
slice.forEach((it, idx) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+idx+1}</td>
|
|
<td>${it[0]}</td>
|
|
<td>${it[1]}</td>
|
|
<td>${it[2]}</td>
|
|
<td>${it[3]}</td>
|
|
<td>${it[4]}</td>
|
|
<td>${it[5]}</td>
|
|
`;
|
|
latestTbody.appendChild(tr);
|
|
});
|
|
// 空行
|
|
for (let i=slice.length; i<pageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="7"> </td>';
|
|
latestTbody.appendChild(tr);
|
|
}
|
|
latestPrev.dataset.page = urllatestPage>1?urllatestPage-1:1;
|
|
latestNext.dataset.page = latestUrls.length>urllatestPage*pageSize?urllatestPage+1:urllatestPage;
|
|
}
|
|
|
|
function urlrenderHistory() {
|
|
historyTbody.innerHTML = '';
|
|
const start = (urlhistoryPage - 1) * pageSize;
|
|
const slice = historyUrls.slice(start, start + pageSize);
|
|
slice.forEach((it, idx) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+idx+1}</td>
|
|
<td>${it[2]}</td>
|
|
<td>${it[0]}</td>
|
|
<td>${it[1]}</td>
|
|
`;
|
|
historyTbody.appendChild(tr);
|
|
});
|
|
for (let i=slice.length; i<pageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="7"> </td>';
|
|
historyTbody.appendChild(tr);
|
|
}
|
|
historyPrev.dataset.page = urlhistoryPage>1?urlhistoryPage-1:1;
|
|
historyNext.dataset.page = historyUrls.length>urlhistoryPage*pageSize?urlhistoryPage+1:urlhistoryPage;
|
|
}
|
|
|
|
// 分页绑定
|
|
latestPrev.addEventListener('click',e=>{e.preventDefault(); urllatestPage=+e.target.dataset.page; renderLatest();});
|
|
latestNext.addEventListener('click',e=>{e.preventDefault(); urllatestPage=+e.target.dataset.page; renderLatest();});
|
|
historyPrev.addEventListener('click',e=>{e.preventDefault(); urlhistoryPage=+e.target.dataset.page; renderHistory();});
|
|
historyNext.addEventListener('click',e=>{e.preventDefault(); urlhistoryPage=+e.target.dataset.page; renderHistory();});
|
|
|
|
// 打开函数
|
|
window.showRelatedDomain = function(ip) {
|
|
urlModalEl.dataset.ip = ip;
|
|
urlModal.show();
|
|
};
|
|
|
|
// 导出(示例 CSV,仅最新/历史当前页)
|
|
document.getElementById('exportUrlData').onclick = () => {
|
|
const active = document.querySelector('#urlTabContent .tab-pane.active').id;
|
|
if (active==='urlLatestPane') {
|
|
let rows=[['序号','域名','子域名','注册人','注册邮箱','创建时间','过期时间']];
|
|
latestUrls.forEach((it,i)=>{
|
|
rows.push([i+1,it[0],it[1],it[2],it[3],it[4],it[5]]);
|
|
});
|
|
urldownloadCSV(rows,'最新关联域名.csv');
|
|
} else {
|
|
let rows=[['序号','时间','变化类型','关联域名']];
|
|
historyUrls.forEach((it,i)=>{
|
|
rows.push([i+1,it[2],it[0],it[1]]);
|
|
});
|
|
urldownloadCSV(rows,'历史关联域名.csv');
|
|
}
|
|
};
|
|
|
|
function urldownloadCSV(arr, fname) {
|
|
const bom='\uFEFF';
|
|
const csv = arr.map(r => r.map(v=>`"${v}"`).join(',')).join('\r\n');
|
|
|
|
const blob = new Blob([bom+csv], {type:'text/csv;charset=utf-8;'});
|
|
const a = document.createElement('a');
|
|
a.href=URL.createObjectURL(blob);
|
|
a.download=fname; a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
|
|
//***********删除**************
|
|
async function confirmDeleteIp(IP) {
|
|
if (!confirm('确认删除?')) return;
|
|
try{
|
|
const redata = await postJSON('/api/assets/delipassets', {IP});
|
|
bsuccess = redata.bsuccess;
|
|
error = redata.error;
|
|
if(bsuccess){
|
|
alert("删除IP资产成功!");
|
|
loadIpAssets(); //刷新数据 --重新拉取数据后刷新,可以考虑本地数据修改后刷新页面。
|
|
}
|
|
else{
|
|
alert("删除IP资产失败!",error)
|
|
}
|
|
} catch (error) {
|
|
console.error("删除IP资产失败!",error);
|
|
}
|
|
}
|
|
|
|
//---------------------------URL-Assets Tab----------------------------
|
|
const urlInfoModalEl = document.getElementById("urlBasicInfoModal")
|
|
const urlInfoModal = new bootstrap.Modal(urlInfoModalEl);
|
|
let urlAssets = [],currentUrlPage = 1
|
|
|
|
async function loadUrlAssets(page=1) {
|
|
const url_filter = document.getElementById("urlFilter").value.trim();
|
|
const user_filter = document.getElementById("ownerFilter").value.trim();
|
|
const email_filter = document.getElementById("emailFilter").value;
|
|
try {
|
|
const data = await postJSON("/api/assets/geturlassets",{url_filter,user_filter,email_filter})
|
|
urlAssets = data.url_assets || [];
|
|
renderUrlTable(page); //刷新表格
|
|
} catch (error) {
|
|
console.error("查询记录出错:", error);
|
|
alert("查询失败!");
|
|
}
|
|
}
|
|
|
|
//刷新Url-assets表格
|
|
function renderUrlTable(page) {
|
|
currentUrlPage = page; //查询数据了,从第一页显示
|
|
const start = (currentUrlPage-1)*pageSize;
|
|
const slice = urlAssets.slice(start, start+pageSize);
|
|
const tbody = document.querySelector('#urlTable tbody');
|
|
tbody.innerHTML = '';
|
|
for (let i=0; i<slice.length; i++) {
|
|
const item = slice[i];
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+i+1}</td>
|
|
<td>${item[1]}</td>
|
|
<td>${item[2]}</td>
|
|
<td>${item[3]}</td>
|
|
<td>${item[4]}</td>
|
|
<td>${item[5]}</td>
|
|
<td>${item[6]}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-info asset-op-btn" onclick="urlBasicInfo('${start+i}')">基本信息</button>
|
|
<button class="btn btn-sm btn-info asset-op-btn" onclick="urlIPData('${item[0]}')">指向IP</button>
|
|
<button class="btn btn-sm btn-danger asset-op-btn" onclick="confirmDeleteUrl('${item[0]}','${start+i}')">删除</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
}
|
|
// 补足空行
|
|
for (let i=slice.length; i<pageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="9"> </td>';
|
|
tbody.appendChild(tr);
|
|
}
|
|
// 更新分页
|
|
document.getElementById('urlPrev').dataset.page = currentUrlPage>1?currentUrlPage-1:1;
|
|
document.getElementById('urlNext').dataset.page = urlAssets.length>currentUrlPage*pageSize?currentUrlPage+1:currentUrlPage;
|
|
}
|
|
|
|
//导出域名信息
|
|
async function exportUrlAssets(){
|
|
// 构造 CSV 文本
|
|
let rows = [
|
|
['序号', '域名', '所属用户', '注册邮箱', '最新检测时间','过期日期','IP'].map(fmtCell).join(',')
|
|
];
|
|
urlAssets.forEach((row, i) => {
|
|
rows.push([
|
|
(i + 1).toString(),
|
|
row[1].toString(),
|
|
row[2] || '',
|
|
row[3] || '',
|
|
row[4] || '',
|
|
row[5] || '',
|
|
row[6] || '',
|
|
].map(fmtCell).join(','));
|
|
});
|
|
|
|
const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM
|
|
|
|
downloadCSV(csv, `url_assets.csv`);
|
|
}
|
|
|
|
// 分页按钮
|
|
document.getElementById('urlPrev').addEventListener('click', e => { e.preventDefault(); renderUrlTable(+e.target.dataset.page); });
|
|
document.getElementById('urlNext').addEventListener('click', e => { e.preventDefault(); renderUrlTable(+e.target.dataset.page); });
|
|
// 查询
|
|
document.getElementById('urlSearchBtn').addEventListener('click', () => loadUrlAssets(1));
|
|
//导出
|
|
document.getElementById("urlExportBtn").addEventListener('click',() => exportUrlAssets())
|
|
|
|
//***********URL基本信息modal**************
|
|
const urlEl = document.getElementById("info_url");
|
|
const timeEl = document.getElementById("url_scanTime");
|
|
const registrantEl = document.getElementById("url_register");
|
|
const emailEl = document.getElementById("url_email");
|
|
const ownerEl = document.getElementById("url_owner");
|
|
const contactEl = document.getElementById("urlcontactName");
|
|
const tellEl = document.getElementById("urlcontactPhone");
|
|
const btnChose = document.getElementById("urlbtnChooseOwner");
|
|
const btnDelO = document.getElementById("urlbtnDelOwner");
|
|
const btnSaveurl = document.getElementById("saveUrlInfo");
|
|
let cur_url_id = 0;
|
|
//url.ID,url.URL,au.uname,url.emails,url.update_time,url.expiration_date,ip.ip_count,url.Registrant,au.tellnum,au.tell_username
|
|
async function urlBasicInfo(index){
|
|
const url_info = urlAssets[index];
|
|
cur_url_id = url_info[0];
|
|
urlEl.textContent = url_info[1];
|
|
timeEl.textContent = url_info[4];
|
|
registrantEl.textContent = url_info[7];
|
|
emailEl.textContent = url_info[3];
|
|
ownerEl.textContent = url_info[2];
|
|
contactEl.textContent = url_info[9];
|
|
tellEl.textContent = url_info[8];
|
|
selectedOwnerId = url_info[10];
|
|
urlInfoModal.show();
|
|
}
|
|
|
|
btnChose.addEventListener("click",()=>{ //和IP共用一个模态框
|
|
curModal = "url"
|
|
ownerDrawer.show();
|
|
loadOwners(ownerSearchEl.value.trim());
|
|
});
|
|
|
|
btnDelO.addEventListener("click",()=>{
|
|
selectedOwnerId = null;
|
|
ownerEl.textContent = '';
|
|
contactEl.textContent = '';
|
|
tellEl.textContent = '';
|
|
});
|
|
|
|
btnSaveurl.addEventListener("click",async ()=>saveUrlInfo());
|
|
|
|
async function saveUrlInfo(){
|
|
const payload = {
|
|
cur_url_id: cur_url_id,
|
|
owner_id: selectedOwnerId
|
|
};
|
|
try {
|
|
const ret = await postJSON('/api/assets/updateurlinfo', payload);
|
|
bsuccess = ret.bsuccess;
|
|
if (bsuccess) {
|
|
showToast('修改成功', 'success');
|
|
loadUrlAssets(currentIpPage); //刷新整个数据。。--要改成局部刷新或本地刷新
|
|
} else {
|
|
showToast(ret.message || '修改失败', 'danger');
|
|
}
|
|
} catch (e) {
|
|
showToast(`提交失败:${e.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
//***********指向IPmodal**************
|
|
const urlIPModalEl = document.getElementById("toIpModal");
|
|
const urlIPModal = new bootstrap.Modal(urlIPModalEl);
|
|
let last_to_ips = [],his_to_ips=[];
|
|
let last_to_ips_page = 1, his_to_ip_page = 1;
|
|
const latestUTPTableBody = document.querySelector('#latesttoipTable tbody');
|
|
const latestUTPPrevBtn = document.getElementById('latesttoipPrev');
|
|
const latestUTPNextBtn = document.getElementById('latesttoipNext');
|
|
const historyutpTableBody = document.querySelector('#historytoipTable tbody');
|
|
const hisPortUTPPrevBtn = document.getElementById('historytoipPrev');
|
|
const hisPortUTPNextBtn = document.getElementById('historytoipNext');
|
|
|
|
const exportUTPDataBtn = document.getElementById("exporttoipData");
|
|
exportUTPDataBtn.addEventListener('click',()=>exportUTPdata());
|
|
|
|
async function urlIPData(cur_url_id){
|
|
//获取数据
|
|
const payload ={cur_url_id};
|
|
try {
|
|
const ret = await postJSON('/api/assets/geturltoIP', payload);
|
|
last_to_ips = ret.last_to_ips;
|
|
his_to_ips = ret.his_to_ips;
|
|
renderUTPlast();
|
|
renderUTPhis();
|
|
urlIPModal.show();
|
|
} catch (e) {
|
|
showToast(`获取指向IP数据失败:${e.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
function renderUTPlast(){
|
|
latestUTPTableBody.innerHTML = '';
|
|
const start = (last_to_ips_page - 1) * pageSize;
|
|
const slice = last_to_ips.slice(start, start + pageSize);
|
|
slice.forEach((it, idx) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+idx+1}</td>
|
|
<td>${it[0]}</td>
|
|
<td>${it[1]}</td>
|
|
`;
|
|
latestUTPTableBody.appendChild(tr);
|
|
});
|
|
// 空行
|
|
for (let i=slice.length; i<pageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="7"> </td>';
|
|
latestUTPTableBody.appendChild(tr);
|
|
}
|
|
latestUTPPrevBtn.dataset.page = last_to_ips_page>1?last_to_ips_page-1:1;
|
|
latestUTPNextBtn.dataset.page = last_to_ips.length>last_to_ips_page*pageSize?last_to_ips_page+1:last_to_ips_page;
|
|
}
|
|
|
|
function renderUTPhis(){
|
|
historyutpTableBody.innerHTML = '';
|
|
const start = (his_to_ip_page - 1) * pageSize;
|
|
const slice = his_to_ips.slice(start, start + pageSize);
|
|
slice.forEach((it, idx) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${start+idx+1}</td>
|
|
<td>${it[0]}</td>
|
|
<td>${it[1]}</td>
|
|
`;
|
|
historyutpTableBody.appendChild(tr);
|
|
});
|
|
// 空行
|
|
for (let i=slice.length; i<pageSize; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = '<td colspan="7"> </td>';
|
|
historyutpTableBody.appendChild(tr);
|
|
}
|
|
hisPortUTPPrevBtn.dataset.page = his_to_ip_page>1?his_to_ip_page-1:1;
|
|
hisPortUTPNextBtn.dataset.page = his_to_ips.length>his_to_ip_page*pageSize?his_to_ip_page+1:his_to_ip_page;
|
|
}
|
|
|
|
// 分页绑定
|
|
latestUTPPrevBtn.addEventListener('click',e=>{e.preventDefault(); last_to_ips_page=+e.target.dataset.page; renderLatest();});
|
|
latestUTPNextBtn.addEventListener('click',e=>{e.preventDefault(); last_to_ips_page=+e.target.dataset.page; renderLatest();});
|
|
hisPortUTPPrevBtn.addEventListener('click',e=>{e.preventDefault(); his_to_ip_page=+e.target.dataset.page; renderHistory();});
|
|
hisPortUTPNextBtn.addEventListener('click',e=>{e.preventDefault(); his_to_ip_page=+e.target.dataset.page; renderHistory();});
|
|
|
|
//导出指向IP的数据
|
|
async function exportUTPdata(){
|
|
// 构造 CSV 文本
|
|
const active = document.querySelector('#toipTabContent .tab-pane.active').id;
|
|
if (active==='toipLatestPane') {
|
|
let rows=[['序号','IP地址','关联时间']];
|
|
last_to_ips.forEach((it,i)=>{
|
|
rows.push([i+1,it[0],it[1]]);
|
|
});
|
|
urldownloadCSV(rows,'当前指向IP的数据.csv');
|
|
} else {
|
|
let rows=[['序号','IP地址','取关时间']];
|
|
his_to_ips.forEach((it,i)=>{
|
|
rows.push([i+1,it[0],it[1]]);
|
|
});
|
|
urldownloadCSV(rows,'历史指向IP的数据.csv');
|
|
}
|
|
}
|
|
|
|
|
|
//***********删除URL**************
|
|
async function confirmDeleteUrl(url,index){
|
|
if (!confirm('确认删除?')) return;
|
|
try{
|
|
const redata = await postJSON('/api/assets/delurlassets', {url});
|
|
bsuccess = redata.bsuccess;
|
|
error = redata.error;
|
|
if(bsuccess){
|
|
alert("删除域名资产成功!");
|
|
//loadUrlAssets(); //刷新数据
|
|
|
|
//只更新本地数据
|
|
urlAssets.splice(index,1);
|
|
renderUrlTable(currentUrlPage);//刷新页面 --不重新请求数据了
|
|
}
|
|
else{
|
|
alert("删除域名资产失败!",error);
|
|
}
|
|
} catch (error) {
|
|
console.error("删除域名资产失败!",error);
|
|
}
|
|
}
|
|
|
|
|