Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@
## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당)
**Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다.
**Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다.
## 2023-11-20 - [String.join Performance Issue]
**Learning:** `String.join` internally creates varargs and Stream processing overhead when used to serialize structured ledger records.
**Action:** Replace `String.join` in `ArtifactLinkLedger` and `KpiSnapshotLedger` with chained `StringBuilder.append()` for significant memory and processing optimization.
8 changes: 8 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@
**Vulnerability:** The document hashing routine in `DefaultDocumentConversionService` processed file streams without enforcing any maximum size limit on the bytes read. An attacker could exploit this by uploading a maliciously large stream (or exploiting a compression bomb if unzipping), exhausting system memory, CPU, or disk space (DoS).
**Learning:** Checking the declared file size (e.g., `file.getSize()`) in initial validation is not always sufficient if the input stream itself can be spoofed or dynamically expanded during reading. The actual bytes read must be verified against bounds continuously.
**Prevention:** Always enforce a strict, configurable size limit (e.g., `ConversionProperties.maxUploadSizeBytes`) within the `while` loop that reads from untrusted input streams. Track `totalRead` and throw an exception immediately if the limit is exceeded.
## 2026-09-30 - [jackson-databind vulnerabilities]
**Vulnerability:** Trivy-FS detected multiple High and Medium severity CVEs (CVE-2026-68497, CVE-2026-91776, CVE-2026-91777, etc.) associated with `jackson-databind` inside `pom.xml`.
**Learning:** `jackson-databind` vulnerabilities regularly surface when BOM properties referencing out-of-date versions (like `2.22.1`) are present in Maven `pom.xml`.
**Prevention:** Increment the BOM version parameter (e.g. `jackson-bom.version`) in `pom.xml` instead of defining a direct dependency override, ensuring all Jackson-related packages are upgraded symmetrically to resolve vulnerabilities.
## 2026-09-30 - [jackson-databind vulnerabilities part 2]
**Vulnerability:** Jackson `2.22.2` is affected by CVE-2026-91776 and CVE-2026-91777 and was flagged by an exact-head admission correction rule.
**Learning:** Checking the latest versions or canonical owner repositories for terminal GREEN status is necessary when fixing vulnerabilities.
**Prevention:** Always bump to the safe root-cause repair version (`2.22.3`) as verified by the canonical dependency owner to ensure the PR can be ordinarily integrated.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
OSV config contains a narrow temporary ignore because OSV Scanner
v2.3.8 still reports 2.22.1 even though the advisory text names it as
patched. -->
<jackson-bom.version>2.22.1</jackson-bom.version>
<jackson-bom.version>2.22.3</jackson-bom.version>
<!-- Security override: GHSA-jhq6-gfmj-v8fx fixes logback-core in 1.5.35. -->
<logback.version>1.5.35</logback.version>
<!-- Security override: CVE-2025-68161 / CVE-2026-34477 / CVE-2026-34478 / CVE-2026-34480. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,20 @@ private void appendLine(String line) {
}

private static String serialize(KpiSnapshotRecord record) {
return String.join("\t",
SNAPSHOT,
field(record.tenantId()),
field(record.subjectId()),
field(record.exportedAt()),
String.valueOf(record.totalJobs()),
String.valueOf(record.submittedJobs()),
String.valueOf(record.processingJobs()),
String.valueOf(record.succeededJobs()),
String.valueOf(record.failedJobs()),
String.valueOf(record.deadLetteredJobs()),
String.valueOf(record.conversionSuccessRate()),
field(record.p95TimeToPreviewMs())
);
return new StringBuilder(256)
.append(SNAPSHOT).append('\t')
.append(field(record.tenantId())).append('\t')
.append(field(record.subjectId())).append('\t')
.append(field(record.exportedAt())).append('\t')
.append(record.totalJobs()).append('\t')
.append(record.submittedJobs()).append('\t')
.append(record.processingJobs()).append('\t')
.append(record.succeededJobs()).append('\t')
.append(record.failedJobs()).append('\t')
.append(record.deadLetteredJobs()).append('\t')
.append(record.conversionSuccessRate()).append('\t')
.append(field(record.p95TimeToPreviewMs()))
.toString();
}

private static String field(String value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,46 +232,46 @@ private void appendLine(String line) {
}

private static String serializeIssued(ArtifactLinkRecord record) {
return String.join("\t",
ISSUED,
field(record.tokenId()),
field(record.tenantId()),
field(record.subjectId()),
record.docId().toString(),
field(record.scope()),
field(record.purpose()),
field(record.artifactChecksum()),
field(record.viewerSessionId()),
field(record.issuedAt()),
field(record.expiresAt()),
field(record.revokedAt()),
field(record.revokedBy()),
field(record.revokeReason())
);
return new StringBuilder(256)
.append(ISSUED).append('\t')
.append(field(record.tokenId())).append('\t')
.append(field(record.tenantId())).append('\t')
.append(field(record.subjectId())).append('\t')
.append(record.docId().toString()).append('\t')
.append(field(record.scope())).append('\t')
.append(field(record.purpose())).append('\t')
.append(field(record.artifactChecksum())).append('\t')
.append(field(record.viewerSessionId())).append('\t')
.append(field(record.issuedAt())).append('\t')
.append(field(record.expiresAt())).append('\t')
.append(field(record.revokedAt())).append('\t')
.append(field(record.revokedBy())).append('\t')
.append(field(record.revokeReason()))
.toString();
}

private static String serializeRevoked(ArtifactLinkRecord record) {
return String.join("\t",
REVOKED,
field(record.tokenId()),
field(record.revokedAt()),
field(record.revokedBy()),
field(record.revokeReason())
);
return new StringBuilder(128)
.append(REVOKED).append('\t')
.append(field(record.tokenId())).append('\t')
.append(field(record.revokedAt())).append('\t')
.append(field(record.revokedBy())).append('\t')
.append(field(record.revokeReason()))
.toString();
}

private static String serializeRead(ArtifactReadEvent event) {
return String.join("\t",
READ,
field(event.tenantId()),
field(event.subjectId()),
event.docId().toString(),
field(event.tokenId()),
field(event.rangeRequested()),
String.valueOf(event.statusCode()),
field(event.traceId()),
field(event.readAt())
);
return new StringBuilder(192)
.append(READ).append('\t')
.append(field(event.tenantId())).append('\t')
.append(field(event.subjectId())).append('\t')
.append(event.docId().toString()).append('\t')
.append(field(event.tokenId())).append('\t')
.append(field(event.rangeRequested())).append('\t')
.append(event.statusCode()).append('\t')
.append(field(event.traceId())).append('\t')
.append(field(event.readAt()))
.toString();
}

private static String field(String value) {
Expand Down
Loading