AuditChain.java

1
package com.rbctcsworld.ecommerce.audit;
2
3
import java.nio.charset.StandardCharsets;
4
import java.security.MessageDigest;
5
import java.security.NoSuchAlgorithmException;
6
import java.time.LocalDateTime;
7
import java.time.format.DateTimeFormatter;
8
import java.util.HexFormat;
9
import java.util.List;
10
11
/**
12
 * The hash chain that makes the audit trail tamper-evident.
13
 *
14
 * hash(row) = SHA-256 of the previous row's hash and every field of this row. Each part is written as
15
 * "length:value" (null as "-1:"), so no two different rows can produce the same text ("ab"+"c" vs "a"+"bc").
16
 * Changing a value, deleting a row or swapping two rows changes a hash that a later row depends on.
17
 */
18
public final class AuditChain {
19
20
    public static final String GENESIS = "0".repeat(64);
21
    private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
22
23
    private AuditChain() {
24
    }
25
26
    public static String hash(String prevHash, long seq, LocalDateTime occurredAt, String actor, String entityType,
27
                              long entityId, String action, String field, String oldValue, String newValue) {
28
        StringBuilder s = new StringBuilder();
29
        for (String part : new String[]{prevHash, Long.toString(seq), occurredAt.format(TIME), actor, entityType,
30
                Long.toString(entityId), action, field, oldValue, newValue}) {
31 2 1. hash : removed conditional - replaced equality check with false → KILLED
2. hash : removed conditional - replaced equality check with false → KILLED
            s.append(part == null ? -1 : part.length()).append(':').append(part == null ? "" : part).append('|');
32
        }
33
        try {
34
            byte[] digest = MessageDigest.getInstance("SHA-256").digest(s.toString().getBytes(StandardCharsets.UTF_8));
35 1 1. hash : replaced return value with "" for com/rbctcsworld/ecommerce/audit/AuditChain::hash → KILLED
            return HexFormat.of().formatHex(digest);
36
        } catch (NoSuchAlgorithmException e) {
37
            throw new IllegalStateException("SHA-256 is part of every JVM", e);
38
        }
39
    }
40
41
    static String hashOf(AuditRecord r) {
42 1 1. hashOf : replaced return value with "" for com/rbctcsworld/ecommerce/audit/AuditChain::hashOf → KILLED
        return hash(r.prevHash(), r.seq(), r.occurredAt(), r.actor(), r.entityType(), r.entityId(), r.action(),
43
                r.field(), r.oldValue(), r.newValue());
44
    }
45
46
    /** Result of checking the whole trail. brokenAtSeq = the first row that does not fit (null when valid). */
47
    public record Verification(boolean valid, int entries, Long brokenAtSeq, String reason) {
48
    }
49
50
    /**
51
     * Checks rows in seq order: numbering without gaps, each row linked to the one before, each hash correct,
52
     * and the last row is the one the head points to (otherwise the newest rows were removed).
53
     */
54
    public static Verification verify(List<AuditRecord> rows, long headSeq, String headHash) {
55
        Verification chain = verify(rows);
56 2 1. verify : removed conditional - replaced equality check with false → KILLED
2. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
        if (!chain.valid()) return chain;
57 2 1. verify : Replaced integer subtraction with addition → KILLED
2. verify : removed conditional - replaced equality check with false → KILLED
        String lastHash = rows.isEmpty() ? GENESIS : rows.get(rows.size() - 1).hash();
58 2 1. verify : removed conditional - replaced equality check with false → KILLED
2. verify : removed conditional - replaced equality check with false → KILLED
        if (rows.size() != headSeq || !lastHash.equals(headHash)) {
59 2 1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
2. verify : Replaced long addition with subtraction → KILLED
            return new Verification(false, rows.size(), (long) rows.size() + 1, "the newest rows are missing");
60
        }
61 1 1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
        return chain;
62
    }
63
64
    /** Checks rows in seq order: numbering without gaps, each row linked to the one before, each hash correct. */
65
    public static Verification verify(List<AuditRecord> rows) {
66
        String expectedPrev = GENESIS;
67
        long expectedSeq = 1;
68
        for (AuditRecord r : rows) {
69 1 1. verify : removed conditional - replaced equality check with false → KILLED
            if (r.seq() != expectedSeq) {
70 1 1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
                return new Verification(false, rows.size(), r.seq(), "row " + expectedSeq + " is missing");
71
            }
72 1 1. verify : removed conditional - replaced equality check with false → KILLED
            if (!r.prevHash().equals(expectedPrev)) {
73 1 1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
                return new Verification(false, rows.size(), r.seq(), "not linked to the row before");
74
            }
75 1 1. verify : removed conditional - replaced equality check with false → KILLED
            if (!hashOf(r).equals(r.hash())) {
76 1 1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
                return new Verification(false, rows.size(), r.seq(), "content does not match its hash");
77
            }
78
            expectedPrev = r.hash();
79 1 1. verify : Replaced long addition with subtraction → KILLED
            expectedSeq++;
80
        }
81 1 1. verify : replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED
        return new Verification(true, rows.size(), null, null);
82
    }
83
}

Mutations

31

1.1
Location : hash
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:theHashIsAKnownSha256OfLengthPrefixedFields()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : hash
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:theHashIsAKnownSha256OfLengthPrefixedFields()]
removed conditional - replaced equality check with false → KILLED

35

1.1
Location : hash
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:theHashIsAKnownSha256OfLengthPrefixedFields()]
replaced return value with "" for com/rbctcsworld/ecommerce/audit/AuditChain::hash → KILLED

42

1.1
Location : hashOf
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEditedValueIsFoundAtItsRow()]
replaced return value with "" for com/rbctcsworld/ecommerce/audit/AuditChain::hashOf → KILLED

56

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:aRemovedMiddleRowIsFound()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEditedValueIsFoundAtItsRow()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

57

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anUntouchedTrailIsValid()]
Replaced integer subtraction with addition → KILLED

2.2
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEmptyTrailIsValidWhenTheHeadIsStillAtGenesis()]
removed conditional - replaced equality check with false → KILLED

58

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEmptyTrailIsValidWhenTheHeadIsStillAtGenesis()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:aHeadThatPointsElsewhereWithTheSameCountIsAlsoReported()]
removed conditional - replaced equality check with false → KILLED

59

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:aHeadThatPointsElsewhereWithTheSameCountIsAlsoReported()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

2.2
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:aHeadThatPointsElsewhereWithTheSameCountIsAlsoReported()]
Replaced long addition with subtraction → KILLED

61

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEmptyTrailIsValidWhenTheHeadIsStillAtGenesis()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

69

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:aRemovedMiddleRowIsFound()]
removed conditional - replaced equality check with false → KILLED

70

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:aRemovedMiddleRowIsFound()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

72

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:theFirstRowMustStartFromGenesis()]
removed conditional - replaced equality check with false → KILLED

73

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:theFirstRowMustStartFromGenesis()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

75

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEditedValueIsFoundAtItsRow()]
removed conditional - replaced equality check with false → KILLED

76

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEditedValueIsFoundAtItsRow()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

79

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEditedValueIsFoundAtItsRow()]
Replaced long addition with subtraction → KILLED

81

1.1
Location : verify
Killed by : com.rbctcsworld.ecommerce.audit.AuditChainTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.audit.AuditChainTest]/[method:anEmptyTrailIsValidWhenTheHeadIsStillAtGenesis()]
replaced return value with null for com/rbctcsworld/ecommerce/audit/AuditChain::verify → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.0 support