pompelmi 1.7.0 → 1.9.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/BADGE.md ADDED
@@ -0,0 +1,29 @@
1
+ # Scanned by pompelmi — Badge
2
+
3
+ Add this badge to your repository's `README.md` to show that your file uploads are scanned with pompelmi.
4
+
5
+ ## Markdown
6
+
7
+ ```markdown
8
+ [![Scanned by pompelmi](https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAQxlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAA5AAAAcgE7AAIAAAASAAAArIdpAAQAAAABAAAAvgAAAAAAAABgAAAAAQAAAGAAAAABQ2FudmEgZG9jPURBSEdqUE42M19JIHVzZXI9VUFHZVZYTlJxNEkgYnJhbmQ9QkFHZVZib2RxREkAAFRvbW1hc28gQmVydG9jY2hpAAAGkAAABwAAAAQwMjEwkQEABwAAAAQBAgMAoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAOoAMABAAAAAEAAAAOAAAAAOn+IX8AAAAJcEhZcwAADsQAAA7EAZUrDhsAAAZHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjY1NTM1PC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xNTAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIxMDwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Rmxhc2hQaXhWZXJzaW9uPjAxMDA8L2V4aWY6Rmxhc2hQaXhWZXJzaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MTUwMDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT4xPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGk+MjwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpPjM8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaT4wPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9leGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkNhbnZhIGRvYz1EQUhHalBONjNfSSB1c2VyPVVBR2VWWE5ScTRJIGJyYW5kPUJBR2VWYm9kcURJPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjk2PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj45NjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5Qcm9nZXR0byBzZW56YSB0aXRvbG8gLSAxPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOkFsdD4KICAgICAgICAgPC9kYzp0aXRsZT4KICAgICAgICAgPGRjOmNyZWF0b3I+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpPlRvbW1hc28gQmVydG9jY2hpPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9kYzpjcmVhdG9yPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kn4j/DQAAAeFJREFUKBWtUjtrFFEU/u68Z5zs7LLPhEgEG0ljETGFImgnNhKsYiEBLYU0KqiERaxEQbcRK8HCiH/AUrFVEEQQ0YiurvvIRp2d2ZnZeezJzIZZSAxi4S3uOffc73zfOede4H8vImJ/4/zj8t7Tc3MdixaYJ70RcsaL6uKd7m4E3M6g6TgrduBf7TQbF463zQX//rXDOzHJeZvixdrJTC7a89hreqdO62VntlzUZD+MAsd96HDqcuXS7X5KIqROYi8fWgom1nsFcX4SimFoYbeF4P1bXhUH54Mw+BZDbqT4can0HEIh7C4rhdx8xMtwbRv+18/wLROm08dgODz7sVaT08SRYjJB/+Wjm+KHzpWNxiswXQE/sxcYAoHVBjERJHIZs/NOiRMHSfKW4u+7hr9uLXk/XUCSwHEcqNWCVNkHafYIpHwJvKquTZ84GqaKW4nZZuS7Zhj0TbC8AfBirJqBV6+DYi7SJ6GVZuaMbOkZ/XitjRUZu2UNNXFVmVBAn9bANroQBA5CpQyKeyTPhZTVFebZByAbo/bGU6XWl2qvOE2cyo6xYtaPdPaEK04dVGR5kTxbo1+dJvrSCsuf6SWK295xVD/FMQYa+fHWfnB9v+4MDNP8Xp+qru76i1LsP9lNSkO4P3HUKYoAAAAASUVORK5CYII=)](https://github.com/pompelmi/pompelmi)
9
+ ```
10
+
11
+ ## HTML
12
+
13
+ ```html
14
+ <a href="https://github.com/pompelmi/pompelmi">
15
+ <img src="https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAQxlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAA5AAAAcgE7AAIAAAASAAAArIdpAAQAAAABAAAAvgAAAAAAAABgAAAAAQAAAGAAAAABQ2FudmEgZG9jPURBSEdqUE42M19JIHVzZXI9VUFHZVZYTlJxNEkgYnJhbmQ9QkFHZVZib2RxREkAAFRvbW1hc28gQmVydG9jY2hpAAAGkAAABwAAAAQwMjEwkQEABwAAAAQBAgMAoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAOoAMABAAAAAEAAAAOAAAAAOn+IX8AAAAJcEhZcwAADsQAAA7EAZUrDhsAAAZHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjY1NTM1PC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xNTAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIxMDwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Rmxhc2hQaXhWZXJzaW9uPjAxMDA8L2V4aWY6Rmxhc2hQaXhWZXJzaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MTUwMDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT4xPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGk+MjwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpPjM8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaT4wPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9leGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkNhbnZhIGRvYz1EQUhHalBONjNfSSB1c2VyPVVBR2VWWE5ScTRJIGJyYW5kPUJBR2VWYm9kcURJPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjk2PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj45NjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5Qcm9nZXR0byBzZW56YSB0aXRvbG8gLSAxPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOkFsdD4KICAgICAgICAgPC9kYzp0aXRsZT4KICAgICAgICAgPGRjOmNyZWF0b3I+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpPlRvbW1hc28gQmVydG9jY2hpPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9kYzpjcmVhdG9yPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kn4j/DQAAAeFJREFUKBWtUjtrFFEU/u68Z5zs7LLPhEgEG0ljETGFImgnNhKsYiEBLYU0KqiERaxEQbcRK8HCiH/AUrFVEEQQ0YiurvvIRp2d2ZnZeezJzIZZSAxi4S3uOffc73zfOede4H8vImJ/4/zj8t7Tc3MdixaYJ70RcsaL6uKd7m4E3M6g6TgrduBf7TQbF463zQX//rXDOzHJeZvixdrJTC7a89hreqdO62VntlzUZD+MAsd96HDqcuXS7X5KIqROYi8fWgom1nsFcX4SimFoYbeF4P1bXhUH54Mw+BZDbqT4can0HEIh7C4rhdx8xMtwbRv+18/wLROm08dgODz7sVaT08SRYjJB/+Wjm+KHzpWNxiswXQE/sxcYAoHVBjERJHIZs/NOiRMHSfKW4u+7hr9uLXk/XUCSwHEcqNWCVNkHafYIpHwJvKquTZ84GqaKW4nZZuS7Zhj0TbC8AfBirJqBV6+DYi7SJ6GVZuaMbOkZ/XitjRUZu2UNNXFVmVBAn9bANroQBA5CpQyKeyTPhZTVFebZByAbo/bGU6XWl2qvOE2cyo6xYtaPdPaEK04dVGR5kTxbo1+dJvrSCsuf6SWK295xVD/FMQYa+fHWfnB9v+4MDNP8Xp+qru76i1LsP9lNSkO4P3HUKYoAAAAASUVORK5CYII=" alt="Scanned by pompelmi">
16
+ </a>
17
+ ```
18
+
19
+ ## RST (reStructuredText)
20
+
21
+ ```rst
22
+ .. image:: https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAQxlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAA5AAAAcgE7AAIAAAASAAAArIdpAAQAAAABAAAAvgAAAAAAAABgAAAAAQAAAGAAAAABQ2FudmEgZG9jPURBSEdqUE42M19JIHVzZXI9VUFHZVZYTlJxNEkgYnJhbmQ9QkFHZVZib2RxREkAAFRvbW1hc28gQmVydG9jY2hpAAAGkAAABwAAAAQwMjEwkQEABwAAAAQBAgMAoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAOoAMABAAAAAEAAAAOAAAAAOn+IX8AAAAJcEhZcwAADsQAAA7EAZUrDhsAAAZHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjY1NTM1PC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xNTAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIxMDwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Rmxhc2hQaXhWZXJzaW9uPjAxMDA8L2V4aWY6Rmxhc2hQaXhWZXJzaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MTUwMDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT4xPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGk+MjwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpPjM8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaT4wPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9leGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkNhbnZhIGRvYz1EQUhHalBONjNfSSB1c2VyPVVBR2VWWE5ScTRJIGJyYW5kPUJBR2VWYm9kcURJPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjk2PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj45NjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5Qcm9nZXR0byBzZW56YSB0aXRvbG8gLSAxPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOkFsdD4KICAgICAgICAgPC9kYzp0aXRsZT4KICAgICAgICAgPGRjOmNyZWF0b3I+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpPlRvbW1hc28gQmVydG9jY2hpPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9kYzpjcmVhdG9yPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kn4j/DQAAAeFJREFUKBWtUjtrFFEU/u68Z5zs7LLPhEgEG0ljETGFImgnNhKsYiEBLYU0KqiERaxEQbcRK8HCiH/AUrFVEEQQ0YiurvvIRp2d2ZnZeezJzIZZSAxi4S3uOffc73zfOede4H8vImJ/4/zj8t7Tc3MdixaYJ70RcsaL6uKd7m4E3M6g6TgrduBf7TQbF463zQX//rXDOzHJeZvixdrJTC7a89hreqdO62VntlzUZD+MAsd96HDqcuXS7X5KIqROYi8fWgom1nsFcX4SimFoYbeF4P1bXhUH54Mw+BZDbqT4can0HEIh7C4rhdx8xMtwbRv+18/wLROm08dgODz7sVaT08SRYjJB/+Wjm+KHzpWNxiswXQE/sxcYAoHVBjERJHIZs/NOiRMHSfKW4u+7hr9uLXk/XUCSwHEcqNWCVNkHafYIpHwJvKquTZ84GqaKW4nZZuS7Zhj0TbC8AfBirJqBV6+DYi7SJ6GVZuaMbOkZ/XitjRUZu2UNNXFVmVBAn9bANroQBA5CpQyKeyTPhZTVFebZByAbo/bGU6XWl2qvOE2cyo6xYtaPdPaEK04dVGR5kTxbo1+dJvrSCsuf6SWK295xVD/FMQYa+fHWfnB9v+4MDNP8Xp+qru76i1LsP9lNSkO4P3HUKYoAAAAASUVORK5CYII=
23
+ :target: https://github.com/pompelmi/pompelmi
24
+ :alt: Scanned by pompelmi
25
+ ```
26
+
27
+ ## Preview
28
+
29
+ [![Scanned by pompelmi](https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAQxlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAA5AAAAcgE7AAIAAAASAAAArIdpAAQAAAABAAAAvgAAAAAAAABgAAAAAQAAAGAAAAABQ2FudmEgZG9jPURBSEdqUE42M19JIHVzZXI9VUFHZVZYTlJxNEkgYnJhbmQ9QkFHZVZib2RxREkAAFRvbW1hc28gQmVydG9jY2hpAAAGkAAABwAAAAQwMjEwkQEABwAAAAQBAgMAoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAOoAMABAAAAAEAAAAOAAAAAOn+IX8AAAAJcEhZcwAADsQAAA7EAZUrDhsAAAZHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjY1NTM1PC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xNTAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIxMDwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Rmxhc2hQaXhWZXJzaW9uPjAxMDA8L2V4aWY6Rmxhc2hQaXhWZXJzaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MTUwMDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT4xPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGk+MjwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpPjM8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaT4wPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9leGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkNhbnZhIGRvYz1EQUhHalBONjNfSSB1c2VyPVVBR2VWWE5ScTRJIGJyYW5kPUJBR2VWYm9kcURJPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjk2PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj45NjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5Qcm9nZXR0byBzZW56YSB0aXRvbG8gLSAxPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOkFsdD4KICAgICAgICAgPC9kYzp0aXRsZT4KICAgICAgICAgPGRjOmNyZWF0b3I+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpPlRvbW1hc28gQmVydG9jY2hpPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9kYzpjcmVhdG9yPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kn4j/DQAAAeFJREFUKBWtUjtrFFEU/u68Z5zs7LLPhEgEG0ljETGFImgnNhKsYiEBLYU0KqiERaxEQbcRK8HCiH/AUrFVEEQQ0YiurvvIRp2d2ZnZeezJzIZZSAxi4S3uOffc73zfOede4H8vImJ/4/zj8t7Tc3MdixaYJ70RcsaL6uKd7m4E3M6g6TgrduBf7TQbF463zQX//rXDOzHJeZvixdrJTC7a89hreqdO62VntlzUZD+MAsd96HDqcuXS7X5KIqROYi8fWgom1nsFcX4SimFoYbeF4P1bXhUH54Mw+BZDbqT4can0HEIh7C4rhdx8xMtwbRv+18/wLROm08dgODz7sVaT08SRYjJB/+Wjm+KHzpWNxiswXQE/sxcYAoHVBjERJHIZs/NOiRMHSfKW4u+7hr9uLXk/XUCSwHEcqNWCVNkHafYIpHwJvKquTZ84GqaKW4nZZuS7Zhj0TbC8AfBirJqBV6+DYi7SJ6GVZuaMbOkZ/XitjRUZu2UNNXFVmVBAn9bANroQBA5CpQyKeyTPhZTVFebZByAbo/bGU6XWl2qvOE2cyo6xYtaPdPaEK04dVGR5kTxbo1+dJvrSCsuf6SWK295xVD/FMQYa+fHWfnB9v+4MDNP8Xp+qru76i1LsP9lNSkO4P3HUKYoAAAAASUVORK5CYII=)](https://github.com/pompelmi/pompelmi)
package/README.md CHANGED
@@ -15,6 +15,8 @@
15
15
  <img src="https://img.shields.io/badge/license-ISC-blue" alt="license">
16
16
  <a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img src="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
17
17
  <a href="https://github.com/pompelmi/pompelmi/actions/workflows/release.yml"><img src="https://github.com/pompelmi/pompelmi/actions/workflows/release.yml/badge.svg" alt="npm publish"></a>
18
+ <img src="https://img.shields.io/badge/TypeScript-types%20included-3178c6?logo=typescript&logoColor=white" alt="TypeScript types included">
19
+ <a href="https://github.com/pompelmi/pompelmi"><img src="https://img.shields.io/badge/scanned%20by-pompelmi-orange?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAQxlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAA5AAAAcgE7AAIAAAASAAAArIdpAAQAAAABAAAAvgAAAAAAAABgAAAAAQAAAGAAAAABQ2FudmEgZG9jPURBSEdqUE42M19JIHVzZXI9VUFHZVZYTlJxNEkgYnJhbmQ9QkFHZVZib2RxREkAAFRvbW1hc28gQmVydG9jY2hpAAAGkAAABwAAAAQwMjEwkQEABwAAAAQBAgMAoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAOoAMABAAAAAEAAAAOAAAAAOn+IX8AAAAJcEhZcwAADsQAAA7EAZUrDhsAAAZHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjY1NTM1PC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xNTAwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIxMDwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Rmxhc2hQaXhWZXJzaW9uPjAxMDA8L2V4aWY6Rmxhc2hQaXhWZXJzaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MTUwMDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT4xPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGk+MjwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpPjM8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaT4wPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9leGlmOkNvbXBvbmVudHNDb25maWd1cmF0aW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkNhbnZhIGRvYz1EQUhHalBONjNfSSB1c2VyPVVBR2VWWE5ScTRJIGJyYW5kPUJBR2VWYm9kcURJPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjk2PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj45NjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5Qcm9nZXR0byBzZW56YSB0aXRvbG8gLSAxPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOkFsdD4KICAgICAgICAgPC9kYzp0aXRsZT4KICAgICAgICAgPGRjOmNyZWF0b3I+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpPlRvbW1hc28gQmVydG9jY2hpPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC9kYzpjcmVhdG9yPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kn4j/DQAAAeFJREFUKBWtUjtrFFEU/u68Z5zs7LLPhEgEG0ljETGFImgnNhKsYiEBLYU0KqiERaxEQbcRK8HCiH/AUrFVEEQQ0YiurvvIRp2d2ZnZeezJzIZZSAxi4S3uOffc73zfOede4H8vImJ/4/zj8t7Tc3MdixaYJ70RcsaL6uKd7m4E3M6g6TgrduBf7TQbF463zQX//rXDOzHJeZvixdrJTC7a89hreqdO62VntlzUZD+MAsd96HDqcuXS7X5KIqROYi8fWgom1nsFcX4SimFoYbeF4P1bXhUH54Mw+BZDbqT4can0HEIh7C4rhdx8xMtwbRv+18/wLROm08dgODz7sVaT08SRYjJB/+Wjm+KHzpWNxiswXQE/sxcYAoHVBjERJHIZs/NOiRMHSfKW4u+7hr9uLXk/XUCSwHEcqNWCVNkHafYIpHwJvKquTZ84GqaKW4nZZuS7Zhj0TbC8AfBirJqBV6+DYi7SJ6GVZuaMbOkZ/XitjRUZu2UNNXFVmVBAn9bANroQBA5CpQyKeyTPhZTVFebZByAbo/bGU6XWl2qvOE2cyo6xYtaPdPaEK04dVGR5kTxbo1+dJvrSCsuf6SWK295xVD/FMQYa+fHWfnB9v+4MDNP8Xp+qru76i1LsP9lNSkO4P3HUKYoAAAAASUVORK5CYII=" alt="Scanned by pompelmi"></a>
18
20
  </p>
19
21
 
20
22
  ---
@@ -345,7 +347,19 @@ choco install clamav -y
345
347
 
346
348
  ## Examples
347
349
 
348
- The [`examples/`](./examples/) directory contains standalone runnable scripts. Each can be run with `node examples/<name>.js`.
350
+ The [`examples/`](./examples/) directory contains standalone runnable scripts and framework-specific starters.
351
+
352
+ ### Framework starters
353
+
354
+ | Directory | Description |
355
+ |-----------|-------------|
356
+ | [`examples/express/`](./examples/express/) | Full Express app with multer + pompelmi middleware |
357
+ | [`examples/nextjs/`](./examples/nextjs/) | Next.js API route that scans raw upload bytes |
358
+ | [`examples/nestjs/`](./examples/nestjs/) | NestJS guard wrapping pompelmi for route-level protection |
359
+
360
+ ### Standalone scripts
361
+
362
+ Each can be run with `node examples/<name>.js`.
349
363
 
350
364
  | File | Description |
351
365
  |------|-------------|
@@ -369,7 +383,7 @@ The [`examples/`](./examples/) directory contains standalone runnable scripts. E
369
383
  | `scan-zip.js` | ZIP archive scan (ClamAV recurses automatically) |
370
384
  | `install-clamav.js` | Programmatic ClamAV installation |
371
385
  | `update-virus-database.js` | Programmatic virus DB update |
372
- | `typescript-usage.ts` | TypeScript example with inline type declarations |
386
+ | `typescript-usage.ts` | TypeScript example with full type declarations |
373
387
 
374
388
  ---
375
389
 
@@ -411,6 +425,7 @@ Scan any repository for viruses on every push or pull request — ClamAV is bund
411
425
  |-------|-------------|---------|
412
426
  | `path` | Directory or file to scan | `.` (full workspace) |
413
427
  | `fail-on-virus` | Fail the workflow step when infected files are found | `true` |
428
+ | `comment-on-pr` | Post a PR comment listing infected files (requires `GITHUB_TOKEN`) | `true` |
414
429
 
415
430
  ### Outputs
416
431
 
@@ -456,6 +471,14 @@ Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing. To r
456
471
 
457
472
  ---
458
473
 
474
+ ## Coming soon
475
+
476
+ - [ ] AWS S3 integration — scan objects directly from S3 without downloading
477
+ - [ ] Cloudflare Workers support — edge-native scanning via the clamd TCP protocol
478
+ - [ ] NestJS official module — `PompelmiModule.forRoot()` with injectable `PompelmiService`
479
+
480
+ ---
481
+
459
482
  ## License
460
483
 
461
484
  [ISC](./LICENSE) — © pompelmi contributors
package/action/Dockerfile CHANGED
@@ -1,24 +1,18 @@
1
1
  FROM node:20-slim
2
2
 
3
- ENV DEBIAN_FRONTEND=noninteractive
4
-
5
3
  RUN apt-get update && \
6
- apt-get install -y --no-install-recommends \
7
- clamav \
8
- clamav-freshclam \
9
- && rm -rf /var/lib/apt/lists/* \
10
- && sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf \
11
- && mkdir -p /var/lib/clamav /run/clamav
4
+ apt-get install -y --no-install-recommends clamav clamav-freshclam && \
5
+ rm -rf /var/lib/apt/lists/* && \
6
+ sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf && \
7
+ mkdir -p /var/lib/clamav /run/clamav
12
8
 
13
9
  WORKDIR /action
14
10
 
15
- # Bundle the pompelmi library from this repository
16
- COPY src/ ./src/
17
- COPY package.json ./
11
+ RUN npm install pompelmi
12
+
13
+ COPY entrypoint.sh ./entrypoint.sh
14
+ COPY scanner.js ./scanner.js
18
15
 
19
- # Action scripts
20
- COPY action/entrypoint.sh ./entrypoint.sh
21
- COPY action/scanner.js ./scanner.js
22
16
  RUN chmod +x ./entrypoint.sh
23
17
 
24
18
  ENTRYPOINT ["/action/entrypoint.sh"]
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "pompelmi-action",
3
+ "version": "1.8.0",
4
+ "private": true,
5
+ "description": "GitHub Action entrypoint for pompelmi ClamAV scanner",
6
+ "main": "scanner.js",
7
+ "scripts": {},
8
+ "dependencies": {
9
+ "@actions/artifact": "^2.1.11"
10
+ }
11
+ }
package/action/scanner.js CHANGED
@@ -2,10 +2,136 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { scan, scanDirectory, Verdict } = require('./src/index.js');
5
+ const { scan, scanDirectory, Verdict } = require('pompelmi');
6
6
 
7
7
  const scanPath = process.argv[2] || '.';
8
8
  const failOnVirus = process.argv[3] !== 'false';
9
+ const commentOnPr = process.argv[4] !== 'false';
10
+
11
+ async function writeReport(clean, malicious, errors, outputDir) {
12
+ const total = clean.length + malicious.length + errors.length;
13
+ const status = malicious.length > 0 ? 'infected' : 'clean';
14
+
15
+ const rows = [
16
+ ...clean.map(f => ({ file: f, status: 'clean', verdict: 'Clean' })),
17
+ ...malicious.map(f => ({ file: f, status: 'infected', verdict: 'Malicious' })),
18
+ ...errors.map(f => ({ file: f, status: 'error', verdict: 'ScanError' })),
19
+ ];
20
+
21
+ const json = JSON.stringify({ status, total, clean: clean.length, malicious: malicious.length, errors: errors.length, files: rows }, null, 2);
22
+ fs.writeFileSync(path.join(outputDir, 'report.json'), json);
23
+
24
+ const tableRows = rows.map(r => {
25
+ const color = r.status === 'clean' ? '#22c55e' : r.status === 'infected' ? '#ef4444' : '#f59e0b';
26
+ return `<tr><td>${escHtml(r.file)}</td><td style="color:${color};font-weight:bold">${r.verdict}</td></tr>`;
27
+ }).join('\n');
28
+
29
+ const html = `<!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <title>Pompelmi Scan Report</title>
34
+ <style>
35
+ body { font-family: sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
36
+ h1 { font-size: 1.5rem; }
37
+ .summary { display: flex; gap: 24px; margin: 16px 0; }
38
+ .stat { background: #f1f5f9; border-radius: 8px; padding: 12px 20px; text-align: center; }
39
+ .stat span { display: block; font-size: 1.8rem; font-weight: bold; }
40
+ table { width: 100%; border-collapse: collapse; margin-top: 24px; }
41
+ th { background: #0f172a; color: #fff; text-align: left; padding: 8px 12px; }
42
+ td { padding: 8px 12px; border-bottom: 1px solid #e2e8f0; word-break: break-all; }
43
+ tr:hover td { background: #f8fafc; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <h1>${status === 'clean' ? '✅' : '❌'} Pompelmi Scan Report</h1>
48
+ <div class="summary">
49
+ <div class="stat"><span>${total}</span>Total</div>
50
+ <div class="stat"><span style="color:#22c55e">${clean.length}</span>Clean</div>
51
+ <div class="stat"><span style="color:#ef4444">${malicious.length}</span>Infected</div>
52
+ <div class="stat"><span style="color:#f59e0b">${errors.length}</span>Errors</div>
53
+ </div>
54
+ <table>
55
+ <thead><tr><th>File</th><th>Verdict</th></tr></thead>
56
+ <tbody>
57
+ ${tableRows}
58
+ </tbody>
59
+ </table>
60
+ </body>
61
+ </html>`;
62
+ fs.writeFileSync(path.join(outputDir, 'report.html'), html);
63
+
64
+ return { jsonPath: path.join(outputDir, 'report.json'), htmlPath: path.join(outputDir, 'report.html') };
65
+ }
66
+
67
+ function escHtml(s) {
68
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
69
+ }
70
+
71
+ async function postPrComment(malicious) {
72
+ if (!commentOnPr) return;
73
+
74
+ const token = process.env.GITHUB_TOKEN;
75
+ const eventName = process.env.GITHUB_EVENT_NAME;
76
+ if (!token || eventName !== 'pull_request') return;
77
+
78
+ const eventPath = process.env.GITHUB_EVENT_PATH;
79
+ if (!eventPath || !fs.existsSync(eventPath)) return;
80
+
81
+ let event;
82
+ try { event = JSON.parse(fs.readFileSync(eventPath, 'utf8')); }
83
+ catch { return; }
84
+
85
+ const prNumber = event.pull_request && event.pull_request.number;
86
+ const repo = process.env.GITHUB_REPOSITORY;
87
+ if (!prNumber || !repo) return;
88
+
89
+ const rows = malicious
90
+ .map(f => `| \`${escHtml(f)}\` | Malicious |`)
91
+ .join('\n');
92
+ const body =
93
+ `## ❌ Pompelmi: Virus Detected\n\n` +
94
+ `The following infected file(s) were found during the scan:\n\n` +
95
+ `| File | Verdict |\n|------|--------|\n${rows}\n\n` +
96
+ `> Scanned by [pompelmi](https://pompelmi.app)`;
97
+
98
+ const https = require('https');
99
+ const payload = JSON.stringify({ body });
100
+
101
+ await new Promise((resolve, reject) => {
102
+ const req = https.request({
103
+ hostname: 'api.github.com',
104
+ path: `/repos/${repo}/issues/${prNumber}/comments`,
105
+ method: 'POST',
106
+ headers: {
107
+ 'Authorization': `Bearer ${token}`,
108
+ 'Content-Type': 'application/json',
109
+ 'Content-Length': Buffer.byteLength(payload),
110
+ 'User-Agent': 'pompelmi-action',
111
+ 'Accept': 'application/vnd.github+json',
112
+ 'X-GitHub-Api-Version': '2022-11-28',
113
+ },
114
+ }, (res) => {
115
+ res.resume();
116
+ if (res.statusCode >= 200 && res.statusCode < 300) resolve();
117
+ else reject(new Error(`GitHub API returned HTTP ${res.statusCode}`));
118
+ });
119
+ req.on('error', reject);
120
+ req.write(payload);
121
+ req.end();
122
+ }).catch(err => console.warn(`Could not post PR comment: ${err.message}`));
123
+ }
124
+
125
+ async function uploadArtifact(jsonPath, htmlPath) {
126
+ try {
127
+ const { DefaultArtifactClient } = require('@actions/artifact');
128
+ const client = new DefaultArtifactClient();
129
+ await client.uploadArtifact('pompelmi-scan-report', [jsonPath, htmlPath], path.dirname(jsonPath));
130
+ console.log('Scan report artifact uploaded: pompelmi-scan-report');
131
+ } catch (err) {
132
+ console.warn(`Could not upload artifact (not running in Actions?): ${err.message}`);
133
+ }
134
+ }
9
135
 
10
136
  async function main() {
11
137
  const resolved = path.resolve(scanPath);
@@ -66,6 +192,11 @@ async function main() {
66
192
  fs.appendFileSync(summaryFile, rows.join('\n') + '\n');
67
193
  }
68
194
 
195
+ // --- Report artifact ---
196
+ const reportDir = process.env.RUNNER_TEMP || '/tmp';
197
+ const { jsonPath, htmlPath } = await writeReport(clean, malicious, errors, reportDir);
198
+ await uploadArtifact(jsonPath, htmlPath);
199
+
69
200
  // --- Console ---
70
201
  console.log(`\nScan complete — ${total} file(s) scanned`);
71
202
  console.log(` Clean: ${clean.length}`);
@@ -76,6 +207,7 @@ async function main() {
76
207
  if (malicious.length > 0) {
77
208
  console.error('\nInfected files:');
78
209
  malicious.forEach(f => console.error(` ${f}`));
210
+ await postPrComment(malicious);
79
211
  if (failOnVirus) {
80
212
  console.error('\n::error::Virus(es) detected — failing workflow.');
81
213
  process.exit(1);
package/action.yml CHANGED
@@ -14,6 +14,10 @@ inputs:
14
14
  description: 'Fail the workflow step when infected files are found'
15
15
  required: false
16
16
  default: 'true'
17
+ comment-on-pr:
18
+ description: 'Post a PR comment listing infected files when a virus is found (requires GITHUB_TOKEN)'
19
+ required: false
20
+ default: 'true'
17
21
 
18
22
  outputs:
19
23
  infected-files:
@@ -27,3 +31,4 @@ runs:
27
31
  args:
28
32
  - ${{ inputs.path }}
29
33
  - ${{ inputs.fail-on-virus }}
34
+ - ${{ inputs.comment-on-pr }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "ClamAV for humans — scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.",
5
5
  "license": "ISC",
6
6
  "author": "pompelmi contributors",
@@ -32,6 +32,7 @@
32
32
  ],
33
33
  "type": "commonjs",
34
34
  "main": "./src/index.js",
35
+ "types": "./types/index.d.ts",
35
36
  "scripts": {
36
37
  "test": "node --test test/unit.test.js && node test/scan.test.js",
37
38
  "lint": "eslint src/"
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { scan, scanBuffer, scanStream, scanDirectory } = require('./ClamAVScanner.js');
2
2
  const { Verdict } = require('./verdicts.js');
3
+ const { middleware } = require('./middleware.js');
3
4
 
4
- module.exports = { scan, scanBuffer, scanStream, scanDirectory, Verdict };
5
+ module.exports = { scan, scanBuffer, scanStream, scanDirectory, Verdict, middleware };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { scanBuffer, Verdict } = require('./ClamAVScanner.js');
4
+
5
+ function middleware(opts = {}) {
6
+ const field = opts.uploadField || 'file';
7
+
8
+ return async function pompelmiMiddleware(req, res, next) {
9
+ let buffers = [];
10
+
11
+ if (req.file) {
12
+ buffers = [req.file.buffer];
13
+ } else if (req.files) {
14
+ const files = Array.isArray(req.files)
15
+ ? req.files
16
+ : (req.files[field] || Object.values(req.files).flat());
17
+ buffers = files.map(f => f.buffer).filter(Boolean);
18
+ }
19
+
20
+ if (buffers.length === 0) return next();
21
+
22
+ try {
23
+ for (const buf of buffers) {
24
+ const verdict = await scanBuffer(buf, opts);
25
+ if (verdict === Verdict.Malicious) {
26
+ return res.status(403).json({ error: 'Malicious file detected' });
27
+ }
28
+ }
29
+ next();
30
+ } catch (err) {
31
+ next(err);
32
+ }
33
+ };
34
+ }
35
+
36
+ module.exports = { middleware };
@@ -0,0 +1,82 @@
1
+ import { Readable } from 'stream';
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+
4
+ /** Options passed to any scan function */
5
+ export interface ScanOptions {
6
+ /** clamd hostname — enables TCP mode when set */
7
+ host?: string;
8
+ /** clamd port (default: 3310) */
9
+ port?: number;
10
+ /** Path to a clamd UNIX domain socket (e.g. /run/clamav/clamd.sock) */
11
+ socket?: string;
12
+ /** Socket idle timeout in milliseconds, clamd mode only (default: 15000) */
13
+ timeout?: number;
14
+ }
15
+
16
+ /** Options for the Express/Fastify middleware */
17
+ export interface MiddlewareOptions extends ScanOptions {
18
+ /** multer field name to look for uploaded files (default: 'file') */
19
+ uploadField?: string;
20
+ }
21
+
22
+ /** Result returned by scanDirectory */
23
+ export interface DirectoryScanResult {
24
+ /** Absolute paths of files that scanned clean */
25
+ clean: string[];
26
+ /** Absolute paths of infected files */
27
+ malicious: string[];
28
+ /** Absolute paths of files that produced a scan error */
29
+ errors: string[];
30
+ }
31
+
32
+ /** Opaque Symbol-based scan verdicts */
33
+ export declare const Verdict: {
34
+ readonly Clean: unique symbol;
35
+ readonly Malicious: unique symbol;
36
+ readonly ScanError: unique symbol;
37
+ };
38
+
39
+ /** The type of any Verdict symbol */
40
+ export type VerdictValue = typeof Verdict[keyof typeof Verdict];
41
+
42
+ type NextFunction = (err?: unknown) => void;
43
+ type RequestHandler = (
44
+ req: IncomingMessage,
45
+ res: ServerResponse,
46
+ next: NextFunction
47
+ ) => void | Promise<void>;
48
+
49
+ /**
50
+ * Scan a file at the given path.
51
+ * Resolves to Verdict.Clean, Verdict.Malicious, or Verdict.ScanError.
52
+ * Rejects if the file is not found or clamscan is unavailable.
53
+ */
54
+ export declare function scan(filePath: string, options?: ScanOptions): Promise<VerdictValue>;
55
+
56
+ /**
57
+ * Scan an in-memory Buffer.
58
+ * In TCP/socket mode the buffer is streamed to clamd with no disk I/O.
59
+ */
60
+ export declare function scanBuffer(buffer: Buffer, options?: ScanOptions): Promise<VerdictValue>;
61
+
62
+ /**
63
+ * Scan a Node.js Readable stream.
64
+ * In TCP/socket mode the stream is piped to clamd with no disk I/O.
65
+ */
66
+ export declare function scanStream(stream: Readable, options?: ScanOptions): Promise<VerdictValue>;
67
+
68
+ /**
69
+ * Recursively scan every file under dirPath.
70
+ * Per-file errors are caught and collected without aborting the full scan.
71
+ */
72
+ export declare function scanDirectory(
73
+ dirPath: string,
74
+ options?: ScanOptions
75
+ ): Promise<DirectoryScanResult>;
76
+
77
+ /**
78
+ * Express / Fastify middleware that scans multer-uploaded files
79
+ * (req.file / req.files) and responds HTTP 403 on any infection.
80
+ * Call after multer, before your route handler.
81
+ */
82
+ export declare function middleware(options?: MiddlewareOptions): RequestHandler;
package/pr_info.tmp DELETED
@@ -1,2 +0,0 @@
1
- {"branch":null,"number":null,"url":null}
2
- EOF