feat(ai-insights): Add CVE feed panel to dashboard
- Add get_cve_feed RPCD method fetching from NVD API - Add CVE feed panel showing recent vulnerabilities with CVSS scores - Cache CVE feed for 30 minutes to reduce API calls - Link CVE IDs to NVD detail pages - Color-code severity (critical/high/medium/low) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4f0e7d7cc4
commit
040b69ad1d
@ -47,6 +47,13 @@ var callAnalyze = rpc.declare({
|
|||||||
expect: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callGetCVEFeed = rpc.declare({
|
||||||
|
object: 'luci.ai-insights',
|
||||||
|
method: 'get_cve_feed',
|
||||||
|
params: ['limit'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
function getPostureColor(score) {
|
function getPostureColor(score) {
|
||||||
if (score >= 80) return 'success';
|
if (score >= 80) return 'success';
|
||||||
if (score >= 60) return 'warning';
|
if (score >= 60) return 'warning';
|
||||||
@ -104,6 +111,7 @@ return baseclass.extend({
|
|||||||
getTimeline: callGetTimeline,
|
getTimeline: callGetTimeline,
|
||||||
runAll: callRunAll,
|
runAll: callRunAll,
|
||||||
analyze: callAnalyze,
|
analyze: callAnalyze,
|
||||||
|
getCVEFeed: callGetCVEFeed,
|
||||||
|
|
||||||
getPostureColor: getPostureColor,
|
getPostureColor: getPostureColor,
|
||||||
getPostureLabel: getPostureLabel,
|
getPostureLabel: getPostureLabel,
|
||||||
|
|||||||
@ -12,12 +12,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
|
cveCache: [],
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
|
var self = this;
|
||||||
var link = document.createElement('link');
|
var link = document.createElement('link');
|
||||||
link.rel = 'stylesheet';
|
link.rel = 'stylesheet';
|
||||||
link.href = L.resource('ai-insights/dashboard.css');
|
link.href = L.resource('ai-insights/dashboard.css');
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
return api.getOverview().catch(function() { return {}; });
|
return Promise.all([
|
||||||
|
api.getOverview().catch(function() { return {}; }),
|
||||||
|
api.getCVEFeed(10).catch(function() { return { cves: [] }; })
|
||||||
|
]).then(function(results) {
|
||||||
|
self.cveCache = (results[1] || {}).cves || [];
|
||||||
|
return results[0];
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
@ -68,6 +77,15 @@ return view.extend({
|
|||||||
E('span', { 'class': 'ai-badge' }, String(alerts.length))
|
E('span', { 'class': 'ai-badge' }, String(alerts.length))
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'ai-card-body', 'id': 'ai-alerts' }, this.renderAlerts(alerts))
|
E('div', { 'class': 'ai-card-body', 'id': 'ai-alerts' }, this.renderAlerts(alerts))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// CVE Feed card
|
||||||
|
E('div', { 'class': 'ai-card' }, [
|
||||||
|
E('div', { 'class': 'ai-card-header' }, [
|
||||||
|
'⚠️ CVE Feed (Recent)',
|
||||||
|
E('span', { 'class': 'ai-badge warning' }, String(this.cveCache.length))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'ai-card-body', 'id': 'ai-cves' }, this.renderCVEs(this.cveCache))
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -159,6 +177,32 @@ return view.extend({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderCVEs: function(cves) {
|
||||||
|
if (!cves || !cves.length) {
|
||||||
|
return E('div', { 'class': 'ai-empty' }, 'Loading CVE feed...');
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', { 'class': 'ai-cve-list' }, cves.slice(0, 10).map(function(cve) {
|
||||||
|
var score = cve.score || 0;
|
||||||
|
var severity = score >= 9.0 ? 'critical' : score >= 7.0 ? 'high' : score >= 4.0 ? 'medium' : 'low';
|
||||||
|
var severityClass = score >= 9.0 ? 'danger' : score >= 7.0 ? 'warning' : score >= 4.0 ? 'caution' : 'success';
|
||||||
|
|
||||||
|
return E('div', { 'class': 'ai-cve-item' }, [
|
||||||
|
E('div', { 'class': 'ai-cve-score ' + severityClass }, [
|
||||||
|
E('span', { 'class': 'score-value' }, score.toFixed(1)),
|
||||||
|
E('span', { 'class': 'score-label' }, severity.toUpperCase())
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'ai-cve-content' }, [
|
||||||
|
E('div', { 'class': 'ai-cve-id' }, [
|
||||||
|
E('a', { 'href': 'https://nvd.nist.gov/vuln/detail/' + cve.id, 'target': '_blank' }, cve.id)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'ai-cve-desc' }, (cve.description || '').substring(0, 120) + '...')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'ai-cve-date' }, cve.published || '')
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
runAllAgents: function() {
|
runAllAgents: function() {
|
||||||
ui.showModal('Running Agents', [
|
ui.showModal('Running Agents', [
|
||||||
E('p', { 'class': 'spinning' }, 'Starting all AI agents...')
|
E('p', { 'class': 'spinning' }, 'Starting all AI agents...')
|
||||||
|
|||||||
@ -104,6 +104,7 @@ case "$1" in
|
|||||||
"get_alerts": {"limit": 50},
|
"get_alerts": {"limit": 50},
|
||||||
"get_posture": {},
|
"get_posture": {},
|
||||||
"get_timeline": {"hours": 24},
|
"get_timeline": {"hours": 24},
|
||||||
|
"get_cve_feed": {"limit": 10},
|
||||||
"run_all": {},
|
"run_all": {},
|
||||||
"analyze": {}
|
"analyze": {}
|
||||||
}
|
}
|
||||||
@ -268,6 +269,60 @@ EOF
|
|||||||
echo "$results"
|
echo "$results"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
get_cve_feed)
|
||||||
|
read -r input
|
||||||
|
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null)
|
||||||
|
[ -z "$limit" ] && limit=10
|
||||||
|
|
||||||
|
# Cache file for CVE feed (refresh every 30 min)
|
||||||
|
cache_file="/tmp/cve_feed_cache.json"
|
||||||
|
cache_age=1800
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
if [ -f "$cache_file" ]; then
|
||||||
|
age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0)))
|
||||||
|
if [ "$age" -lt "$cache_age" ]; then
|
||||||
|
cat "$cache_file"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch from NVD API (last 3 days)
|
||||||
|
start_date=$(date -d "3 days ago" +%Y-%m-%dT00:00:00.000 2>/dev/null || date -u +%Y-%m-%dT00:00:00.000)
|
||||||
|
end_date=$(date +%Y-%m-%dT23:59:59.999 2>/dev/null || date -u +%Y-%m-%dT23:59:59.999)
|
||||||
|
|
||||||
|
response=$(curl -s --max-time 15 \
|
||||||
|
"https://services.nvd.nist.gov/rest/json/cves/2.0?pubStartDate=${start_date}&pubEndDate=${end_date}&resultsPerPage=${limit}" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$response" ]; then
|
||||||
|
# Parse and format CVEs
|
||||||
|
json_init
|
||||||
|
json_add_array "cves"
|
||||||
|
|
||||||
|
echo "$response" | jsonfilter -e '@.vulnerabilities[*]' 2>/dev/null | while read -r vuln; do
|
||||||
|
cve_id=$(echo "$vuln" | jsonfilter -e '@.cve.id' 2>/dev/null)
|
||||||
|
desc=$(echo "$vuln" | jsonfilter -e '@.cve.descriptions[0].value' 2>/dev/null | head -c 200 | sed 's/"/\\"/g')
|
||||||
|
score=$(echo "$vuln" | jsonfilter -e '@.cve.metrics.cvssMetricV31[0].cvssData.baseScore' 2>/dev/null)
|
||||||
|
[ -z "$score" ] && score=$(echo "$vuln" | jsonfilter -e '@.cve.metrics.cvssMetricV2[0].cvssData.baseScore' 2>/dev/null)
|
||||||
|
[ -z "$score" ] && score="0"
|
||||||
|
published=$(echo "$vuln" | jsonfilter -e '@.cve.published' 2>/dev/null | cut -c1-10)
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "id" "$cve_id"
|
||||||
|
json_add_string "description" "$desc"
|
||||||
|
json_add_double "score" "$score"
|
||||||
|
json_add_string "published" "$published"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump > "$cache_file"
|
||||||
|
cat "$cache_file"
|
||||||
|
else
|
||||||
|
echo '{"cves":[],"error":"Failed to fetch CVE feed"}'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
analyze)
|
analyze)
|
||||||
# Get AI security analysis
|
# Get AI security analysis
|
||||||
localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091")
|
localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091")
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Grant access to AI Insights Dashboard",
|
"description": "Grant access to AI Insights Dashboard",
|
||||||
"read": {
|
"read": {
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"luci.ai-insights": ["status", "get_alerts", "get_posture", "get_timeline"]
|
"luci.ai-insights": ["status", "get_alerts", "get_posture", "get_timeline", "get_cve_feed"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"write": {
|
"write": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user