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
25 changes: 25 additions & 0 deletions application/single_app/functions_rmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,31 @@ def initialize_rmf_service(group_id, user_id, group_role, payload):
)


def start_rmf_collect(group_id, user_id, group_role, subscription_id, resource_group, agentic):
return _rmf_request(
"POST",
"/api/v1/workspaces/current/collect",
group_id,
user_id,
group_role,
payload={
"subscription_id": subscription_id,
"resource_group": resource_group or None,
"agentic": bool(agentic),
},
)


def get_rmf_collect_job(group_id, user_id, group_role, job_id):
return _rmf_request(
"GET",
f"/api/v1/workspaces/current/collect/jobs/{job_id}",
group_id,
user_id,
group_role,
)


def update_group_rmf_setup_status(group_id, setup_status):
group_doc = find_group_by_id(group_id)
if not group_doc:
Expand Down
48 changes: 48 additions & 0 deletions application/single_app/route_backend_rmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
get_rmf_chat_session,
get_rmf_chat_sessions,
get_rmf_chat_source,
get_rmf_collect_job,
get_rmf_control,
get_rmf_controls,
get_rmf_evidence,
Expand All @@ -63,6 +64,7 @@
send_rmf_chat_message,
set_rmf_control_applicability,
start_rmf_analysis,
start_rmf_collect,
update_group_rmf_setup_status,
update_group_rmf_state,
validate_rmf_xlsx_package,
Expand Down Expand Up @@ -1496,6 +1498,52 @@ def withdraw_rmf_workspace_evidence(import_id):
return jsonify({"error": str(exc)}), exc.status_code
return jsonify(result), 200

@bp.route("/api/rmf/workspace/evidence/collect", methods=["POST"])
@swagger_route(security=get_auth_security())
@login_required
@user_required
@enabled_required("enable_group_workspaces")
@enabled_required("enable_rmf")
def start_rmf_workspace_collect():
user_id = get_current_user_id()
group_id, _, role, error_response = _rmf_group_context(
user_id,
allowed_roles=RMF_EVIDENCE_MANAGER_ROLES,
)
if error_response:
return error_response
payload = request.get_json(silent=True) or {}
subscription_id = payload.get("subscription_id", "").strip()
if not subscription_id:
return jsonify({"error": "subscription_id is required"}), 400
resource_group = payload.get("resource_group") or None
agentic = bool(payload.get("agentic", False))
try:
result = start_rmf_collect(group_id, user_id, role, subscription_id, resource_group, agentic)
except RMFServiceError as exc:
return jsonify({"error": str(exc)}), exc.status_code
return jsonify(result), 202

@bp.route("/api/rmf/workspace/evidence/collect/jobs/<job_id>", methods=["GET"])
@swagger_route(security=get_auth_security())
@login_required
@user_required
@enabled_required("enable_group_workspaces")
@enabled_required("enable_rmf")
def get_rmf_workspace_collect_job(job_id):
user_id = get_current_user_id()
group_id, _, role, error_response = _rmf_group_context(
user_id,
allowed_roles=RMF_EVIDENCE_MANAGER_ROLES,
)
if error_response:
return error_response
try:
result = get_rmf_collect_job(group_id, user_id, role, job_id)
except RMFServiceError as exc:
return jsonify({"error": str(exc)}), exc.status_code
return jsonify(result), 200

@bp.route("/api/rmf/workspace", methods=["PATCH"])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
157 changes: 157 additions & 0 deletions application/single_app/static/js/rmf/rmf-collect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* rmf-collect.js — Collect from Azure UI for the RMF evidence tab.
*
* Looks for [data-rmf-collect-form] in the DOM and wires up the collect flow:
* 1. User fills in subscription ID + optional resource group
* 2. POST /api/rmf/workspace/evidence/collect → job_id
* 3. Poll GET .../collect/jobs/<job_id> every 3 s
* 4. Append log lines to the log panel; show final status
* 5. On success, dispatch "rmf:evidence-refresh" so the evidence table reloads
*/

(function () {
"use strict";

const POLL_INTERVAL_MS = 3000;
const TERMINAL_STATUSES = new Set(["succeeded", "failed"]);

function init() {
const form = document.querySelector("[data-rmf-collect-form]");
if (!form) return;

const submitBtn = form.querySelector("[data-rmf-collect-submit]");
const subscriptionInput = form.querySelector("[data-rmf-collect-subscription]");
const resourceGroupInput = form.querySelector("[data-rmf-collect-resource-group]");
const statusPanel = form.querySelector("[data-rmf-collect-status]");
const logPanel = form.querySelector("[data-rmf-collect-log]");
const statusBadge = form.querySelector("[data-rmf-collect-status-badge]");

let pollTimer = null;

function setRunning(running) {
submitBtn.disabled = running;
subscriptionInput.disabled = running;
if (resourceGroupInput) resourceGroupInput.disabled = running;
submitBtn.textContent = running ? "Collecting…" : "Collect";
}

function showStatus(visible) {
statusPanel.hidden = !visible;
}

function appendLog(lines) {
if (!logPanel) return;
lines.forEach(function (line) {
const el = document.createElement("div");
el.textContent = line;
logPanel.appendChild(el);
logPanel.scrollTop = logPanel.scrollHeight;
});
}

function setBadge(status) {
if (!statusBadge) return;
const map = {
queued: ["bg-secondary", "Queued"],
running: ["bg-primary", "Running"],
succeeded: ["bg-success", "Succeeded"],
failed: ["bg-danger", "Failed"],
};
const [cls, label] = map[status] || ["bg-secondary", status];
statusBadge.className = "badge " + cls;
statusBadge.textContent = label;
}

function poll(jobId, seenLogCount) {
fetch("/api/rmf/workspace/evidence/collect/jobs/" + encodeURIComponent(jobId))
.then(function (r) {
if (!r.ok) {
return r.text().then(function (t) {
throw new Error("HTTP " + r.status + ": " + t.substring(0, 200));
});
}
return r.json();
})
.then(function (job) {
const log = job.log || [];
if (log.length > seenLogCount) {
appendLog(log.slice(seenLogCount));
}
setBadge(job.status);

if (TERMINAL_STATUSES.has(job.status)) {
clearTimeout(pollTimer);
setRunning(false);
if (job.status === "succeeded") {
document.dispatchEvent(new CustomEvent("rmf:evidence-refresh"));
} else if (job.error) {
appendLog(["Error: " + job.error]);
}
} else {
pollTimer = setTimeout(function () {
poll(jobId, log.length);
}, POLL_INTERVAL_MS);
}
})
.catch(function (err) {
appendLog(["Poll error: " + err.message]);
setRunning(false);
});
}

form.addEventListener("submit", function (e) {
e.preventDefault();

const subscriptionId = (subscriptionInput.value || "").trim();
if (!subscriptionId) {
subscriptionInput.focus();
return;
}

const resourceGroup = resourceGroupInput
? (resourceGroupInput.value || "").trim() || null
: null;

// Reset log
if (logPanel) logPanel.innerHTML = "";
showStatus(true);
setBadge("queued");
setRunning(true);

fetch("/api/rmf/workspace/evidence/collect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
subscription_id: subscriptionId,
resource_group: resourceGroup,
agentic: true,
}),
})
.then(function (r) {
if (!r.ok) {
return r.json().then(function (body) {
throw new Error(body.error || r.statusText);
});
}
return r.json();
})
.then(function (job) {
setBadge(job.status);
appendLog(["Job started: " + job.id]);
poll(job.id, 0);
})
.catch(function (err) {
showStatus(true);
appendLog(["Failed to start collection: " + err.message]);
setBadge("failed");
setRunning(false);
});
});
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
49 changes: 49 additions & 0 deletions application/single_app/templates/rmf_workspace.html
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,53 @@ <h2 class="h5 mb-1">Evidence</h2>
</div>
</div>
</div>

<div class="card shadow-sm border-0 mt-3">
<div class="card-body p-4">
<h2 class="h5 mb-1">Collect from Azure</h2>
<p class="text-muted mb-3">
Enumerate Azure resources and import them as evidence directly from your subscription.
Static collection uses the Azure CLI; enable Agentic to also gather RBAC, diagnostics,
and Defender posture via Azure MCP (requires MCP credentials on the server).
</p>
<form data-rmf-collect-form>
<div class="row g-3 mb-3">
<div class="col-sm-6">
<label class="form-label fw-semibold" for="rmf-collect-subscription">
Subscription ID <span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-sm font-monospace"
id="rmf-collect-subscription"
data-rmf-collect-subscription
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
autocomplete="off" spellcheck="false" required>
</div>
<div class="col-sm-4">
<label class="form-label fw-semibold" for="rmf-collect-rg">
Resource Group <span class="text-muted fw-normal">(optional)</span>
</label>
<input type="text" class="form-control form-control-sm"
id="rmf-collect-rg"
data-rmf-collect-resource-group
placeholder="rg-my-workload"
autocomplete="off" spellcheck="false">
</div>
</div>
<button type="submit" class="btn btn-primary btn-sm" data-rmf-collect-submit>
Collect
</button>
<div hidden data-rmf-collect-status class="mt-3">
<div class="d-flex align-items-center gap-2 mb-2">
<span class="fw-semibold small">Status:</span>
<span class="badge bg-secondary" data-rmf-collect-status-badge>Queued</span>
</div>
<div class="border rounded p-2 bg-light font-monospace small"
style="max-height:160px;overflow-y:auto;"
data-rmf-collect-log></div>
</div>
</form>
</div>
</div>
</section>

<section
Expand Down Expand Up @@ -3324,6 +3371,7 @@ <h2 class="h5">Export history</h2>
"click",
loadRmfEvidence
);
document.addEventListener("rmf:evidence-refresh", loadRmfEvidence);

document.querySelectorAll('input[name="rmf-analysis-scope"]').forEach((input) => {
input.addEventListener("change", () => {
Expand Down Expand Up @@ -3654,4 +3702,5 @@ <h2 class="h5">Export history</h2>

loadRmfServiceState();
</script>
<script src="{{ url_for('static', filename='js/rmf/rmf-collect.js') }}"></script>
{% endblock %}