Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 107 additions & 17 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,20 @@ def inject_global_vars():
def get_slack_webhook_url() -> str:
return os.getenv('SLACK_WEBHOOK_URL', '').strip()

def build_share_url(result_id: str, req) -> str:
referer = req.headers.get('Referer') if req else None
if referer:
parsed = urlparse(referer)
origin = f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme and parsed.netloc else ""
path = parsed.path or ""
if origin:
return f"{origin}{path}?scan_id={result_id}"

origin = req.headers.get('Origin') if req else None
if origin:
return f"{origin}/?scan_id={result_id}"
def build_share_url(result_id: str, req, metadata=None) -> str:
clean_repo = "report"
if metadata and 'repository_name' in metadata:
import re
clean_repo = re.sub(r'[^a-z0-9]+', '-', metadata['repository_name'].lower()).strip('-')
if not clean_repo:
clean_repo = "report"

scan_path = f"report/{clean_repo}-{result_id}"

if req and req.host_url:
return f"{req.host_url.rstrip('/')}/?scan_id={result_id}"
return f"{req.host_url.rstrip('/')}/{scan_path}"

return result_id
return f"/{scan_path}"

def send_slack_notification(message: str) -> None:
webhook_url = get_slack_webhook_url()
Expand All @@ -86,13 +83,106 @@ def send_slack_notification(message: str) -> None:

@app.route('/')
def index():
scan_id = request.args.get('scan_id')
if scan_id:
import re
match = re.search(r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$', scan_id.lower())
real_uuid = match.group(1) if match else scan_id

file_path = os.path.join(app.config['RESULTS_DIR'], f"{real_uuid}.json")
if os.path.exists(file_path):
try:
with open(file_path, 'r') as f:
data = json.load(f)
metadata = data.get('metadata', {})
clean_repo = "report"
if 'repository_name' in metadata:
clean_repo = re.sub(r'[^a-z0-9]+', '-', metadata['repository_name'].lower()).strip('-') or "report"
return redirect(url_for('report_view', scan_id=f"{clean_repo}-{real_uuid}"), code=301)
except Exception:
pass
return redirect(url_for('report_view', scan_id=scan_id), code=301)

return render_template('index.html')

@app.route('/robots.txt')
@app.route('/sitemap.xml')
def static_from_root():
return send_from_directory(app.static_folder, request.path[1:])

@app.route('/sitemap.xml')
def sitemap():
results_dir = app.config['RESULTS_DIR']
try:
files = [f for f in os.listdir(results_dir) if f.endswith('.json')]
except FileNotFoundError:
files = []

urls = []
host_url = request.host_url.rstrip('/')
urls.append(f"{host_url}/")

import re
for filename in files:
file_path = os.path.join(results_dir, filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
metadata = data.get('metadata', {})
if not metadata.get('is_private', False):
real_uuid = filename.replace('.json', '')
clean_repo = "report"
if 'repository_name' in metadata:
clean_repo = re.sub(r'[^a-z0-9]+', '-', metadata['repository_name'].lower()).strip('-') or "report"
urls.append(f"{host_url}/report/{clean_repo}-{real_uuid}")
except Exception:
continue

xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for url in urls:
xml += f' <url><loc>{url}</loc></url>\n'
xml += '</urlset>'

return app.response_class(xml, mimetype='application/xml')

@app.route('/report/<path:scan_id>')
def report_view(scan_id):
import re
match = re.search(r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$', scan_id.lower())
real_uuid = match.group(1) if match else scan_id

if '..' in real_uuid or '/' in real_uuid or '\\' in real_uuid or not real_uuid.replace('-', '').isalnum():
return "Invalid scan ID", 400

file_path = os.path.join(app.config['RESULTS_DIR'], f"{real_uuid}.json")
if not os.path.exists(file_path):
return "Report not found", 404

try:
with open(file_path, 'r') as f:
data = json.load(f)
except Exception:
return "Error reading report", 500

metadata = data.get('metadata', {})
clean_repo = "report"
if 'repository_name' in metadata:
clean_repo = re.sub(r'[^a-z0-9]+', '-', metadata['repository_name'].lower()).strip('-') or "report"
canonical_scan_id = f"{clean_repo}-{real_uuid}"

return render_template('report.html',
data=data,
report_json=json.dumps(data).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026').replace("'", '\\u0027'),
current_scan_id=canonical_scan_id,
metadata=metadata,
summary=data.get('summary', {}),
grade_report={
'overall': data.get('overall', {}),
'cost': data.get('cost', {}),
'security': data.get('security', {}),
'container': data.get('container', {}),
})

@app.route('/api/scanner/status')
def scanner_status():
"""Return information about available scanners."""
Expand Down Expand Up @@ -370,7 +460,7 @@ def save_results():
repo_url = metadata.get('repository_url', 'unknown')


share_url = build_share_url(result_id, request)
share_url = build_share_url(result_id, request, metadata)

slack_message = (
"🔗 InfraScan results shared | "
Expand All @@ -379,7 +469,7 @@ def save_results():
)
send_slack_notification(slack_message)

return jsonify({'id': result_id})
return jsonify({'id': result_id, 'share_url': share_url})

@app.route('/api/results/<scan_id>', methods=['GET'])
def get_results(scan_id):
Expand Down
56 changes: 43 additions & 13 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,20 @@ function initApp() {

if (!data) return;

// Hide all web app specific UI parts
if (scanInputContainer) scanInputContainer.style.display = 'none';
if (document.querySelector('.tabs')) document.querySelector('.tabs').style.display = 'none';
if (landingInfo) landingInfo.style.display = 'none';
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
if (newsletterModal) newsletterModal.style.display = 'none';
if (feedbackModal) feedbackModal.style.display = 'none';
const isHostedReport = window.location.pathname.startsWith('/report/');

if (!isHostedReport) {
// Hide all web app specific UI parts for standalone CLI
if (scanInputContainer) scanInputContainer.style.display = 'none';
if (document.querySelector('.tabs')) document.querySelector('.tabs').style.display = 'none';
if (landingInfo) landingInfo.style.display = 'none';
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
if (newsletterModal) newsletterModal.style.display = 'none';
if (feedbackModal) feedbackModal.style.display = 'none';
} else {
if (scanInputContainer) scanInputContainer.classList.add('hidden');
if (landingInfo) landingInfo.classList.add('collapsed');
}

const gradeReport = {
overall: data.overall,
Expand All @@ -100,8 +107,19 @@ function initApp() {
setupPdfExport();

// Hide elements that don't make sense in standalone report
if (newScanBtn) newScanBtn.style.display = 'none';
if (shareBtn) shareBtn.style.display = 'none';
if (!isHostedReport) {
if (newScanBtn) newScanBtn.style.display = 'none';
if (shareBtn) shareBtn.style.display = 'none';
} else {
// We need currentScanId to be populated for sharing to work
const pathParts = window.location.pathname.split('/');
const lastPart = pathParts[pathParts.length - 1];
// Extract UUID from the last part (e.g. clean-repo-name-uuid)
const uuidMatch = lastPart.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
if (uuidMatch) {
currentScanId = uuidMatch[1];
}
}

// Ensure container is correctly styled for full-width report
if (mainContainer) mainContainer.classList.add('expanded');
Expand All @@ -114,7 +132,7 @@ function initApp() {
loadSharedResults();

// Trigger Newsletter Modal after 3 seconds if not already closed
if (!localStorage.getItem('newsletter_closed') && !window.location.search.includes('scan_id')) {
if (!localStorage.getItem('newsletter_closed') && !window.location.search.includes('scan_id') && !window.location.pathname.startsWith('/report/')) {
setTimeout(() => {
if (newsletterModal) newsletterModal.classList.remove('hidden');
}, 3000);
Expand Down Expand Up @@ -422,7 +440,11 @@ function initApp() {

try {
if (currentScanId) {
const shareUrl = `${window.location.origin}${window.location.pathname}?scan_id=${currentScanId}`;
let cleanRepo = "report";
if (currentMetadata && currentMetadata.repository_name) {
cleanRepo = currentMetadata.repository_name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || "report";
}
const shareUrl = `${window.location.origin}/report/${cleanRepo}-${currentScanId}`;
shareUrlInput.value = shareUrl;
shareLinkContainer.classList.remove('hidden');
shareBtn.textContent = 'Results Shared';
Expand Down Expand Up @@ -454,7 +476,7 @@ function initApp() {
if (!response.ok) throw new Error(data.error || `Server error (${response.status})`);

currentScanId = data.id;
const shareUrl = `${window.location.origin}${window.location.pathname}?scan_id=${data.id}`;
const shareUrl = data.share_url || `${window.location.origin}/report/${data.id}`;
shareUrlInput.value = shareUrl;
shareLinkContainer.classList.remove('hidden');
shareBtn.textContent = 'Results Shared';
Expand All @@ -477,6 +499,10 @@ function initApp() {
// New Scan Button
if (newScanBtn) {
newScanBtn.addEventListener('click', () => {
if (window.location.pathname.startsWith('/report/')) {
window.location.href = '/';
return;
}
resultsArea.classList.add('hidden');
if (scanInputContainer) scanInputContainer.classList.remove('hidden');
if (landingInfo) landingInfo.classList.remove('collapsed');
Expand Down Expand Up @@ -837,7 +863,11 @@ function initApp() {

const recipientBadge = '';

const viewUrl = `${window.location.origin}${window.location.pathname}?scan_id=${scan.id}`;
let cleanRepo = "report";
if (scan.repository_name) {
cleanRepo = scan.repository_name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || "report";
}
const viewUrl = `${window.location.origin}/report/${cleanRepo}-${scan.id}`;

return `
<div class="scan-history-card">
Expand Down
8 changes: 0 additions & 8 deletions static/sitemap.xml

This file was deleted.

Loading
Loading