Skip to content

Commit

Permalink
[backend/frontend] Command Details in execution traces (#1232)
Browse files Browse the repository at this point in the history
Signed-off-by: Damien Goujard <[email protected]>
  • Loading branch information
damgouj authored Sep 18, 2024
1 parent d197cea commit 900923f
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,28 @@ public ExecutionProcess process(@NotNull final Execution execution, @NotNull fin
return new ExecutionProcess(true, expectations);
}

@Override
public InjectStatusCommandLine getCommandsLines(String externalId) {
InjectStatusCommandLine commandLine = new InjectStatusCommandLine();
Set<String> contents = new HashSet<>();
Set<String> cleanCommands = new HashSet<>();
Ability ability = calderaService.findAbilityById(externalId);
if(ability != null) {
ability.getExecutors().forEach(executor -> {
if(executor.getCommand() != null && !executor.getCommand().isBlank()) {
contents.add(executor.getCommand());
}
if(executor.getCleanup() != null && !executor.getCleanup().isEmpty()) {
cleanCommands.addAll(executor.getCleanup());
}
});
}
commandLine.setExternalId(externalId);
commandLine.setContent(contents.stream().toList());
commandLine.setCleanupCommand(cleanCommands.stream().toList());
return commandLine;
}

// -- PRIVATE --

private Map<Asset, Boolean> resolveAllAssets(@NotNull final ExecutableInject inject) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ public List<Ability> abilities() {
}
}

public Ability findAbilityById(String abilityId) {
try {
String jsonResponse = this.get(this.config.getRestApiV2Url() + ABILITIES_URI + "/" + abilityId);
return this.objectMapper.readValue(jsonResponse, new TypeReference<>() {
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public Ability createAbility(Map<String, Object> body) {
try {
String jsonResponse = this.post(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import java.util.List;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Executor {

private String name;
private String platform;
private String command;
private List<String> cleanup;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public List<Ability> abilities() {
return this.client.abilities();
}

public Ability findAbilityById(String abilityId) {
return this.client.findAbilityById(abilityId);
}

public String exploit(
@NotBlank final String obfuscator,
@NotBlank final String paw,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.openbas.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.stereotype.Component;

import java.sql.Statement;

@Component
public class V3_39__Add_column_status_commands_lines extends BaseJavaMigration {

@Override
public void migrate(Context context) throws Exception {
Statement select = context.getConnection().createStatement();
select.execute("ALTER TABLE injects_statuses ADD status_commands_lines text;");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public class InjectResultDTO {
@JsonProperty("inject_content")
private ObjectNode content;

@JsonProperty("inject_commands_lines")
private InjectStatusCommandLine commandsLines;

@JsonProperty("inject_expectations")
private List<InjectExpectation> expectations;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.openbas.execution.ExecutableInject;
import io.openbas.execution.ExecutionExecutorService;
import io.openbas.helper.InjectHelper;
import io.openbas.service.AtomicTestingService;
import jakarta.annotation.Resource;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
Expand Down Expand Up @@ -45,6 +46,7 @@ public class InjectsExecutionJob implements Job {
private final ExerciseRepository exerciseRepository;
private final QueueService queueService;
private final ExecutionExecutorService executionExecutorService;
private final AtomicTestingService atomicTestingService;

private final List<ExecutionStatus> executionStatusesNotReady =
List.of(ExecutionStatus.QUEUING, ExecutionStatus.DRAFT, ExecutionStatus.EXECUTING, ExecutionStatus.PENDING);
Expand Down Expand Up @@ -162,6 +164,7 @@ private void executeInject(ExecutableInject executableInject) {
status.getTraces().add(InjectStatusExecution.traceError(errorMsg));
status.setName(ExecutionStatus.ERROR);
status.setTrackingSentDate(Instant.now());
status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject));
injectStatusRepository.save(status);
} else {
inject.getInjectorContract().ifPresent(injectorContract -> {
Expand All @@ -174,12 +177,14 @@ private void executeInject(ExecutableInject executableInject) {
status.setName(ExecutionStatus.ERROR);
status.setTrackingSentDate(Instant.now());
status.setInject(inject);
status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject));
injectStatusRepository.save(status);
} else {
InjectStatus status = inject.getStatus().get();
status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)"));
status.setName(ExecutionStatus.ERROR);
status.setTrackingSentDate(Instant.now());
status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject));
injectStatusRepository.save(status);
}
return;
Expand All @@ -197,11 +202,13 @@ private void executeInject(ExecutableInject executableInject) {
status.setName(ExecutionStatus.EXECUTING);
status.setTrackingSentDate(Instant.now());
status.setInject(inject);
status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject));
injectStatusRepository.save(status);
} else {
InjectStatus status = inject.getStatus().get();
status.setName(ExecutionStatus.EXECUTING);
status.setTrackingSentDate(Instant.now());
status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject));
injectStatusRepository.save(status);
}
newExecutableInject = this.executionExecutorService.launchExecutorContext(executableInject, inject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.java.Log;
import org.hibernate.Hibernate;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
Expand All @@ -43,6 +46,7 @@

import static io.openbas.config.SessionHelper.currentUser;
import static io.openbas.database.criteria.GenericCriteria.countQuery;
import static io.openbas.database.model.Command.COMMAND_TYPE;
import static io.openbas.helper.StreamHelper.fromIterable;
import static io.openbas.helper.StreamHelper.iterableToSet;
import static io.openbas.utils.AtomicTestingUtils.*;
Expand Down Expand Up @@ -71,18 +75,26 @@ public class AtomicTestingService {
private final TeamRepository teamRepository;
private final TagRepository tagRepository;
private final DocumentRepository documentRepository;
private ApplicationContext context;

private static final String PRE_DEFINE_EXPECTATIONS = "predefinedExpectations";
private static final String EXPECTATIONS = "expectations";

@PersistenceContext
private EntityManager entityManager;

@Autowired
public void setContext(ApplicationContext context) {
this.context = context;
}

public InjectResultDTO findById(String injectId) {
Optional<Inject> inject = injectRepository.findWithStatusById(injectId);
return inject
InjectResultDTO result = inject
.map(AtomicTestingMapper::toDtoWithTargetResults)
.orElseThrow(ElementNotFoundException::new);
result.setCommandsLines(getCommandsLinesFromInject(inject.get()));
return result;
}

@Transactional
Expand Down Expand Up @@ -414,4 +426,25 @@ private List<AtomicTestingOutput> execAtomicTesting(TypedQuery<Tuple> query) {
.toList();
}

public InjectStatusCommandLine getCommandsLinesFromInject(final Inject inject) {
if (inject.getStatus().isPresent() && inject.getStatus().get().getCommandsLines() != null) {
// Commands lines saved because inject has been executed
return inject.getStatus().get().getCommandsLines();
} else if (inject.getInjectorContract().isPresent()) {
InjectorContract injectorContract = inject.getInjectorContract().get();
if(injectorContract.getPayload() != null && COMMAND_TYPE.equals(injectorContract.getPayload().getType())) {
// Inject has a command payload
Payload payload = injectorContract.getPayload();
Command payloadCommand = (Command) Hibernate.unproxy(payload);
return new InjectStatusCommandLine(!payloadCommand.getContent().isBlank() ? List.of(payloadCommand.getContent()) : null,
!payloadCommand.getCleanupCommand().isBlank() ? List.of(payload.getCleanupCommand()) : null, payload.getExternalId());
} else {
// Inject comes from Caldera ability and tomorrow from other(s) Executor(s)
io.openbas.execution.Injector executor = context.getBean(injectorContract.getInjector().getType(), io.openbas.execution.Injector.class);
return executor.getCommandsLines(injectorContract.getId());
}
}
return null;
}

}
154 changes: 154 additions & 0 deletions openbas-api/src/test/java/io/openbas/rest/AtomicTestingApiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package io.openbas.rest;

import com.jayway.jsonpath.JsonPath;
import io.openbas.IntegrationTest;
import io.openbas.database.model.*;
import io.openbas.database.repository.InjectRepository;
import io.openbas.database.repository.InjectStatusRepository;
import io.openbas.database.repository.InjectorContractRepository;
import io.openbas.utils.mockUser.WithMockAdminUser;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.time.Instant;
import java.util.List;

import static io.openbas.injectors.email.EmailContract.EMAIL_DEFAULT;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

@TestInstance(PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AtomicTestingApiTest extends IntegrationTest {

public static final String ATOMIC_TESTINGS_URI = "/api/atomic_testings";

static Inject INJECT_WITH_PAYLOAD;
static Inject INJECT_WITHOUT_PAYLOAD;
static InjectStatus INJECT_STATUS;
static String NEW_INJECT_ID;

@Autowired
private MockMvc mvc;
@Autowired
private InjectRepository injectRepository;
@Autowired
private InjectorContractRepository injectorContractRepository;
@Autowired
private InjectStatusRepository injectStatusRepository;

@BeforeAll
void beforeAll() {
Inject injectToCreate1 = new Inject();
injectToCreate1.setTitle("Inject without payload");
injectToCreate1.setCreatedAt(Instant.now());
injectToCreate1.setUpdatedAt(Instant.now());
injectToCreate1.setDependsDuration(0L);
injectToCreate1.setEnabled(true);
injectToCreate1.setInjectorContract(injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow());
INJECT_WITHOUT_PAYLOAD = injectRepository.save(injectToCreate1);

Inject injectToCreate2 = new Inject();
injectToCreate2.setTitle("Inject with payload");
injectToCreate2.setCreatedAt(Instant.now());
injectToCreate2.setUpdatedAt(Instant.now());
injectToCreate2.setDependsDuration(0L);
injectToCreate2.setEnabled(true);
injectToCreate2.setInjectorContract(injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow());
INJECT_WITH_PAYLOAD = injectRepository.save(injectToCreate2);
InjectStatus injectStatus = new InjectStatus();
injectStatus.setInject(injectToCreate2);
injectStatus.setTrackingSentDate(Instant.now());
injectStatus.setName(ExecutionStatus.SUCCESS);
injectStatus.setCommandsLines(new InjectStatusCommandLine(List.of("cmd"), List.of("clean cmd"), "id1234567"));
INJECT_STATUS = injectStatusRepository.save(injectStatus);
}

@DisplayName("Find an atomic testing without payload")
@Test
@WithMockAdminUser
@Order(1)
void findAnAtomicTestingTestWithoutPayload() throws Exception {
String response = mvc.perform(get(ATOMIC_TESTINGS_URI + "/" + INJECT_WITHOUT_PAYLOAD.getId())
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
// -- ASSERT --
assertNotNull(response);
assertEquals(INJECT_WITHOUT_PAYLOAD.getId(), JsonPath.read(response, "$.inject_id"));
assertNull(JsonPath.read(response, "$.inject_commands_lines"));
}

@DisplayName("Find an atomic testing with payload")
@Test
@WithMockAdminUser
@Order(2)
void findAnAtomicTestingTestWithPayload() throws Exception {
String response = mvc.perform(get(ATOMIC_TESTINGS_URI + "/" + INJECT_WITH_PAYLOAD.getId())
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
// -- ASSERT --
assertNotNull(response);
assertEquals(INJECT_WITH_PAYLOAD.getId(), JsonPath.read(response, "$.inject_id"));
assertNotNull(JsonPath.read(response, "$.inject_commands_lines"));
}

@DisplayName("Duplicate and delete an atomic testing")
@Test
@WithMockAdminUser
@Order(3)
void duplicateAndDeleteAtomicTestingTest() throws Exception {
// Duplicate
String response = mvc.perform(post(ATOMIC_TESTINGS_URI + "/" + INJECT_WITHOUT_PAYLOAD.getId())
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
assertNotNull(response);
// Assert duplicate
NEW_INJECT_ID = JsonPath.read(response, "$.inject_id");
response = mvc.perform(get(ATOMIC_TESTINGS_URI + "/" + NEW_INJECT_ID)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
assertEquals(NEW_INJECT_ID, JsonPath.read(response, "$.inject_id"));
// Delete
response = mvc.perform(delete(ATOMIC_TESTINGS_URI + "/" + NEW_INJECT_ID)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
assertNotNull(response);
// Assert delete
response = mvc.perform(get(ATOMIC_TESTINGS_URI + "/" + NEW_INJECT_ID)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is4xxClientError())
.andReturn()
.getResponse()
.getContentAsString();
assertNotNull(response);
}

@AfterAll
void afterAll() {
injectStatusRepository.delete(INJECT_STATUS);
injectRepository.delete(INJECT_WITH_PAYLOAD);
injectRepository.delete(INJECT_WITHOUT_PAYLOAD);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public void setFileService(FileService fileService) {

public abstract ExecutionProcess process(Execution execution, ExecutableInject injection) throws Exception;

public InjectStatusCommandLine getCommandsLines(String externalId) {
return null;
}

private InjectExpectation expectationConverter(
@NotNull final ExecutableInject executableInject,
Expectation expectation) {
Expand Down
Loading

0 comments on commit 900923f

Please sign in to comment.