diff --git a/src/ostorlab/serve_app/oxo.py b/src/ostorlab/serve_app/oxo.py index 387ccf765..e1c8bc02e 100644 --- a/src/ostorlab/serve_app/oxo.py +++ b/src/ostorlab/serve_app/oxo.py @@ -292,11 +292,11 @@ def mutate( return ExportScanMutation(content=export_file_content) -class DeleteScanMutation(graphene.Mutation): +class DeleteScansMutation(graphene.Mutation): """Delete Scan & its information mutation.""" class Arguments: - scan_id = graphene.Int(required=True) + scan_ids = graphene.List(graphene.Int, required=True) result = graphene.Boolean() @@ -304,13 +304,13 @@ class Arguments: def mutate( root, info: graphql_base.ResolveInfo, - scan_id: int, - ) -> "DeleteScanMutation": + scan_ids: list[int], + ) -> "DeleteScansMutation": """Delete a scan & its information. Args: info: `graphql_base.ResolveInfo` instance. - scan_id: The scan ID. + scan_ids: The scan IDs. Raises: graphql.GraphQLError in case the scan does not exist. @@ -320,18 +320,23 @@ def mutate( """ with models.Database() as session: - scan_query = session.query(models.Scan).filter_by(id=scan_id) - if scan_query.count() == 0: - raise graphql.GraphQLError("Scan not found.") - scan_query.delete() - session.query(models.Vulnerability).filter_by(scan_id=scan_id).delete() - session.query(models.ScanStatus).filter_by(scan_id=scan_id).delete() - DeleteScanMutation._delete_assets(scan_id, session) - session.commit() - return DeleteScanMutation(result=True) + scans = ( + session.query(models.Scan).filter(models.Scan.id.in_(scan_ids)).all() + ) + if len(scans) == 0: + raise graphql.GraphQLError("No scan is found.") + + for scan in scans: + scan_query = session.query(models.Scan).filter_by(id=scan.id) + scan_query.delete() + session.query(models.Vulnerability).filter_by(scan_id=scan.id).delete() + session.query(models.ScanStatus).filter_by(scan_id=scan.id).delete() + DeleteScansMutation.delete_assets(scan.id, session) + session.commit() + return DeleteScansMutation(result=True) @staticmethod - def _delete_assets(scan_id: int, session: models.Database) -> None: + def delete_assets(scan_id: int, session: models.Database) -> None: """Delete assets. Args: @@ -481,21 +486,21 @@ def _validate(asset: types.OxoAssetInputType) -> Optional[str]: return None -class StopScanMutation(graphene.Mutation): +class StopScansMutation(graphene.Mutation): """Stop scan mutation.""" class Arguments: - scan_id = graphene.Int(required=True) + scan_ids = graphene.List(graphene.Int, required=True) - scan = graphene.Field(types.OxoScanType) + scans = graphene.List(types.OxoScanType) @staticmethod - def mutate(root, info: graphql_base.ResolveInfo, scan_id: int): + def mutate(root, info: graphql_base.ResolveInfo, scan_ids: list[int]): """Stop the desired scan. Args: info: `graphql_base.ResolveInfo` instance. - scan_id: The scan ID. + scan_ids: The scan IDs. Raises: graphql.GraphQLError in case the scan does not exist or the scan id is invalid. @@ -505,11 +510,14 @@ def mutate(root, info: graphql_base.ResolveInfo, scan_id: int): """ with models.Database() as session: - scan = session.query(models.Scan).get(scan_id) - if scan is None: - raise graphql.GraphQLError("Scan not found.") - local_runtime.LocalRuntime().stop(scan_id=str(scan_id)) - return StopScanMutation(scan=scan) + scans = ( + session.query(models.Scan).filter(models.Scan.id.in_(scan_ids)).all() + ) + if len(scans) == 0: + raise graphql.GraphQLError("No scan is found.") + for scan_id in scan_ids: + local_runtime.LocalRuntime().stop(scan_id=str(scan_id)) + return StopScansMutation(scans=scans) class PublishAgentGroupMutation(graphene.Mutation): @@ -867,7 +875,7 @@ def _run_scan_background( class Mutations(graphene.ObjectType): - delete_scan = DeleteScanMutation.Field( + delete_scans = DeleteScansMutation.Field( description="Delete a scan & all its information." ) delete_agent_group = DeleteAgentGroupMutation.Field( @@ -876,7 +884,7 @@ class Mutations(graphene.ObjectType): import_scan = ImportScanMutation.Field(description="Import scan from file.") export_scan = ExportScanMutation.Field(description="Export scan to file.") create_assets = CreateAssetsMutation.Field(description="Create an asset.") - stop_scan = StopScanMutation.Field( + stop_scans = StopScansMutation.Field( description="Stops running scan, scan is marked as stopped once the engine has completed cancellation." ) publish_agent_group = PublishAgentGroupMutation.Field( diff --git a/tests/serve_app/oxo_test.py b/tests/serve_app/oxo_test.py index 5ecbcb1ac..3c2d3adb4 100644 --- a/tests/serve_app/oxo_test.py +++ b/tests/serve_app/oxo_test.py @@ -711,7 +711,7 @@ def testQueryScan_whenScanDoesNotExist_returnErrorMessage( assert response.get_json()["errors"][0]["message"] == "Scan not found." -def testDeleteScanMutation_whenScanExist_deleteScanAndVulnz( +def testDeleteScansMutation_whenScanExist_deleteScanAndVulnz( authenticated_flask_client: testing.FlaskClient, android_scan: models.Scan ) -> None: """Ensure the delete scan mutation deletes the scan, its statuses & vulnerabilities.""" @@ -730,15 +730,15 @@ def testDeleteScanMutation_whenScanExist_deleteScanAndVulnz( ) query = """ - mutation DeleteScan ($scanId: Int!){ - deleteScan (scanId: $scanId) { + mutation DeleteScans ($scanIds: [Int]!){ + deleteScans (scanIds: $scanIds) { result } } """ response = authenticated_flask_client.post( - "/graphql", json={"query": query, "variables": {"scanId": android_scan.id}} + "/graphql", json={"query": query, "variables": {"scanIds": [android_scan.id]}} ) assert response.status_code == 200, response.get_json() @@ -763,24 +763,24 @@ def testDeleteScanMutation_whenScanExist_deleteScanAndVulnz( ) -def testDeleteScanMutation_whenScanDoesNotExist_returnErrorMessage( +def testDeleteScansMutation_whenScanDoesNotExist_returnErrorMessage( authenticated_flask_client: testing.FlaskClient, ) -> None: """Ensure the delete scan mutation returns an error message when the scan does not exist.""" query = """ - mutation DeleteScan ($scanId: Int!){ - deleteScan (scanId: $scanId) { + mutation DeleteScans ($scanIds: [Int]!){ + deleteScans (scanIds: $scanIds) { result } } """ response = authenticated_flask_client.post( - "/graphql", json={"query": query, "variables": {"scanId": 42}} + "/graphql", json={"query": query, "variables": {"scanIds": [42]}} ) assert response.status_code == 200, response.get_json() - assert response.get_json()["errors"][0]["message"] == "Scan not found." + assert response.get_json()["errors"][0]["message"] == "No scan is found." def testScansQuery_withPagination_shouldReturnPageInfo( @@ -2016,13 +2016,13 @@ def testQueryAssets_whenScanHasMultipleAssets_shouldReturnAllAssets( ] -def testStopScanMutation_whenScanIsRunning_shouldStopScan( +def testStopScansMutation_whenScanIsRunning_shouldStopScan( authenticated_flask_client: testing.FlaskClient, in_progress_web_scan: models.Scan, mocker: plugin.MockerFixture, db_engine_path: str, ) -> None: - """Test stopScan mutation when scan is running should stop scan.""" + """Test stopScans mutation when scan is running should stop scan.""" mocker.patch( "ostorlab.cli.docker_requirements_checker.is_docker_installed", return_value=True, @@ -2051,56 +2051,55 @@ def testStopScanMutation_whenScanIsRunning_shouldStopScan( scan = session.query(models.Scan).get(in_progress_web_scan.id) scan_progress = scan.progress query = """ - mutation stopScan($scanId: Int!){ - stopScan(scanId: $scanId){ - scan{ + mutation stopScans($scanIds: [Int]!){ + stopScans(scanIds: $scanIds){ + scans{ id } } } """ response = authenticated_flask_client.post( - "/graphql", json={"query": query, "variables": {"scanId": str(scan.id)}} + "/graphql", json={"query": query, "variables": {"scanIds": [scan.id]}} ) - assert response.status_code == 200, response.get_json() session.refresh(scan) scan = session.query(models.Scan).get(in_progress_web_scan.id) response_json = response.get_json() nbr_scans_after = session.query(models.Scan).count() assert response_json["data"] == { - "stopScan": {"scan": {"id": str(in_progress_web_scan.id)}} + "stopScans": {"scans": [{"id": str(in_progress_web_scan.id)}]} } assert scan.progress.name == "STOPPED" assert scan.progress != scan_progress assert nbr_scans_after == nbr_scans_before -def testStopScanMutation_whenNoScanFound_shouldReturnError( +def testStopScansMutation_whenNoScanFound_shouldReturnError( authenticated_flask_client: testing.FlaskClient, mocker: plugin.MockerFixture, db_engine_path: str, clean_db: None, ) -> None: - """Test stopScan mutation when scan doesn't exist should return error message.""" + """Test stopScans mutation when scan doesn't exist should return error message.""" del clean_db mocker.patch.object(models, "ENGINE_URL", db_engine_path) query = """ - mutation stopScan($scanId: Int!){ - stopScan(scanId: $scanId){ - scan{ + mutation stopScans($scanIds: [Int]!){ + stopScans(scanIds: $scanIds){ + scans{ id } } } """ response = authenticated_flask_client.post( - "/graphql", json={"query": query, "variables": {"scanId": "5"}} + "/graphql", json={"query": query, "variables": {"scanIds": ["5"]}} ) assert response.status_code == 200, response.get_json() response_json = response.get_json() - assert response_json["errors"][0]["message"] == "Scan not found." + assert response_json["errors"][0]["message"] == "No scan is found." def testQueryVulnerabilitiesOfKb_withPagination_shouldReturnPageInfo(