]> git.ipfire.org Git - people/ms/suricata.git/blame - doc/userguide/lua/lua-functions.rst
userguide: rename pg Lua Scripting->Lua Detection
[people/ms/suricata.git] / doc / userguide / lua / lua-functions.rst
CommitLineData
0c4bf2d3
EL
1.. _lua-functions:
2
3Lua functions
4=============
5
e7f1736f
JF
6Differences between `output` and `detect`:
7------------------------------------------
8
9Currently, the ``needs`` key initialization varies, depending on what is the goal of the script: output or detection.
10
4256c1cc 11If the script is for detection, the ``needs`` initialization should be as seen in the example below (see :ref:`lua-detection` for a complete example of a detection script):
e7f1736f
JF
12
13::
14
15 function init (args)
16 local needs = {}
17 needs["packet"] = tostring(true)
18 return needs
19 end
20
21For output logs, follow the pattern below. (The complete script structure can be seen at :ref:`lua-output`:)
22
23::
24
25 function init (args)
26 local needs = {}
27 needs["protocol"] = "http"
28 return needs
29 end
30
31
32Do notice that the functions and protocols available for ``log`` and ``match`` may also vary. DNP3, for instance, is not
33available for logging.
34
0c4bf2d3
EL
35packet
36------
37
38Initialize with:
39
40::
41
42 function init (args)
43 local needs = {}
44 needs["type"] = "packet"
45 return needs
46 end
47
48SCPacketTimestamp
49~~~~~~~~~~~~~~~~~
50
51Get packets timestamp as 2 numbers: seconds & microseconds elapsed since
521970-01-01 00:00:00 UTC.
53
54::
55
56 function log(args)
57 local sec, usec = SCPacketTimestamp()
58 end
59
60SCPacketTimeString
61~~~~~~~~~~~~~~~~~~
62
06f41f60 63Use ``SCPacketTimeString`` to get the packet's time string in the format:
0c4bf2d3
EL
6411/24/2009-18:57:25.179869
65
66::
67
68 function log(args)
69 ts = SCPacketTimeString()
70
71SCPacketTuple
72~~~~~~~~~~~~~
73
74::
75
76 ipver, srcip, dstip, proto, sp, dp = SCPacketTuple()
77
78SCPacketPayload
79~~~~~~~~~~~~~~~
80
81::
82
83 p = SCPacketPayload()
84
85flow
86----
87
88::
89
90 function init (args)
91 local needs = {}
92 needs["type"] = "flow"
93 return needs
94 end
95
96SCFlowTimestamps
97~~~~~~~~~~~~~~~~
98
99Get timestamps (seconds and microseconds) of the first and the last packet from
100the flow.
101
102::
103
104 startts, lastts = SCFlowTimestamps()
105 startts_s, lastts_s, startts_us, lastts_us = SCFlowTimestamps()
106
107SCFlowTimeString
108~~~~~~~~~~~~~~~~
109
110::
111
112 startts = SCFlowTimeString()
113
114SCFlowTuple
115~~~~~~~~~~~
116
117::
118
119 ipver, srcip, dstip, proto, sp, dp = SCFlowTuple()
120
121SCFlowAppLayerProto
122~~~~~~~~~~~~~~~~~~~
123
06f41f60 124Get alproto as a string from the flow. If a alproto is not (yet) known, it
0c4bf2d3
EL
125returns "unknown".
126
127Example:
128
129::
130
131 function log(args)
132 alproto = SCFlowAppLayerProto()
133 if alproto ~= nil then
134 print (alproto)
135 end
136 end
137
138Returns 5 values: <alproto> <alproto_ts> <alproto_tc> <alproto_orig> <alproto_expect>
139
140Orig and expect are used when changing and upgrading protocols. In a SMTP STARTTLS
141case, orig would normally be set to "smtp" and expect to "tls".
142
143
144SCFlowHasAlerts
145~~~~~~~~~~~~~~~
146
147Returns true if flow has alerts.
148
149Example:
150
151::
152
153 function log(args)
154 has_alerts = SCFlowHasAlerts()
155 if has_alerts then
156 -- do something
157 end
158 end
159
160SCFlowStats
161~~~~~~~~~~~
162
163Gets the packet and byte counts per flow.
164
165::
166
167 tscnt, tsbytes, tccnt, tcbytes = SCFlowStats()
168
169SCFlowId
170~~~~~~~~
171
172Gets the flow id.
173
174::
175
176 id = SCFlowId()
177
178Note that simply printing 'id' will likely result in printing a scientific
179notation. To avoid that, simply do:
180
181::
182
183 id = SCFlowId()
184 idstr = string.format("%.0f",id)
185 print ("Flow ID: " .. idstr .. "\n")
186
187
188http
189----
190
e7f1736f 191For output, init with:
0c4bf2d3
EL
192
193::
194
195 function init (args)
196 local needs = {}
197 needs["protocol"] = "http"
198 return needs
199 end
200
e7f1736f
JF
201For detection, use the specific buffer (cf :ref:`lua-detection` for a complete list), as with:
202
203::
204
205 function init (args)
206 local needs = {}
207 needs["http.uri"] = tostring(true)
208 return needs
209 end
210
0c4bf2d3
EL
211HttpGetRequestBody and HttpGetResponseBody.
212~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
213
214Make normalized body data available to the script through
215HttpGetRequestBody and HttpGetResponseBody.
216
b116a56a 217There no guarantees that all of the body will be available.
0c4bf2d3
EL
218
219Example:
220
221::
222
223 function log(args)
224 a, o, e = HttpGetResponseBody();
225 --print("offset " .. o .. " end " .. e)
226 for n, v in ipairs(a) do
227 print(v)
228 end
229 end
230
231HttpGetRequestHost
232~~~~~~~~~~~~~~~~~~
233
234Get the host from libhtp's tx->request_hostname, which can either be
235the host portion of the url or the host portion of the Host header.
236
237Example:
238
239::
240
241 http_host = HttpGetRequestHost()
242 if http_host == nil then
243 http_host = "<hostname unknown>"
244 end
245
246HttpGetRequestHeader
247~~~~~~~~~~~~~~~~~~~~
248
249::
250
251 http_ua = HttpGetRequestHeader("User-Agent")
252 if http_ua == nil then
253 http_ua = "<useragent unknown>"
254 end
255
256HttpGetResponseHeader
257~~~~~~~~~~~~~~~~~~~~~
258
259::
260
261 server = HttpGetResponseHeader("Server");
262 print ("Server: " .. server);
263
264HttpGetRequestLine
265~~~~~~~~~~~~~~~~~~
266
267::
268
269 rl = HttpGetRequestLine();
270 print ("Request Line: " .. rl);
271
272HttpGetResponseLine
273~~~~~~~~~~~~~~~~~~~
274
275::
276
277 rsl = HttpGetResponseLine();
278 print ("Response Line: " .. rsl);
279
280HttpGetRawRequestHeaders
281~~~~~~~~~~~~~~~~~~~~~~~~
282
283::
284
285 rh = HttpGetRawRequestHeaders();
286 print ("Raw Request Headers: " .. rh);
287
288HttpGetRawResponseHeaders
289~~~~~~~~~~~~~~~~~~~~~~~~~
290
291::
292
293 rh = HttpGetRawResponseHeaders();
294 print ("Raw Response Headers: " .. rh);
295
296HttpGetRequestUriRaw
297~~~~~~~~~~~~~~~~~~~~
298
299::
300
301 http_uri = HttpGetRequestUriRaw()
302 if http_uri == nil then
303 http_uri = "<unknown>"
304 end
305
306HttpGetRequestUriNormalized
307~~~~~~~~~~~~~~~~~~~~~~~~~~~
308
309::
310
311 http_uri = HttpGetRequestUriNormalized()
312 if http_uri == nil then
313 http_uri = "<unknown>"
314 end
315
316HttpGetRequestHeaders
317~~~~~~~~~~~~~~~~~~~~~
318
319::
320
321 a = HttpGetRequestHeaders();
322 for n, v in pairs(a) do
323 print(n,v)
324 end
325
326HttpGetResponseHeaders
327~~~~~~~~~~~~~~~~~~~~~~
328
329::
330
331 a = HttpGetResponseHeaders();
332 for n, v in pairs(a) do
333 print(n,v)
334 end
335
336DNS
337---
338
e7f1736f
JF
339If your purpose is to create a logging script, initialize the buffer as:
340
341::
342
343 function init (args)
344 local needs = {}
345 needs["protocol"] = "dns"
346 return needs
347 end
348
349If you are going to use the script for rule matching, choose one of the available DNS buffers listed in
350:ref:`lua-detection` and follow the pattern:
351
352::
353
354 function init (args)
355 local needs = {}
356 needs["dns.rrname"] = tostring(true)
357 return needs
358 end
359
0c4bf2d3
EL
360DnsGetQueries
361~~~~~~~~~~~~~
362
363::
364
365 dns_query = DnsGetQueries();
366 if dns_query ~= nil then
367 for n, t in pairs(dns_query) do
368 rrname = t["rrname"]
369 rrtype = t["type"]
370
371 print ("QUERY: " .. ts .. " " .. rrname .. " [**] " .. rrtype .. " [**] " ..
372 "TODO" .. " [**] " .. srcip .. ":" .. sp .. " -> " ..
373 dstip .. ":" .. dp)
374 end
375 end
376
377returns a table of tables
378
379DnsGetAnswers
380~~~~~~~~~~~~~
381
382::
383
384 dns_answers = DnsGetAnswers();
385 if dns_answers ~= nil then
386 for n, t in pairs(dns_answers) do
387 rrname = t["rrname"]
388 rrtype = t["type"]
389 ttl = t["ttl"]
390
391 print ("ANSWER: " .. ts .. " " .. rrname .. " [**] " .. rrtype .. " [**] " ..
392 ttl .. " [**] " .. srcip .. ":" .. sp .. " -> " ..
393 dstip .. ":" .. dp)
394 end
395 end
396
397returns a table of tables
398
399DnsGetAuthorities
400~~~~~~~~~~~~~~~~~
401
402::
403
404 dns_auth = DnsGetAuthorities();
405 if dns_auth ~= nil then
406 for n, t in pairs(dns_auth) do
407 rrname = t["rrname"]
408 rrtype = t["type"]
409 ttl = t["ttl"]
410
411 print ("AUTHORITY: " .. ts .. " " .. rrname .. " [**] " .. rrtype .. " [**] " ..
412 ttl .. " [**] " .. srcip .. ":" .. sp .. " -> " ..
413 dstip .. ":" .. dp)
414 end
415 end
416
417returns a table of tables
418
419DnsGetRcode
420~~~~~~~~~~~
421
422::
423
424 rcode = DnsGetRcode();
425 if rcode == nil then
426 return 0
427 end
428 print (rcode)
429
430returns a lua string with the error message, or nil
431
432DnsGetRecursionDesired
433~~~~~~~~~~~~~~~~~~~~~~
434
435::
436
437 if DnsGetRecursionDesired() == true then
438 print ("RECURSION DESIRED")
439 end
440
441returns a bool
442
443TLS
444---
445
e7f1736f 446For log output, initialize with:
0c4bf2d3
EL
447
448::
449
450 function init (args)
451 local needs = {}
452 needs["protocol"] = "tls"
453 return needs
454 end
455
e7f1736f
JF
456For detection, initialization is as follows:
457
458::
459
460 function init (args)
461 local needs = {}
462 needs["tls"] = tostring(true)
463 return needs
464 end
465
4d38d084
MK
466TlsGetVersion
467~~~~~~~~~~~~~
468
469Get the negotiated version in a TLS session as a string through TlsGetVersion.
470
471Example:
472
473::
474
475 function log (args)
476 version = TlsGetVersion()
477 if version then
478 -- do something
479 end
480 end
481
0c4bf2d3
EL
482TlsGetCertInfo
483~~~~~~~~~~~~~~
484
485Make certificate information available to the script through TlsGetCertInfo.
486
487Example:
488
489::
490
491 function log (args)
492 version, subject, issuer, fingerprint = TlsGetCertInfo()
493 if version == nil then
494 return 0
495 end
496 end
497
498TlsGetCertChain
499~~~~~~~~~~~~~~~
500
501Make certificate chain available to the script through TlsGetCertChain.
502
503The output is an array of certificate with each certificate being an hash
504with `data` and `length` keys.
505
506Example:
507
508::
509
510 -- Use debian lua-luaossl coming from https://github.com/wahern/luaossl
511 local x509 = require"openssl.x509"
512
513 chain = TlsGetCertChain()
514 for k, v in pairs(chain) do
515 -- v.length is length of data
516 -- v.data is raw binary data of certificate
517 cert = x509.new(v["data"], "DER")
518 print(cert:text() .. "\n")
519 end
520
521
522TlsGetCertNotAfter
523~~~~~~~~~~~~~~~~~~
524
525Get the Unix timestamp of end of validity of certificate.
526
527Example:
528
529::
530
531 function log (args)
532 notafter = TlsGetCertNotAfter()
533 if notafter < os.time() then
534 -- expired certificate
535 end
536 end
537
538TlsGetCertNotBefore
539~~~~~~~~~~~~~~~~~~~
540
541Get the Unix timestamp of beginning of validity of certificate.
542
543Example:
544
545::
546
547 function log (args)
548 notbefore = TlsGetCertNotBefore()
549 if notbefore > os.time() then
550 -- not yet valid certificate
551 end
552 end
553
554TlsGetCertSerial
555~~~~~~~~~~~~~~~~
556
557Get TLS certificate serial number through TlsGetCertSerial.
558
559Example:
560
561::
562
563 function log (args)
564 serial = TlsGetCertSerial()
565 if serial then
566 -- do something
567 end
568 end
569
570TlsGetSNI
571~~~~~~~~~
572
573Get the Server name Indication from a TLS connection.
574
575Example:
576
577::
578
579 function log (args)
580 asked_domain = TlsGetSNI()
581 if string.find(asked_domain, "badguys") then
582 -- ok connection to bad guys let's do someting
583 end
584 end
585
586
587JA3
588---
589
590JA3 must be enabled in the Suricata config file (set 'app-layer.protocols.tls.ja3-fingerprints' to 'yes').
591
e7f1736f 592For log output, initialize with:
0c4bf2d3
EL
593
594::
595
596 function init (args)
597 local needs = {}
598 needs["protocol"] = "tls"
599 return needs
600 end
601
e7f1736f
JF
602For detection, initialization is as follows:
603
604::
605
606 function init (args)
607 local needs = {}
608 needs["tls"] = tostring(true)
609 return needs
610 end
611
0c4bf2d3
EL
612Ja3GetHash
613~~~~~~~~~~
614
615Get the JA3 hash (md5sum of JA3 string) through Ja3GetHash.
616
617Example:
618
619::
620
621 function log (args)
622 hash = Ja3GetHash()
623 if hash == nil then
624 return
625 end
626 end
627
628Ja3GetString
629~~~~~~~~~~~~
630
631Get the JA3 string through Ja3GetString.
632
633Example:
634
635::
636
637 function log (args)
638 str = Ja3GetString()
639 if str == nil then
640 return
641 end
642 end
643
37a05944
MK
644Ja3SGetHash
645~~~~~~~~~~~
646
647Get the JA3S hash (md5sum of JA3S string) through JA3SGetHash.
648
e7f1736f 649Examples:
37a05944
MK
650
651::
652
653 function log (args)
654 hash = Ja3SGetHash()
655 if hash == nil then
656 return
657 end
658 end
0c4bf2d3 659
e7f1736f
JF
660Or, for detection:
661
662::
663
664 function match (args)
665 hash = Ja3SGetHash()
666 if hash == nil then
667 return 0
668 end
669
670 // matching code
671
672 return 0
673 end
674
d15903a2
MK
675JA3SGetString
676~~~~~~~~~~~~~
677
678Get the JA3S string through Ja3SGetString.
679
e7f1736f 680Examples:
d15903a2
MK
681
682::
683
684 function log (args)
685 str = Ja3SGetString()
686 if str == nil then
687 return
688 end
689 end
690
e7f1736f
JF
691Or, for detection:
692
693::
694
695 function match (args)
696 str = Ja3SGetString()
697 if str == nil then
698 return 0
699 end
700
701 // matching code
702
703 return 0
704 end
705
0c4bf2d3
EL
706SSH
707---
708
709Initialize with:
710
711::
712
0c4bf2d3
EL
713 function init (args)
714 local needs = {}
715 needs["protocol"] = "ssh"
716 return needs
717 end
718
719SshGetServerProtoVersion
720~~~~~~~~~~~~~~~~~~~~~~~~
721
722Get SSH protocol version used by the server through SshGetServerProtoVersion.
723
724Example:
725
726::
727
728 function log (args)
729 version = SshGetServerProtoVersion()
730 if version == nil then
731 return 0
732 end
733 end
734
735SshGetServerSoftwareVersion
736~~~~~~~~~~~~~~~~~~~~~~~~~~~
737
738Get SSH software used by the server through SshGetServerSoftwareVersion.
739
740Example:
741
742::
743
0c4bf2d3
EL
744 function log (args)
745 software = SshGetServerSoftwareVersion()
746 if software == nil then
747 return 0
748 end
749 end
750
751SshGetClientProtoVersion
752~~~~~~~~~~~~~~~~~~~~~~~~
753
754Get SSH protocol version used by the client through SshGetClientProtoVersion.
755
756Example:
757
758::
759
760 function log (args)
761 version = SshGetClientProtoVersion()
762 if version == nil then
763 return 0
764 end
765 end
766
767SshGetClientSoftwareVersion
768~~~~~~~~~~~~~~~~~~~~~~~~~~~
769
770Get SSH software used by the client through SshGetClientSoftwareVersion.
771
772Example:
773
774::
775
776 function log (args)
777 software = SshGetClientSoftwareVersion()
778 if software == nil then
779 return 0
780 end
781 end
782
a80f705d
VM
783
784HasshGet
785~~~~~~~~
786
787Get MD5 of hassh algorithms used by the client through HasshGet.
788
789Example:
790
791::
792
793 function log (args)
794 hassh = HasshGet()
795 if hassh == nil then
796 return 0
797 end
798 end
799
800HasshGetString
801~~~~~~~~~~~~~~
802
803Get hassh algorithms used by the client through HasshGetString.
804
805Example:
806
807::
808
809 function log (args)
810 hassh_string = HasshGetString()
811 if hassh == nil then
812 return 0
813 end
814 end
815
816HasshServerGet
817~~~~~~~~~~~~~~
818
819Get MD5 of hassh algorithms used by the server through HasshServerGet.
820
821Example:
822
823::
824
825 function log (args)
826 hassh_string = HasshServerGet()
827 if hassh == nil then
828 return 0
829 end
830 end
831
832HasshServerGetString
833~~~~~~~~~~~~~~~~~~~~
834
835Get hassh algorithms used by the server through HasshServerGetString.
836
837Example:
838
839::
840
841 function log (args)
842 hassh_string = HasshServerGetString()
843 if hassh == nil then
844 return 0
845 end
846 end
847
848
0c4bf2d3
EL
849Files
850-----
851
852To use the file logging API, the script's init() function needs to look like:
853
854::
855
856 function init (args)
857 local needs = {}
858 needs['type'] = 'file'
859 return needs
860 end
861
862SCFileInfo
863~~~~~~~~~~
864
865::
866
867
751906b7 868 fileid, txid, name, size, magic, md5, sha1, sha256 = SCFileInfo()
0c4bf2d3
EL
869
870returns fileid (number), txid (number), name (string), size (number),
751906b7 871magic (string), md5 in hex (string), sha1 (string), sha256 (string)
0c4bf2d3
EL
872
873SCFileState
874~~~~~~~~~~~
875
876::
877
878 state, stored = SCFileState()
879
880returns state (string), stored (bool)
881
882Alerts
883------
884
885Alerts are a subset of the 'packet' logger:
886
887::
888
889 function init (args)
890 local needs = {}
891 needs["type"] = "packet"
892 needs["filter"] = "alerts"
893 return needs
894 end
895
896SCRuleIds
897~~~~~~~~~
898
899::
900
901 sid, rev, gid = SCRuleIds()
902
9b840104
CS
903SCRuleAction
904~~~~~~~~~~~~
905
906::
907
908 action = SCRuleAction()
909
910returns one of 'pass', 'reject', 'drop' or 'alert'
911
0c4bf2d3
EL
912SCRuleMsg
913~~~~~~~~~
914
915::
916
917 msg = SCRuleMsg()
918
919SCRuleClass
920~~~~~~~~~~~
921
922::
923
924
925 class, prio = SCRuleClass()
926
927Streaming Data
928--------------
929
930Streaming data can currently log out reassembled TCP data and
931normalized HTTP data. The script will be invoked for each consecutive
932data chunk.
933
934In case of TCP reassembled data, all possible overlaps are removed
935according to the host OS settings.
936
937::
938
939 function init (args)
940 local needs = {}
941 needs["type"] = "streaming"
942 needs["filter"] = "tcp"
943 return needs
944 end
945
946In case of HTTP body data, the bodies are unzipped and dechunked if applicable.
947
948::
949
950 function init (args)
951 local needs = {}
952 needs["type"] = "streaming"
953 needs["protocol"] = "http"
954 return needs
955 end
956
957SCStreamingBuffer
958~~~~~~~~~~~~~~~~~
959
960::
961
962 function log(args)
963 data = SCStreamingBuffer()
964 hex_dump(data)
965 end
966
2546e86a
EL
967Flow variables
968--------------
969
970It is possible to access, define and modify Flow variables from Lua. To do so,
971you must use the functions described in this section and declare the counter in
972init function:
973
974::
975
976 function init(args)
977 local needs = {}
978 needs["tls"] tostring(true)
979 needs["flowint"] = {"tls-cnt"}
980 return needs
981 end
982
983Here we define a `tls-cnt` Flowint that can now be used in output or in a
b116a56a 984signature via dedicated functions. The access to the Flow variable is done by
2546e86a
EL
985index so in our case we need to use 0.
986
987::
988
989 function match(args)
25e94831 990 a = SCFlowintGet(0);
2546e86a 991 if a then
25e94831 992 SCFlowintSet(0, a + 1)
2546e86a 993 else
25e94831 994 SCFlowintSet(0, 1)
2546e86a
EL
995 end
996
25e94831 997SCFlowintGet
2546e86a
EL
998~~~~~~~~~~~~
999
1000Get the Flowint at index given by the parameter.
1001
25e94831 1002SCFlowintSet
2546e86a
EL
1003~~~~~~~~~~~~
1004
1005Set the Flowint at index given by the first parameter. The second parameter is the value.
1006
25e94831 1007SCFlowintIncr
2546e86a
EL
1008~~~~~~~~~~~~~
1009
1010Increment Flowint at index given by the first parameter.
1011
25e94831 1012SCFlowintDecr
2546e86a
EL
1013~~~~~~~~~~~~~
1014
1015Decrement Flowint at index given by the first parameter.
1016
25e94831 1017SCFlowvarGet
2546e86a
EL
1018~~~~~~~~~~~~
1019
1020Get the Flowvar at index given by the parameter.
1021
25e94831 1022SCFlowvarSet
2546e86a
EL
1023~~~~~~~~~~~~
1024
1025Set a Flowvar. First parameter is the index, second is the data
1026and third is the length of data.
1027
1028You can use it to set string
1029
1030::
1031
1032 function init (args)
1033 local needs = {}
1034 needs["http.request_headers"] = tostring(true)
1035 needs["flowvar"] = {"cnt"}
1036 return needs
1037 end
1038
1039 function match(args)
25e94831 1040 a = SCFlowvarGet(0);
2546e86a
EL
1041 if a then
1042 a = tostring(tonumber(a)+1)
25e94831 1043 SCFlowvarSet(0, a, #a)
2546e86a
EL
1044 else
1045 a = tostring(1)
25e94831 1046 SCFlowvarSet(0, a, #a)
2546e86a
EL
1047 end
1048
0c4bf2d3
EL
1049Misc
1050----
1051
1052SCThreadInfo
1053~~~~~~~~~~~~
1054
1055::
1056
1057 tid, tname, tgroup = SCThreadInfo()
1058
1059It gives: tid (integer), tname (string), tgroup (string)
1060
1061SCLogError, SCLogWarning, SCLogNotice, SCLogInfo, SCLogDebug
1062~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1063
1064Print a message. It will go into the outputs defined in the
1065yaml. Whether it will be printed depends on the log level.
1066
1067Example:
1068
1069::
1070
1071 SCLogError("some error message")
1072
1073SCLogPath
1074~~~~~~~~~
1075
1076Expose the log path.
1077
1078::
1079
1080
1081 name = "fast_lua.log"
1082 function setup (args)
1083 filename = SCLogPath() .. "/" .. name
1084 file = assert(io.open(filename, "a"))
1085 end