@paniniapi/forensics 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asn1.d.ts +74 -0
- package/dist/asn1.d.ts.map +1 -0
- package/dist/asn1.js +186 -0
- package/dist/asn1.js.map +1 -0
- package/dist/consistency.d.ts +29 -0
- package/dist/consistency.d.ts.map +1 -0
- package/dist/consistency.js +163 -0
- package/dist/consistency.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf/content.d.ts +22 -0
- package/dist/pdf/content.d.ts.map +1 -0
- package/dist/pdf/content.js +91 -0
- package/dist/pdf/content.js.map +1 -0
- package/dist/pdf/signature.d.ts +40 -0
- package/dist/pdf/signature.d.ts.map +1 -0
- package/dist/pdf/signature.js +351 -0
- package/dist/pdf/signature.js.map +1 -0
- package/dist/pdf/structure.d.ts +31 -0
- package/dist/pdf/structure.d.ts.map +1 -0
- package/dist/pdf/structure.js +172 -0
- package/dist/pdf/structure.js.map +1 -0
- package/dist/report.d.ts +29 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +159 -0
- package/dist/report.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -4
- package/README.md +0 -3
package/dist/asn1.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Minimal DER (X.690) parser — enough surface to walk PKCS#7/CMS
|
|
18
|
+
* SignedData and X.509 certificate wrappers embedded in PDF digital
|
|
19
|
+
* signatures. Zero-dependency; throws `Asn1Error` on malformed input so
|
|
20
|
+
* callers can degrade to a signal rather than crash.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Asn1Error extends Error {
|
|
23
|
+
}
|
|
24
|
+
export interface Asn1Node {
|
|
25
|
+
/** Class: 0 universal, 1 application, 2 context-specific, 3 private. */
|
|
26
|
+
readonly tagClass: number;
|
|
27
|
+
readonly constructed: boolean;
|
|
28
|
+
readonly tagNumber: number;
|
|
29
|
+
/** Raw TLV bytes including tag+length — needed to re-sign structures. */
|
|
30
|
+
readonly tlv: Uint8Array;
|
|
31
|
+
/** Content octets (for primitives) or the concatenated children TLVs. */
|
|
32
|
+
readonly content: Uint8Array;
|
|
33
|
+
/** Parsed children for constructed nodes. */
|
|
34
|
+
readonly children: readonly Asn1Node[];
|
|
35
|
+
}
|
|
36
|
+
export declare const TAG: {
|
|
37
|
+
readonly EOC: 0;
|
|
38
|
+
readonly BOOLEAN: 1;
|
|
39
|
+
readonly INTEGER: 2;
|
|
40
|
+
readonly BIT_STRING: 3;
|
|
41
|
+
readonly OCTET_STRING: 4;
|
|
42
|
+
readonly NULL: 5;
|
|
43
|
+
readonly OBJECT_ID: 6;
|
|
44
|
+
readonly UTF8_STRING: 12;
|
|
45
|
+
readonly PRINTABLE_STRING: 19;
|
|
46
|
+
readonly TELETEX_STRING: 20;
|
|
47
|
+
readonly IA5_STRING: 22;
|
|
48
|
+
readonly UTC_TIME: 23;
|
|
49
|
+
readonly GENERALIZED_TIME: 24;
|
|
50
|
+
readonly SEQUENCE: 16;
|
|
51
|
+
readonly SET: 17;
|
|
52
|
+
};
|
|
53
|
+
/** Parse the single top-level TLV in `buf`. Trailing bytes are an error. */
|
|
54
|
+
export declare function parseDer(buf: Uint8Array): Asn1Node;
|
|
55
|
+
/**
|
|
56
|
+
* Parse a top-level TLV tolerating trailing 0x00 padding — PDF /Contents
|
|
57
|
+
* hex strings are padded with zeros to a fixed length per ISO 32000.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseDerPadded(buf: Uint8Array): Asn1Node;
|
|
60
|
+
/** Expect a universal tag; throws on mismatch. */
|
|
61
|
+
export declare function expectTag(node: Asn1Node, tag: number, what: string): Asn1Node;
|
|
62
|
+
/** Integer content → bigint (handles arbitrary size, positive only). */
|
|
63
|
+
export declare function intValue(node: Asn1Node): bigint;
|
|
64
|
+
/** OID content → dotted string. */
|
|
65
|
+
export declare function oidValue(node: Asn1Node): string;
|
|
66
|
+
/** Any text-ish string content → utf8-ish string. */
|
|
67
|
+
export declare function stringValue(node: Asn1Node): string;
|
|
68
|
+
/** Context-specific child with tag `n` (explicit-wrapped or direct). */
|
|
69
|
+
export declare function ctxTag(node: Asn1Node, n: number): Asn1Node | undefined;
|
|
70
|
+
/** Find first direct child matching class/tag. */
|
|
71
|
+
export declare function child(node: Asn1Node, tagClass: number, tagNumber: number): Asn1Node | undefined;
|
|
72
|
+
/** Find first direct child of a universal tag. */
|
|
73
|
+
export declare function universal(node: Asn1Node, tagNumber: number): Asn1Node | undefined;
|
|
74
|
+
//# sourceMappingURL=asn1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asn1.d.ts","sourceRoot":"","sources":["../src/asn1.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AAEH,qBAAa,SAAU,SAAQ,KAAK;CAAG;AAEvC,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,CAAC;CACxC;AAoBD,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;CAAY,CAAC;AAmD7B,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,CAIlD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,CAMxD;AAED,kDAAkD;AAClD,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAK7E;AAED,wEAAwE;AACxE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAK/C;AAED,mCAAmC;AACnC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAc/C;AAED,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAQlD;AAED,wEAAwE;AACxE,wBAAgB,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAGtE;AAED,kDAAkD;AAClD,wBAAgB,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAE/F;AAED,kDAAkD;AAClD,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAEjF"}
|
package/dist/asn1.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.TAG = exports.Asn1Error = void 0;
|
|
19
|
+
exports.parseDer = parseDer;
|
|
20
|
+
exports.parseDerPadded = parseDerPadded;
|
|
21
|
+
exports.expectTag = expectTag;
|
|
22
|
+
exports.intValue = intValue;
|
|
23
|
+
exports.oidValue = oidValue;
|
|
24
|
+
exports.stringValue = stringValue;
|
|
25
|
+
exports.ctxTag = ctxTag;
|
|
26
|
+
exports.child = child;
|
|
27
|
+
exports.universal = universal;
|
|
28
|
+
/**
|
|
29
|
+
* Minimal DER (X.690) parser — enough surface to walk PKCS#7/CMS
|
|
30
|
+
* SignedData and X.509 certificate wrappers embedded in PDF digital
|
|
31
|
+
* signatures. Zero-dependency; throws `Asn1Error` on malformed input so
|
|
32
|
+
* callers can degrade to a signal rather than crash.
|
|
33
|
+
*/
|
|
34
|
+
class Asn1Error extends Error {
|
|
35
|
+
}
|
|
36
|
+
exports.Asn1Error = Asn1Error;
|
|
37
|
+
const UNIVERSAL = {
|
|
38
|
+
EOC: 0,
|
|
39
|
+
BOOLEAN: 1,
|
|
40
|
+
INTEGER: 2,
|
|
41
|
+
BIT_STRING: 3,
|
|
42
|
+
OCTET_STRING: 4,
|
|
43
|
+
NULL: 5,
|
|
44
|
+
OBJECT_ID: 6,
|
|
45
|
+
UTF8_STRING: 12,
|
|
46
|
+
PRINTABLE_STRING: 19,
|
|
47
|
+
TELETEX_STRING: 20,
|
|
48
|
+
IA5_STRING: 22,
|
|
49
|
+
UTC_TIME: 23,
|
|
50
|
+
GENERALIZED_TIME: 24,
|
|
51
|
+
SEQUENCE: 16,
|
|
52
|
+
SET: 17,
|
|
53
|
+
};
|
|
54
|
+
exports.TAG = UNIVERSAL;
|
|
55
|
+
function readLength(buf, off) {
|
|
56
|
+
if (off >= buf.length)
|
|
57
|
+
throw new Asn1Error('DER: truncated length');
|
|
58
|
+
const first = buf[off];
|
|
59
|
+
if (first < 0x80)
|
|
60
|
+
return { len: first, next: off + 1 };
|
|
61
|
+
const n = first & 0x7f;
|
|
62
|
+
if (n === 0)
|
|
63
|
+
throw new Asn1Error('DER: indefinite length is not DER');
|
|
64
|
+
if (n > 4 || off + 1 + n > buf.length)
|
|
65
|
+
throw new Asn1Error('DER: bad long-form length');
|
|
66
|
+
let len = 0;
|
|
67
|
+
for (let i = 0; i < n; i++)
|
|
68
|
+
len = len * 256 + buf[off + 1 + i];
|
|
69
|
+
return { len, next: off + 1 + n };
|
|
70
|
+
}
|
|
71
|
+
/** Parse one TLV at `off`; returns node and offset past it. */
|
|
72
|
+
function readNode(buf, off) {
|
|
73
|
+
if (off >= buf.length)
|
|
74
|
+
throw new Asn1Error('DER: truncated tag');
|
|
75
|
+
const tagByte = buf[off];
|
|
76
|
+
const tagClass = (tagByte >> 6) & 0x03;
|
|
77
|
+
const constructed = (tagByte & 0x20) !== 0;
|
|
78
|
+
let tagNumber = tagByte & 0x1f;
|
|
79
|
+
let pos = off + 1;
|
|
80
|
+
if (tagNumber === 0x1f) {
|
|
81
|
+
// high-tag-number form
|
|
82
|
+
tagNumber = 0;
|
|
83
|
+
for (;;) {
|
|
84
|
+
if (pos >= buf.length)
|
|
85
|
+
throw new Asn1Error('DER: truncated high tag');
|
|
86
|
+
const b = buf[pos++];
|
|
87
|
+
tagNumber = tagNumber * 128 + (b & 0x7f);
|
|
88
|
+
if ((b & 0x80) === 0)
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const { len, next } = readLength(buf, pos);
|
|
93
|
+
if (next + len > buf.length)
|
|
94
|
+
throw new Asn1Error('DER: content overruns buffer');
|
|
95
|
+
const content = buf.subarray(next, next + len);
|
|
96
|
+
const tlv = buf.subarray(off, next + len);
|
|
97
|
+
const children = [];
|
|
98
|
+
if (constructed) {
|
|
99
|
+
let c = 0;
|
|
100
|
+
while (c < content.length) {
|
|
101
|
+
const r = readNode(content, c);
|
|
102
|
+
children.push(r.node);
|
|
103
|
+
c = r.next;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
node: { tagClass, constructed, tagNumber, content, tlv, children },
|
|
108
|
+
next: next + len,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Parse the single top-level TLV in `buf`. Trailing bytes are an error. */
|
|
112
|
+
function parseDer(buf) {
|
|
113
|
+
const { node, next } = readNode(buf, 0);
|
|
114
|
+
if (next !== buf.length)
|
|
115
|
+
throw new Asn1Error('DER: trailing bytes after top-level TLV');
|
|
116
|
+
return node;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse a top-level TLV tolerating trailing 0x00 padding — PDF /Contents
|
|
120
|
+
* hex strings are padded with zeros to a fixed length per ISO 32000.
|
|
121
|
+
*/
|
|
122
|
+
function parseDerPadded(buf) {
|
|
123
|
+
const { node, next } = readNode(buf, 0);
|
|
124
|
+
for (let i = next; i < buf.length; i++) {
|
|
125
|
+
if (buf[i] !== 0)
|
|
126
|
+
throw new Asn1Error('DER: non-zero trailing bytes after top-level TLV');
|
|
127
|
+
}
|
|
128
|
+
return node;
|
|
129
|
+
}
|
|
130
|
+
/** Expect a universal tag; throws on mismatch. */
|
|
131
|
+
function expectTag(node, tag, what) {
|
|
132
|
+
if (node.tagClass !== 0 || node.tagNumber !== tag) {
|
|
133
|
+
throw new Asn1Error(`DER: expected ${what} (universal ${tag})`);
|
|
134
|
+
}
|
|
135
|
+
return node;
|
|
136
|
+
}
|
|
137
|
+
/** Integer content → bigint (handles arbitrary size, positive only). */
|
|
138
|
+
function intValue(node) {
|
|
139
|
+
expectTag(node, UNIVERSAL.INTEGER, 'INTEGER');
|
|
140
|
+
let v = 0n;
|
|
141
|
+
for (const b of node.content)
|
|
142
|
+
v = (v << 8n) | BigInt(b);
|
|
143
|
+
return v;
|
|
144
|
+
}
|
|
145
|
+
/** OID content → dotted string. */
|
|
146
|
+
function oidValue(node) {
|
|
147
|
+
expectTag(node, UNIVERSAL.OBJECT_ID, 'OBJECT IDENTIFIER');
|
|
148
|
+
const c = node.content;
|
|
149
|
+
if (c.length === 0)
|
|
150
|
+
throw new Asn1Error('DER: empty OID');
|
|
151
|
+
const parts = [Math.floor(c[0] / 40), c[0] % 40];
|
|
152
|
+
let v = 0;
|
|
153
|
+
for (let i = 1; i < c.length; i++) {
|
|
154
|
+
v = v * 128 + (c[i] & 0x7f);
|
|
155
|
+
if ((c[i] & 0x80) === 0) {
|
|
156
|
+
parts.push(v);
|
|
157
|
+
v = 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return parts.join('.');
|
|
161
|
+
}
|
|
162
|
+
/** Any text-ish string content → utf8-ish string. */
|
|
163
|
+
function stringValue(node) {
|
|
164
|
+
if (node.tagNumber === UNIVERSAL.UTC_TIME || node.tagNumber === UNIVERSAL.GENERALIZED_TIME) {
|
|
165
|
+
return Buffer.from(node.content).toString('latin1');
|
|
166
|
+
}
|
|
167
|
+
if (node.tagNumber === UNIVERSAL.BIT_STRING || node.tagNumber === UNIVERSAL.OCTET_STRING) {
|
|
168
|
+
return Buffer.from(node.content).toString('latin1');
|
|
169
|
+
}
|
|
170
|
+
return Buffer.from(node.content).toString('utf8');
|
|
171
|
+
}
|
|
172
|
+
/** Context-specific child with tag `n` (explicit-wrapped or direct). */
|
|
173
|
+
function ctxTag(node, n) {
|
|
174
|
+
if (node.tagClass === 2 && node.tagNumber === n)
|
|
175
|
+
return node;
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
/** Find first direct child matching class/tag. */
|
|
179
|
+
function child(node, tagClass, tagNumber) {
|
|
180
|
+
return node.children.find((c) => c.tagClass === tagClass && c.tagNumber === tagNumber);
|
|
181
|
+
}
|
|
182
|
+
/** Find first direct child of a universal tag. */
|
|
183
|
+
function universal(node, tagNumber) {
|
|
184
|
+
return child(node, 0, tagNumber);
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=asn1.js.map
|
package/dist/asn1.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asn1.js","sourceRoot":"","sources":["../src/asn1.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Arithmetic consistency forensics — the strongest deterministic tamper
|
|
18
|
+
* signal available on any format: a running balance is a checksum that
|
|
19
|
+
* covers every row. Edit one amount and every balance after it breaks;
|
|
20
|
+
* recompute them all and the stated opening/closing no longer reconcile.
|
|
21
|
+
* Every break is reported with the row index and expected-vs-actual.
|
|
22
|
+
*/
|
|
23
|
+
import type { ExtractedStatement } from '@paniniapi/extract';
|
|
24
|
+
import type { ConsistencyCheck, IntegritySignal } from './types';
|
|
25
|
+
export declare function checkConsistency(st: ExtractedStatement): {
|
|
26
|
+
check: ConsistencyCheck;
|
|
27
|
+
signals: IntegritySignal[];
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=consistency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consistency.d.ts","sourceRoot":"","sources":["../src/consistency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA4BjE,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,kBAAkB,GAAG;IACxD,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B,CAkIA"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.checkConsistency = checkConsistency;
|
|
19
|
+
/** Banks round differently across currencies — 1 paise covers drift. */
|
|
20
|
+
const TOLERANCE_PAISE = 1;
|
|
21
|
+
/** Extract the running-balance chain from rows that carry one. */
|
|
22
|
+
function balanceChain(st) {
|
|
23
|
+
const chain = [];
|
|
24
|
+
for (let i = 0; i < st.transactions.length; i++) {
|
|
25
|
+
const t = st.transactions[i];
|
|
26
|
+
if (t.balancePaise === null)
|
|
27
|
+
continue;
|
|
28
|
+
chain.push({
|
|
29
|
+
index: i,
|
|
30
|
+
balance: t.balancePaise,
|
|
31
|
+
amount: t.amountPaise,
|
|
32
|
+
direction: t.direction,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return chain;
|
|
36
|
+
}
|
|
37
|
+
function checkConsistency(st) {
|
|
38
|
+
const signals = [];
|
|
39
|
+
let balanceChainBreaks = 0;
|
|
40
|
+
let openingMismatch = null;
|
|
41
|
+
let closingMismatch = null;
|
|
42
|
+
let outOfPeriodRows = 0;
|
|
43
|
+
let duplicateRows = 0;
|
|
44
|
+
const chain = balanceChain(st);
|
|
45
|
+
// ── running-balance continuity ──────────────────────────────────────
|
|
46
|
+
for (let i = 1; i < chain.length; i++) {
|
|
47
|
+
const prev = chain[i - 1];
|
|
48
|
+
const cur = chain[i];
|
|
49
|
+
if (cur.amount === null || cur.direction === null || cur.direction === 'UNKNOWN')
|
|
50
|
+
continue;
|
|
51
|
+
const delta = cur.direction === 'CREDIT' ? cur.amount : -cur.amount;
|
|
52
|
+
const expected = prev.balance + delta;
|
|
53
|
+
if (Math.abs(cur.balance - expected) > TOLERANCE_PAISE) {
|
|
54
|
+
balanceChainBreaks++;
|
|
55
|
+
if (balanceChainBreaks <= 5) {
|
|
56
|
+
signals.push({
|
|
57
|
+
ruleId: 'math.balance_chain_break',
|
|
58
|
+
severity: 'critical',
|
|
59
|
+
message: `row ${cur.index} balance breaks the running chain — the amount or balance was altered`,
|
|
60
|
+
evidence: `expected ${expected} paise (prev ${prev.balance} ${cur.direction === 'CREDIT' ? '+' : '-'} ${cur.amount}), row shows ${cur.balance}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (balanceChainBreaks > 5) {
|
|
66
|
+
signals.push({
|
|
67
|
+
ruleId: 'math.balance_chain_break',
|
|
68
|
+
severity: 'critical',
|
|
69
|
+
message: `…plus ${balanceChainBreaks - 5} more balance-chain breaks`,
|
|
70
|
+
evidence: `total ${balanceChainBreaks} breaks`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// ── opening/closing reconciliation ──────────────────────────────────
|
|
74
|
+
const open = Number(st.metadata['openingBalancePaise'] ?? NaN);
|
|
75
|
+
const close = Number(st.metadata['closingBalancePaise'] ?? NaN);
|
|
76
|
+
const first = chain[0];
|
|
77
|
+
const last = chain[chain.length - 1];
|
|
78
|
+
if (!Number.isNaN(open) &&
|
|
79
|
+
first !== undefined &&
|
|
80
|
+
first.amount !== null &&
|
|
81
|
+
first.direction !== null) {
|
|
82
|
+
// first row's pre-transaction balance should equal opening balance.
|
|
83
|
+
const preFirst = first.direction === 'CREDIT' ? first.balance - first.amount : first.balance + first.amount;
|
|
84
|
+
openingMismatch = Math.abs(preFirst - open) > TOLERANCE_PAISE;
|
|
85
|
+
if (openingMismatch) {
|
|
86
|
+
signals.push({
|
|
87
|
+
ruleId: 'math.opening_mismatch',
|
|
88
|
+
severity: 'critical',
|
|
89
|
+
message: 'first transaction does not reconcile with the stated opening balance',
|
|
90
|
+
evidence: `implied opening ${preFirst} paise vs stated ${open}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!Number.isNaN(close) && last !== undefined) {
|
|
95
|
+
closingMismatch = Math.abs(last.balance - close) > TOLERANCE_PAISE;
|
|
96
|
+
if (closingMismatch) {
|
|
97
|
+
signals.push({
|
|
98
|
+
ruleId: 'math.closing_mismatch',
|
|
99
|
+
severity: 'critical',
|
|
100
|
+
message: 'last running balance does not equal the stated closing balance',
|
|
101
|
+
evidence: `last row ${last.balance} paise vs stated ${close}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ── period bounds ────────────────────────────────────────────────────
|
|
106
|
+
const from = st.metadata['statementFrom'];
|
|
107
|
+
const to = st.metadata['statementTo'];
|
|
108
|
+
if (from !== undefined && to !== undefined) {
|
|
109
|
+
for (let i = 0; i < st.transactions.length; i++) {
|
|
110
|
+
const d = st.transactions[i].date;
|
|
111
|
+
if (d !== null && (d < from || d > to))
|
|
112
|
+
outOfPeriodRows++;
|
|
113
|
+
}
|
|
114
|
+
if (outOfPeriodRows > 0) {
|
|
115
|
+
signals.push({
|
|
116
|
+
ruleId: 'math.out_of_period',
|
|
117
|
+
severity: 'warning',
|
|
118
|
+
message: `${outOfPeriodRows} transaction(s) dated outside the stated period ${from}..${to}`,
|
|
119
|
+
evidence: `period ${from}..${to}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── duplicate rows (copy-paste fabrication signature) ────────────────
|
|
124
|
+
const seen = new Map();
|
|
125
|
+
for (const t of st.transactions) {
|
|
126
|
+
if (t.amountPaise === null)
|
|
127
|
+
continue;
|
|
128
|
+
const k = `${t.date}|${t.narration}|${t.amountPaise}`;
|
|
129
|
+
seen.set(k, (seen.get(k) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
for (const [k, n] of seen) {
|
|
132
|
+
if (n > 1)
|
|
133
|
+
duplicateRows += n - 1;
|
|
134
|
+
if (n > 2) {
|
|
135
|
+
signals.push({
|
|
136
|
+
ruleId: 'math.duplicate_rows',
|
|
137
|
+
severity: 'warning',
|
|
138
|
+
message: `row duplicated ${n}× — repeated identical transaction`,
|
|
139
|
+
evidence: k.slice(0, 80),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (duplicateRows > 0 && !signals.some((s) => s.ruleId === 'math.duplicate_rows')) {
|
|
144
|
+
signals.push({
|
|
145
|
+
ruleId: 'math.duplicate_rows',
|
|
146
|
+
severity: 'info',
|
|
147
|
+
message: `${duplicateRows} duplicate transaction row(s) — often legitimate (repeat purchases) but worth review`,
|
|
148
|
+
evidence: `${duplicateRows} duplicates`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
check: {
|
|
153
|
+
balanceChainBreaks,
|
|
154
|
+
openingMismatch,
|
|
155
|
+
closingMismatch,
|
|
156
|
+
sumMismatch: null,
|
|
157
|
+
outOfPeriodRows,
|
|
158
|
+
duplicateRows,
|
|
159
|
+
},
|
|
160
|
+
signals,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=consistency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consistency.js","sourceRoot":"","sources":["../src/consistency.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export { inspectStatement, type InspectOptions } from './report';
|
|
17
|
+
export { checkConsistency } from './consistency';
|
|
18
|
+
export { verifyPdfSignatures, type SignatureOptions } from './pdf/signature';
|
|
19
|
+
export { inspectPdfStructure, type StructureOptions } from './pdf/structure';
|
|
20
|
+
export { inspectPdfContent } from './pdf/content';
|
|
21
|
+
export type { ConsistencyCheck, IntegrityReport, IntegritySignal, IntegrityVerdict, SignalSeverity, SignatureCheck, } from './types';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,GACf,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.inspectPdfContent = exports.inspectPdfStructure = exports.verifyPdfSignatures = exports.checkConsistency = exports.inspectStatement = void 0;
|
|
19
|
+
var report_1 = require("./report");
|
|
20
|
+
Object.defineProperty(exports, "inspectStatement", { enumerable: true, get: function () { return report_1.inspectStatement; } });
|
|
21
|
+
var consistency_1 = require("./consistency");
|
|
22
|
+
Object.defineProperty(exports, "checkConsistency", { enumerable: true, get: function () { return consistency_1.checkConsistency; } });
|
|
23
|
+
var signature_1 = require("./pdf/signature");
|
|
24
|
+
Object.defineProperty(exports, "verifyPdfSignatures", { enumerable: true, get: function () { return signature_1.verifyPdfSignatures; } });
|
|
25
|
+
var structure_1 = require("./pdf/structure");
|
|
26
|
+
Object.defineProperty(exports, "inspectPdfStructure", { enumerable: true, get: function () { return structure_1.inspectPdfStructure; } });
|
|
27
|
+
var content_1 = require("./pdf/content");
|
|
28
|
+
Object.defineProperty(exports, "inspectPdfContent", { enumerable: true, get: function () { return content_1.inspectPdfContent; } });
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH,mCAAiE;AAAxD,0GAAA,gBAAgB,OAAA;AACzB,6CAAiD;AAAxC,+GAAA,gBAAgB,OAAA;AACzB,6CAA6E;AAApE,gHAAA,mBAAmB,OAAA;AAC5B,6CAA6E;AAApE,gHAAA,mBAAmB,OAAA;AAC5B,yCAAkD;AAAzC,4GAAA,iBAAiB,OAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import type { IntegritySignal } from '../types';
|
|
17
|
+
/**
|
|
18
|
+
* Text-content anomaly signals. Never throws — an unreadable content
|
|
19
|
+
* stream yields a `content_unreadable` warning signal.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inspectPdfContent(pdf: Uint8Array): IntegritySignal[];
|
|
22
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/pdf/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAoBhD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,GAAG,eAAe,EAAE,CA2CpE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 Kapil — Project Panini Contributors
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.inspectPdfContent = inspectPdfContent;
|
|
19
|
+
/**
|
|
20
|
+
* Content-layer forensics: anomalies in the rendered text itself —
|
|
21
|
+
* the classic "typed Salary over the real narration" attack leaves
|
|
22
|
+
* stacked fragments at the same page position, and invisible-text
|
|
23
|
+
* rendering modes smuggle content past a visual review.
|
|
24
|
+
*/
|
|
25
|
+
const extract_1 = require("@paniniapi/extract");
|
|
26
|
+
const X_TOL = 2;
|
|
27
|
+
const Y_TOL = 3;
|
|
28
|
+
function overlappingPairs(fragments) {
|
|
29
|
+
const hits = [];
|
|
30
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
31
|
+
for (let j = i + 1; j < fragments.length; j++) {
|
|
32
|
+
const a = fragments[i];
|
|
33
|
+
const b = fragments[j];
|
|
34
|
+
if (a.page !== b.page)
|
|
35
|
+
continue;
|
|
36
|
+
if (Math.abs(a.y - b.y) > Y_TOL || Math.abs(a.x - b.x) > X_TOL)
|
|
37
|
+
continue;
|
|
38
|
+
if (a.text === b.text)
|
|
39
|
+
continue; // identical duplicate draw — harmless
|
|
40
|
+
hits.push([a, b]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return hits;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Text-content anomaly signals. Never throws — an unreadable content
|
|
47
|
+
* stream yields a `content_unreadable` warning signal.
|
|
48
|
+
*/
|
|
49
|
+
function inspectPdfContent(pdf) {
|
|
50
|
+
const signals = [];
|
|
51
|
+
const latin = Buffer.from(pdf).toString('latin1');
|
|
52
|
+
// Invisible-text rendering modes: `3 Tr` (invisible), `7 Tr` (invisible
|
|
53
|
+
// +clip). Legit statements never hide text.
|
|
54
|
+
const invisible = (latin.match(/\b[37]\s+Tr\b/g) ?? []).length;
|
|
55
|
+
if (invisible > 0) {
|
|
56
|
+
signals.push({
|
|
57
|
+
ruleId: 'pdf.invisible_text',
|
|
58
|
+
severity: 'critical',
|
|
59
|
+
message: `${invisible} text run(s) render invisibly — content a reviewer cannot see is present in the document`,
|
|
60
|
+
evidence: `Tr-mode-3/7 operators ×${invisible}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
let fragments;
|
|
64
|
+
try {
|
|
65
|
+
fragments = (0, extract_1.extractPdfFragments)(pdf);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
signals.push({
|
|
69
|
+
ruleId: 'pdf.content_unreadable',
|
|
70
|
+
severity: 'info',
|
|
71
|
+
message: 'content stream could not be decomposed for overlay analysis',
|
|
72
|
+
evidence: 'fragment extraction failed',
|
|
73
|
+
});
|
|
74
|
+
return signals;
|
|
75
|
+
}
|
|
76
|
+
const hits = overlappingPairs(fragments);
|
|
77
|
+
if (hits.length > 0) {
|
|
78
|
+
const sample = hits
|
|
79
|
+
.slice(0, 3)
|
|
80
|
+
.map(([a, b]) => `p${a.page} ("${a.text.slice(0, 24)}" vs "${b.text.slice(0, 24)}")`)
|
|
81
|
+
.join('; ');
|
|
82
|
+
signals.push({
|
|
83
|
+
ruleId: 'pdf.overlapping_text',
|
|
84
|
+
severity: 'critical',
|
|
85
|
+
message: `${hits.length} stacked text fragments at identical positions — overtyped content`,
|
|
86
|
+
evidence: sample,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return signals;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=content.js.map
|