File size: 16,040 Bytes
9373c61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5cfe5c4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package com.dalab.policyengine.service;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.api.Rules;
import org.jeasy.rules.api.RulesEngine;
import org.jeasy.rules.core.DefaultRulesEngine;
import org.jeasy.rules.mvel.MVELRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import com.dalab.common.event.AssetChangeEvent;
import com.dalab.policyengine.common.ResourceNotFoundException;
import com.dalab.policyengine.dto.PolicyEvaluationOutputDTO;
import com.dalab.policyengine.dto.PolicyEvaluationRequestDTO;
import com.dalab.policyengine.dto.PolicyEvaluationSummaryDTO;
import com.dalab.policyengine.kafka.producer.PolicyActionProducer;
import com.dalab.policyengine.mapper.PolicyMapper;
import com.dalab.policyengine.model.Policy;
import com.dalab.policyengine.model.PolicyEvaluation;
import com.dalab.policyengine.model.PolicyEvaluationStatus;
import com.dalab.policyengine.model.PolicyRule;
import com.dalab.policyengine.model.PolicyStatus;
import com.dalab.policyengine.repository.PolicyEvaluationRepository;
import com.dalab.policyengine.repository.PolicyRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.Timestamp;

import jakarta.persistence.criteria.Predicate;

@Service
@Transactional
public class PolicyEvaluationService implements IPolicyEvaluationService {

    private static final Logger log = LoggerFactory.getLogger(PolicyEvaluationService.class);

    private final PolicyRepository policyRepository;
    private final PolicyEvaluationRepository policyEvaluationRepository;
    private final PolicyMapper policyMapper;
    private final RulesEngine rulesEngine;
    private final ObjectMapper objectMapper; // For converting asset data and actions
    private final PolicyActionProducer policyActionProducer;

    @Autowired
    public PolicyEvaluationService(PolicyRepository policyRepository,

                                 PolicyEvaluationRepository policyEvaluationRepository,

                                 PolicyMapper policyMapper,

                                 ObjectMapper objectMapper,

                                 PolicyActionProducer policyActionProducer) {
        this.policyRepository = policyRepository;
        this.policyEvaluationRepository = policyEvaluationRepository;
        this.policyMapper = policyMapper;
        this.objectMapper = objectMapper;
        this.rulesEngine = new DefaultRulesEngine();
        this.policyActionProducer = policyActionProducer;
    }

    @Override
    public PolicyEvaluationOutputDTO evaluatePolicyForAsset(UUID policyId, PolicyEvaluationRequestDTO evaluationRequest, UUID triggeredByUserId) {
        Policy policy = policyRepository.findById(policyId)
                .orElseThrow(() -> new ResourceNotFoundException("Policy", "id", policyId.toString()));

        if (policy.getStatus() == PolicyStatus.DISABLED) {
            log.warn("Policy {} is disabled. Evaluation skipped for asset {}.", policy.getName(), evaluationRequest.getTargetAssetId());
            PolicyEvaluation evaluation = createEvaluationRecord(policy, evaluationRequest.getTargetAssetId(), PolicyEvaluationStatus.NOT_APPLICABLE, triggeredByUserId, Collections.singletonMap("reason", "Policy disabled"), null);
            return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy.getName());
        }

        Map<String, Object> assetData = fetchAssetData(evaluationRequest.getTargetAssetId());
        if (assetData == null || assetData.isEmpty()) {
            log.error("Asset data not found for assetId: {}. Evaluation cannot proceed.", evaluationRequest.getTargetAssetId());
            PolicyEvaluation evaluation = createEvaluationRecord(policy, evaluationRequest.getTargetAssetId(), PolicyEvaluationStatus.ERROR, triggeredByUserId, Collections.singletonMap("error", "Asset data not found"), null);
            return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy.getName());
        }

        Facts facts = new Facts();
        facts.put("asset", assetData); 
        if (evaluationRequest.getEvaluationContext() != null) {
            evaluationRequest.getEvaluationContext().forEach(facts::put);
        }

        Rules easyRules = new Rules();
        Map<String, Boolean> ruleResults = new HashMap<>();
        for (PolicyRule ruleEntity : policy.getRules()) {
            Rule easyRule = new MVELRule()
                    .name(ruleEntity.getName())
                    .description(ruleEntity.getDescription())
                    .priority(ruleEntity.getPriority())
                    .when(ruleEntity.getCondition())
                    .then("ruleResults.put(\"" + ruleEntity.getName() + "\", true); " +
                          "log.debug(\"Rule '{}' evaluated to true for asset {}\", \"" + ruleEntity.getName() + "\", evaluationRequest.getTargetAssetId());");
            easyRules.register(easyRule);
        }

        rulesEngine.fire(easyRules, facts);

        boolean overallPolicyConditionMet = true;
        if (StringUtils.hasText(policy.getConditionLogic())) {
            overallPolicyConditionMet = ruleResults.values().stream().allMatch(Boolean::booleanValue) && !ruleResults.isEmpty();
            log.debug("Policy condition logic '{}' evaluated to {} based on rule results: {}", policy.getConditionLogic(), overallPolicyConditionMet, ruleResults);
        } else {
            overallPolicyConditionMet = ruleResults.values().stream().allMatch(Boolean::booleanValue) && !ruleResults.isEmpty();
            log.debug("No policy condition logic. Overall result based on all rules: {}. Rule results: {}", overallPolicyConditionMet, ruleResults);
        }
        PolicyEvaluationStatus finalStatus = overallPolicyConditionMet ? PolicyEvaluationStatus.PASS : PolicyEvaluationStatus.FAIL;
        Map<String, Object> triggeredPolicyActionsSummary = null;

        PolicyEvaluation evaluation = createEvaluationRecord(policy, evaluationRequest.getTargetAssetId(), finalStatus, triggeredByUserId, 
            Map.of("rulesEvaluated", ruleResults, "factsSnapshot", objectMapper.convertValue(facts.asMap(), new TypeReference<Map<String, Object>>() {})), 
            null);

        if (finalStatus == PolicyEvaluationStatus.PASS) {
            log.info("Policy '{}' PASSED for asset '{}'", policy.getName(), evaluationRequest.getTargetAssetId());
            triggeredPolicyActionsSummary = executePolicyActions(policy, evaluationRequest.getTargetAssetId(), assetData, facts, evaluation.getId());
            evaluation.setTriggeredActions(triggeredPolicyActionsSummary); 
            policyEvaluationRepository.save(evaluation); 
        } else {
            log.info("Policy '{}' FAILED for asset '{}'", policy.getName(), evaluationRequest.getTargetAssetId());
        }
        
        return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy.getName());
    }

    @Override
    public PolicyEvaluationOutputDTO triggerPolicyEvaluation(UUID policyId, PolicyEvaluationRequestDTO evaluationRequest, UUID triggeredByUserId) {
        // Delegate to the main evaluation method
        return evaluatePolicyForAsset(policyId, evaluationRequest, triggeredByUserId);
    }

    private PolicyEvaluation createEvaluationRecord(Policy policy, String targetAssetId, PolicyEvaluationStatus status, UUID triggeredByUserId, Map<String, Object> details, Map<String, Object> triggeredActions) {
        PolicyEvaluation evaluation = new PolicyEvaluation();
        evaluation.setPolicyId(policy.getId());
        evaluation.setTargetAssetId(targetAssetId);
        evaluation.setStatus(status);
        evaluation.setEvaluationDetails(details);
        evaluation.setTriggeredActions(triggeredActions);
        evaluation.setEvaluatedAt(Instant.now());
        evaluation.setEvaluationTriggeredByUserId(triggeredByUserId);
        return policyEvaluationRepository.save(evaluation);
    }

    private Map<String, Object> fetchAssetData(String assetId) {
        log.debug("Fetching asset data for assetId: {}", assetId);
        Map<String, Object> data = new HashMap<>();
        if ("asset123".equals(assetId)) {
            data.put("name", "Sensitive S3 Bucket");
            data.put("assetType", "S3_BUCKET");
            data.put("region", "us-east-1");
            data.put("tags", Arrays.asList("PII", "FinancialData"));
            data.put("publicAccess", "true");
            data.put("sizeGB", 1024);
        } else if ("asset456".equals(assetId)) {
            data.put("name", "Dev EC2 Instance");
            data.put("assetType", "EC2_INSTANCE");
            data.put("region", "eu-west-1");
            data.put("tags", Arrays.asList("Development"));
            data.put("instanceType", "t2.micro");
        }
        return data.isEmpty() ? null : data;
    }

    private Map<String, Object> executePolicyActions(Policy policy, String assetId, Map<String, Object> assetData, Facts facts, UUID evaluationId) {
        if (policy.getActions() == null || policy.getActions().isEmpty()) {
            return Collections.emptyMap();
        }
        log.info("Executing actions for policy '{}' (id={}) on asset '{}': {}", 
            policy.getName(), policy.getId(), assetId, policy.getActions());

        Map<String, Object> executedActionsSummary = new HashMap<>();
        Instant now = Instant.now();
        Timestamp eventTimestamp = Timestamp.newBuilder().setSeconds(now.getEpochSecond()).setNanos(now.getNano()).build();

        final String effectiveAssetId = assetData.getOrDefault("id", assetId).toString(); // Prefer ID from assetData if available

        policy.getActions().forEach((actionType, actionConfig) -> {
            // For now, create a simple map-based event instead of protobuf until we add the protobuf definitions
            Map<String, Object> policyActionEvent = new HashMap<>();
            policyActionEvent.put("policyId", policy.getId().toString());
            policyActionEvent.put("assetId", effectiveAssetId);
            policyActionEvent.put("actionType", actionType);
            policyActionEvent.put("timestamp", now.toString());
            
            if (evaluationId != null) {
                policyActionEvent.put("evaluationId", evaluationId.toString());
            }
            
            if (actionConfig instanceof Map) {
                policyActionEvent.put("actionParameters", actionConfig);
            } else if (actionConfig != null) {
                policyActionEvent.put("actionParameters", Map.of("value", actionConfig));
            }

            // TODO: Replace with actual protobuf-based PolicyActionEvent when protobuf definitions are added
            log.info("Policy action event (simplified): {}", policyActionEvent);
            policyActionProducer.sendPolicyActionEvent(policyActionEvent);
            
            executedActionsSummary.put(actionType, actionConfig);
            log.debug("Created PolicyActionEvent for actionType: {} with assetId: {}", actionType, effectiveAssetId);
        });

        return executedActionsSummary;
    }

    @Override
    @Transactional(readOnly = true)
    public Page<PolicyEvaluationSummaryDTO> getPolicyEvaluations(Pageable pageable, UUID policyIdFilter, String targetAssetIdFilter, String statusFilter) {
        Specification<PolicyEvaluation> spec = (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (policyIdFilter != null) {
                predicates.add(criteriaBuilder.equal(root.get("policyId"), policyIdFilter));
            }
            if (StringUtils.hasText(targetAssetIdFilter)) {
                predicates.add(criteriaBuilder.equal(root.get("targetAssetId"), targetAssetIdFilter));
            }
            if (StringUtils.hasText(statusFilter)) {
                try {
                    predicates.add(criteriaBuilder.equal(root.get("status"), PolicyEvaluationStatus.valueOf(statusFilter.toUpperCase())));
                } catch (IllegalArgumentException e) {
                    log.warn("Invalid policy evaluation status provided for filtering: {}", statusFilter);
                    predicates.add(criteriaBuilder.disjunction());
                }
            }
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };

        Page<PolicyEvaluation> evaluationPage = policyEvaluationRepository.findAll(spec, pageable);
        
        List<UUID> policyIds = evaluationPage.getContent().stream()
                                .map(PolicyEvaluation::getPolicyId)
                                .distinct()
                                .collect(Collectors.toList());
        Map<UUID, String> policyNamesMap = Collections.emptyMap();
        if (!policyIds.isEmpty()) {
            policyNamesMap = policyRepository.findAllById(policyIds).stream()
                .collect(Collectors.toMap(Policy::getId, Policy::getName));
        }
        final Map<UUID, String> finalPolicyNamesMap = policyNamesMap;

        return evaluationPage.map(eval -> policyMapper.toPolicyEvaluationSummaryDTO(eval, finalPolicyNamesMap.getOrDefault(eval.getPolicyId(), "Unknown Policy")));
    }

    @Override
    @Transactional(readOnly = true)
    public PolicyEvaluationOutputDTO getPolicyEvaluationById(UUID evaluationId) {
        PolicyEvaluation evaluation = policyEvaluationRepository.findById(evaluationId)
                .orElseThrow(() -> new ResourceNotFoundException("PolicyEvaluation", "id", evaluationId.toString()));
        Policy policy = policyRepository.findById(evaluation.getPolicyId())
            .orElse(null); // Policy might be deleted, handle gracefully
        return policyMapper.toPolicyEvaluationOutputDTO(evaluation, policy != null ? policy.getName() : "Unknown/Deleted Policy");
    }

    @Override
    public void evaluatePolicyForAssetInternal(AssetChangeEvent assetChangeEvent, UUID eventInitiatorId) {
        String assetIdStr = assetChangeEvent.getAssetId();
        log.info("Processing AssetChangeEvent for assetId: {}, eventType: {}", assetIdStr, assetChangeEvent.getEventType());

        List<Policy> activePolicies = policyRepository.findByStatus(PolicyStatus.ENABLED);
        if (activePolicies.isEmpty()) {
            log.info("No active policies found. No evaluations will be triggered for asset {}", assetIdStr);
            return;
        }

        log.info("Found {} active policies. Triggering evaluations for asset {}...", activePolicies.size(), assetIdStr);

        for (Policy policy : activePolicies) {
            try {
                PolicyEvaluationRequestDTO requestDTO = new PolicyEvaluationRequestDTO();
                requestDTO.setTargetAssetId(assetIdStr);
                evaluatePolicyForAsset(policy.getId(), requestDTO, eventInitiatorId);

            } catch (Exception e) {
                log.error("Error during evaluation of policy {} for asset {}: {}", policy.getName(), assetIdStr, e.getMessage(), e);
            }
        }
        log.info("Finished processing asset change event for assetId: {}", assetIdStr);
    }
}