{"arazzo":"1.0.1","info":{"title":"check-opencloud-security scan workflows","summary":"Submitting scans, waiting for them, taking the result away, and erasing it on request.","description":"Every workflow here runs against a public deployment with no account and no API key.\n\nScanning is asynchronous. Submission answers 202 Accepted with a uuid and the state 'queued'; the rating does not exist yet. Poll GET /api/scans/{uuid} every 3 seconds until the response carries 'done': true, which happens when the state reaches 'completed' or 'failed'. Allow up to about 5 minutes.\n\n429 is not a refusal. A client limit and a per-target cooldown both answer 429 with Retry-After in seconds; wait that long and try again, at most 3 times. A Retry-After longer than 300 seconds is a daily cap or a block for scanning hosts that were not OpenCloud: do not wait it out, tell the user. 403 means the deployment scans approved instances only. The whole scanner is open source and runs locally with no limits at all: https://github.com/sowoi/check-opencloud-security\n\nThe same workflows are exposed to agents as MCP tools; both are listed in the discovery document at /.well-known/ai.json.","version":"1.31.2"},"sourceDescriptions":[{"name":"checkOpenCloudSecurityApi","url":"https://scan.okxo.de/openapi.json","type":"openapi"}],"workflows":[{"workflowId":"scanOneInstance","summary":"Scan one instance and wait for its rating.","description":"The whole task, end to end: submit a target, receive a uuid, poll while the scan is queued or running, detect completion and return the rating.\n\nScanning is asynchronous. Submission answers 202 Accepted with a uuid and the state 'queued'; the rating does not exist yet. Poll GET /api/scans/{uuid} every 3 seconds until the response carries 'done': true, which happens when the state reaches 'completed' or 'failed'. Allow up to about 5 minutes.\n\nThe uuid is the whole of the authorisation. Whoever has it can read that scan and nobody else can, there is no endpoint that lists scans, and an unknown, malformed or expired uuid all answer 404 alike. Treat it as a secret, do not guess one, and do not retry a 404 - the result is gone.","inputs":{"type":"object","required":["target_url"],"properties":{"target_url":{"type":"string","description":"The instance to scan. Must be publicly resolvable; a private or loopback address is refused with 400 and must not be retried.","examples":["https://opencloud.example.com"]},"ignore_hardenings":{"type":"array","items":{"type":"string"},"description":"Hardening identifiers to waive. A waived finding is still reported, it simply stops capping the rating."},"release_track":{"type":"string","enum":["auto","rolling","production","lts"],"description":"Which release line the version is judged against. Changes the rating, never the probing."}}},"steps":[{"stepId":"submitScan","description":"A submission chooses what to scan, never how hard. Any other field is a 422 naming it. The answer is 202 Accepted with a uuid and no rating.","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1scans/post","requestBody":{"contentType":"application/json","payload":{"target_url":"$inputs.target_url","ignore_hardenings":"$inputs.ignore_hardenings","release_track":"$inputs.release_track","output_format":"json"}},"successCriteria":[{"condition":"$statusCode == 202"}],"outputs":{"uuid":"$response.body#/uuid"},"onFailure":[{"name":"waitOutTheRateLimit","type":"retry","retryAfter":60,"retryLimit":3,"criteria":[{"condition":"$statusCode == 429"}]},{"name":"targetRefused","type":"end","criteria":[{"condition":"$statusCode == 400"}]},{"name":"requestNotAccepted","type":"end","criteria":[{"condition":"$statusCode == 422"}]}]},{"stepId":"awaitScan","description":"Hand the uuid to the polling workflow and wait for it to finish.","workflowId":"awaitScanResult","parameters":[{"name":"identifier","value":"$steps.submitScan.outputs.uuid"}],"outputs":{"state":"$outputs.state","rating":"$outputs.rating","label":"$outputs.label","eol":"$outputs.eol","remediation":"$outputs.remediation","exports":"$outputs.exports"}}],"outputs":{"uuid":"$steps.submitScan.outputs.uuid","state":"$steps.awaitScan.outputs.state","rating":"$steps.awaitScan.outputs.rating","label":"$steps.awaitScan.outputs.label","eol":"$steps.awaitScan.outputs.eol","remediation":"$steps.awaitScan.outputs.remediation","exports":"$steps.awaitScan.outputs.exports"}},{"workflowId":"awaitScanResult","summary":"Poll one already-submitted scan until it finishes.","description":"The waiting half of every other workflow, so that a single scan and a batch wait in exactly the same way.\n\nQueued and running mean poll again in 3 seconds; queueing is normal, because an overloaded service queues rather than refusing and the response says where in line the scan stands. 'done' becoming true is the signal to stop, and it appears for a failed scan as well as a completed one. 404 is final: unknown, malformed and expired uuids are deliberately indistinguishable, and polling one forever is how a client turns its own mistake into load.","inputs":{"type":"object","required":["identifier"],"properties":{"identifier":{"type":"string","format":"uuid","description":"The uuid a scan submission returned. The uuid is the whole of the authorisation. Whoever has it can read that scan and nobody else can, there is no endpoint that lists scans, and an unknown, malformed or expired uuid all answer 404 alike. Treat it as a secret, do not guess one, and do not retry a 404 - the result is gone."}}},"steps":[{"stepId":"readScanState","description":"Succeeds only when the scan has finished. Anything else is handled below: still working means retry, gone means stop.","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1scans~1{identifier}/get","parameters":[{"name":"identifier","in":"path","value":"$inputs.identifier"}],"successCriteria":[{"condition":"$statusCode == 200"},{"context":"$response.body","condition":"$.done == true","type":"jsonpath"}],"onFailure":[{"name":"stillQueuedOrRunning","type":"retry","retryAfter":3,"retryLimit":100,"criteria":[{"condition":"$statusCode == 200"}]},{"name":"unknownOrExpired","type":"end","criteria":[{"condition":"$statusCode == 404"}]}],"outputs":{"state":"$response.body#/state","done":"$response.body#/done","target":"$response.body#/target","rating":"$response.body#/summary/rating","label":"$response.body#/summary/label","eol":"$response.body#/summary/eol","explanation":"$response.body#/summary/explanation","remediation":"$response.body#/summary/remediation","exports":"$response.body#/exports"}}],"outputs":{"state":"$steps.readScanState.outputs.state","target":"$steps.readScanState.outputs.target","rating":"$steps.readScanState.outputs.rating","label":"$steps.readScanState.outputs.label","eol":"$steps.readScanState.outputs.eol","explanation":"$steps.readScanState.outputs.explanation","remediation":"$steps.readScanState.outputs.remediation","exports":"$steps.readScanState.outputs.exports"}},{"workflowId":"scanManyInstances","summary":"Submit several instances at once and wait for the accepted ones.","description":"A batch is a convenience for a caller with an estate to check, not a discount on the limits: every target is counted against the client limit and claims its own target cooldown, in the order it was written.\n\nThe answer is therefore two lists. Everything in 'accepted' already has a uuid and is waited for with the awaitScanResult workflow. Everything in 'rejected' was refused with a status and a reason and must NOT be resubmitted by this workflow: 400 and 422 will not change, and 429 carries the number of seconds after which a new batch may be sent.\n\nArazzo has no fan-out construct, so the second step waits for the first accepted uuid. A runner repeats that step - or the awaitScanResult workflow on its own - once per entry of the accepted list, whose uuids are all in the batch response.","inputs":{"type":"object","required":["targets"],"properties":{"targets":{"type":"array","items":{"type":"string"},"description":"The instances to scan. Non-empty, and no longer than the deployment's batch limit; either mistake is a 422 that must not be retried unchanged."},"ignore_hardenings":{"type":"array","items":{"type":"string"}},"release_track":{"type":"string","enum":["auto","rolling","production","lts"]}}},"steps":[{"stepId":"submitBatch","description":"JSON only. Succeeds when at least one target started; a batch where nothing started answers with the reason its first target was refused.","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1scans~1batch/post","requestBody":{"contentType":"application/json","payload":{"targets":"$inputs.targets","ignore_hardenings":"$inputs.ignore_hardenings","release_track":"$inputs.release_track","output_format":"json"}},"successCriteria":[{"condition":"$statusCode == 202"}],"outputs":{"accepted":"$response.body#/accepted","rejected":"$response.body#/rejected","counts":"$response.body#/counts","firstAcceptedUuid":"$response.body#/accepted/0/uuid"},"onFailure":[{"name":"everyTargetRateLimited","type":"end","criteria":[{"condition":"$statusCode == 429"}]},{"name":"everyTargetRefused","type":"end","criteria":[{"condition":"$statusCode == 400"}]},{"name":"batchNotAccepted","type":"end","criteria":[{"condition":"$statusCode == 422"}]}]},{"stepId":"awaitAcceptedScan","description":"Wait for one accepted uuid, using the same polling as a single scan. Repeat for each further uuid in the accepted list. Never feed a rejected target in here: it has no uuid, and it was refused on purpose.","workflowId":"awaitScanResult","parameters":[{"name":"identifier","value":"$steps.submitBatch.outputs.firstAcceptedUuid"}],"outputs":{"state":"$outputs.state","rating":"$outputs.rating","label":"$outputs.label"}}],"outputs":{"accepted":"$steps.submitBatch.outputs.accepted","rejected":"$steps.submitBatch.outputs.rejected","counts":"$steps.submitBatch.outputs.counts","firstRating":"$steps.awaitAcceptedScan.outputs.rating","firstLabel":"$steps.awaitAcceptedScan.outputs.label"}},{"workflowId":"planRemediation","summary":"Work out what would raise the grade, and in what order.","description":"The question a report does not answer: not what is wrong, but what to do first. The plan is the rating's own arithmetic replayed with one finding removed at a time, so every predicted grade is the grade the same scanner would award - a caller must not recompute or reorder it.\n\nThe steps are already ordered by what pays off soonest. A step whose ratingGain is 0 is still necessary: findings of one severity share a single cap, so the grade moves only when the last of them is gone. When the instance is behind on releases the plan contains an 'upgrade' step, because fixing findings can never lift the rating above what the version allows.\n\nIt waits for the scan the same way every other workflow does: queued and running mean poll again, 404 means the uuid is unknown or expired and is final.","inputs":{"type":"object","required":["identifier"],"properties":{"identifier":{"type":"string","format":"uuid","description":"The uuid a scan submission returned. The uuid is the whole of the authorisation. Whoever has it can read that scan and nobody else can, there is no endpoint that lists scans, and an unknown, malformed or expired uuid all answer 404 alike. Treat it as a secret, do not guess one, and do not retry a 404 - the result is gone."}}},"steps":[{"stepId":"awaitScan","description":"There is no plan until there is a result, so this is the same wait as everywhere else rather than a second one.","workflowId":"awaitScanResult","parameters":[{"name":"identifier","value":"$inputs.identifier"}],"outputs":{"state":"$outputs.state","rating":"$outputs.rating","label":"$outputs.label","remediation":"$outputs.remediation"}}],"outputs":{"state":"$steps.awaitScan.outputs.state","rating":"$steps.awaitScan.outputs.rating","label":"$steps.awaitScan.outputs.label","remediation":"$steps.awaitScan.outputs.remediation"}},{"workflowId":"compareScans","summary":"Check whether the fixes between two scans worked.","description":"The question after a remediation plan: did it help? Two finished scans of the same instance are read as whole documents and the later one is compared against the earlier.\n\nBoth have to still exist. This service keeps no history to look an old scan up in - a comparison is of two live results, and a uuid that has expired answers 404 like any unknown one. The normal way to get a pair is to keep the uuid the plan was written against and scan again once the changes are deployed.\n\nWhat counts as a new finding is the plugin's own baseline arithmetic, the same one an operator's monitoring uses, so the answer here and the answer their alerts give cannot disagree. A grade that did not move is not a failed remediation: findings of one severity share a single cap.\n\n409 from an export means the scan exists but has not finished: wait 5 seconds and ask again, up to 36 times. 404 means the scan is unknown or expired and no amount of waiting will change that. Never treat the two alike.\n\nResults expire. Once the retention window passes the uuid stops resolving and answers 404 like any unknown one, so export or read what is needed rather than storing a uuid for later.","inputs":{"type":"object","required":["baseline","current"],"properties":{"baseline":{"type":"string","format":"uuid","description":"The uuid a scan submission returned. The uuid is the whole of the authorisation. Whoever has it can read that scan and nobody else can, there is no endpoint that lists scans, and an unknown, malformed or expired uuid all answer 404 alike. Treat it as a secret, do not guess one, and do not retry a 404 - the result is gone."},"current":{"type":"string","format":"uuid","description":"The uuid a scan submission returned. The uuid is the whole of the authorisation. Whoever has it can read that scan and nobody else can, there is no endpoint that lists scans, and an unknown, malformed or expired uuid all answer 404 alike. Treat it as a secret, do not guess one, and do not retry a 404 - the result is gone."}}},"steps":[{"stepId":"readBaselineDocument","description":"The whole scanner document rather than the summary: a comparison is arithmetic over findings, and the summary does not carry all of them.","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1scans~1{identifier}~1export~1{fmt}/get","parameters":[{"name":"identifier","in":"path","value":"$inputs.baseline"},{"name":"fmt","in":"path","value":"json"}],"successCriteria":[{"condition":"$statusCode == 200"}],"onFailure":[{"name":"baselineNotFinished","type":"retry","retryAfter":5,"retryLimit":36,"criteria":[{"condition":"$statusCode == 409"}]},{"name":"baselineUnknownOrExpired","type":"end","criteria":[{"condition":"$statusCode == 404"}]}],"outputs":{"baselineRating":"$response.body#/rating"}},{"stepId":"readCurrentDocument","description":"The later scan, read the same way. Reached only once the earlier one is known to still exist, so an expired baseline is reported before a second scan is spent.","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1scans~1{identifier}~1export~1{fmt}/get","parameters":[{"name":"identifier","in":"path","value":"$inputs.current"},{"name":"fmt","in":"path","value":"json"}],"successCriteria":[{"condition":"$statusCode == 200"}],"onFailure":[{"name":"currentNotFinished","type":"retry","retryAfter":5,"retryLimit":36,"criteria":[{"condition":"$statusCode == 409"}]},{"name":"currentUnknownOrExpired","type":"end","criteria":[{"condition":"$statusCode == 404"}]}],"outputs":{"currentRating":"$response.body#/rating"}}],"outputs":{"baselineRating":"$steps.readBaselineDocument.outputs.baselineRating","currentRating":"$steps.readCurrentDocument.outputs.currentRating"}},{"workflowId":"exportFinishedScan","summary":"Download a finished scan as a file.","description":"JSON, CSV, SARIF and PDF are four renderings of the same finished result.\n\n409 from an export means the scan exists but has not finished: wait 5 seconds and ask again, up to 36 times. 404 means the scan is unknown or expired and no amount of waiting will change that. Never treat the two alike.\n\nResults expire. Once the retention window passes the uuid stops resolving and answers 404 like any unknown one, so export or read what is needed rather than storing a uuid for later.","inputs":{"type":"object","required":["identifier"],"properties":{"identifier":{"type":"string","format":"uuid","description":"The uuid a scan submission returned. The uuid is the whole of the authorisation. Whoever has it can read that scan and nobody else can, there is no endpoint that lists scans, and an unknown, malformed or expired uuid all answer 404 alike. Treat it as a secret, do not guess one, and do not retry a 404 - the result is gone."},"format":{"type":"string","enum":["json","csv","sarif","pdf","html","remediation-md","remediation-html"],"default":"pdf"}}},"steps":[{"stepId":"downloadExport","description":"409 and 404 are different answers to different questions, and the two failure actions below are the whole point of this workflow: wait for the first, stop on the second.","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1scans~1{identifier}~1export~1{fmt}/get","parameters":[{"name":"identifier","in":"path","value":"$inputs.identifier"},{"name":"fmt","in":"path","value":"$inputs.format"}],"successCriteria":[{"condition":"$statusCode == 200"}],"onFailure":[{"name":"existsButNotFinished","type":"retry","retryAfter":5,"retryLimit":36,"criteria":[{"condition":"$statusCode == 409"}]},{"name":"unknownExpiredOrUnsupportedFormat","type":"end","criteria":[{"condition":"$statusCode == 404"}]}],"outputs":{"contentType":"$response.header.content-type","disposition":"$response.header.content-disposition"}}],"outputs":{"contentType":"$steps.downloadExport.outputs.contentType","disposition":"$steps.downloadExport.outputs.disposition"}},{"workflowId":"eraseInstanceData","summary":"Erase everything held for one instance, and keep the proof.","description":"The erasure half of GDPR Article 17, for an operator answering a request about an instance they run. Destructive and irreversible: it deletes results belonging to whoever is currently reading them.\n\nIt is authorised with the operator's purge credential, presented as a bearer token. There is no way to obtain one from this API. An automated caller that does not already hold it must stop and ask a human rather than retrying, and must never put the credential anywhere but the Authorization header.\n\nThe answer is a receipt: counts of what was removed, a second pass confirming nothing matching was left, and an HMAC over the whole of it when the deployment configured a signing key. Keep it - the data it describes no longer exists to be shown. A deployment with no credential configured has no erasure endpoint at all and answers 404.","inputs":{"type":"object","required":["target","authorization"],"properties":{"target":{"type":"string","description":"The instance hostname, with or without a scheme.","examples":["opencloud.example.com"]},"authorization":{"type":"string","format":"password","writeOnly":true,"description":"The operator's purge credential as a bearer token, written 'Bearer <token>'. Supplied by the operator, never echoed back, never logged."}}},"steps":[{"stepId":"eraseTarget","operationPath":"{$sourceDescriptions.checkOpenCloudSecurityApi.url}#/paths/~1api~1purge/delete","parameters":[{"name":"target","in":"query","value":"$inputs.target"},{"name":"Authorization","in":"header","value":"$inputs.authorization"}],"successCriteria":[{"condition":"$statusCode == 200"},{"context":"$response.body","condition":"$.complete == true","type":"jsonpath"}],"onFailure":[{"name":"notAuthorised","type":"end","criteria":[{"condition":"$statusCode == 401"}]},{"name":"purgeNotDeployed","type":"end","criteria":[{"condition":"$statusCode == 404"}]},{"name":"targetNotUsable","type":"end","criteria":[{"condition":"$statusCode == 422"}]}],"outputs":{"receiptId":"$response.body#/receiptId","issuedAt":"$response.body#/issuedAt","target":"$response.body#/target","deleted":"$response.body#/deleted","remaining":"$response.body#/remaining","complete":"$response.body#/complete","signature":"$response.body#/signature"}}],"outputs":{"receiptId":"$steps.eraseTarget.outputs.receiptId","issuedAt":"$steps.eraseTarget.outputs.issuedAt","target":"$steps.eraseTarget.outputs.target","deleted":"$steps.eraseTarget.outputs.deleted","remaining":"$steps.eraseTarget.outputs.remaining","complete":"$steps.eraseTarget.outputs.complete","signature":"$steps.eraseTarget.outputs.signature"}}]}