├── LICENCE
├── README.md
├── analyze-filters.c
├── analyze-filters.pl
├── checkLdapPwdExpiration.sh
├── cleanLdapBrokenAliases.sh
├── convertgroup.pl
├── convertldif.pl
├── file2ldif.pl
├── ldap-stats.pl
└── spreadPwdChangedTime.pl
/LICENCE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LDAP Scripts
2 |
3 | ## Presentation
4 |
5 | This is a collection of scripts for LDAP directories:
6 | * Send warning mails on password expiration
7 | * Check broken aliases
8 | * Data conversion
9 |
10 | ## Documentation
11 |
12 | See http://ltb-project.org/wiki/documentation#scripts
13 |
14 | ## Download
15 |
16 | See http://ltb-project.org/wiki/download#other
17 |
--------------------------------------------------------------------------------
/analyze-filters.c:
--------------------------------------------------------------------------------
1 | /*
2 | Program: Analyze filters in OpenLDAP logs
3 |
4 | Source code home: https://github.com/ltb-project/ldap-scripts/analyze-filters.c
5 |
6 | Author: LDAP Tool Box project
7 | Author: David Coutadeur
8 |
9 | Current Version: 1
10 |
11 | Purpose:
12 | Display the number of occurrences for each type of filter in OpenLDAP logs
13 | Mainly used for index tuning
14 |
15 | License:
16 |
17 | Redistribution and use in source and binary forms, with or without
18 | modification, are permitted only as authorized by the OpenLDAP
19 | Public License.
20 |
21 | A copy of this license is available in the file LICENSE in the
22 | top-level directory of the distribution or, alternatively, at
23 | .
24 |
25 | Installation:
26 | 1. Enable a minimum of 'loglevel 256' in OpenLDAP configuration
27 | 2. Copy the program to a suitable location.
28 | 3. Refer to the usage section for options and examples.
29 |
30 | Usage:
31 | gcc -Wall -o analyze-filters analyze-filters.c -lpcre2-8
32 | ./analyze-filters slapd.log
33 | */
34 |
35 | #define PCRE2_CODE_UNIT_WIDTH 8
36 | #define LINE_MAX_SIZE 65536
37 | #define FILTER_MAX_SIZE 16384
38 | #define FILTER_COMP_MAX_SIZE 256
39 | #define MAX_FILTERS 2048
40 | #define ATTR_MAX_SIZE 256
41 | #define VAL_MAX_SIZE 1024
42 |
43 |
44 | #include
45 | #include
46 | #include
47 | #include
48 |
49 | typedef struct sfilter sfilter;
50 | struct sfilter
51 | {
52 | char filter[FILTER_MAX_SIZE];
53 | int occurrence;
54 | };
55 |
56 | int min_length(char *string1, char *string2)
57 | {
58 | int i = strlen(string1);
59 | int j = strlen(string2);
60 |
61 | if(i <= j)
62 | {
63 | return i;
64 | }
65 | else
66 | {
67 | return j;
68 | }
69 | }
70 |
71 | void insert_filter(sfilter *full_filter, char *formatted_filter)
72 | {
73 | int i = 0;
74 |
75 | // search for an existing filter
76 | while( full_filter[i].occurrence != 0 )
77 | {
78 | if(strncmp(full_filter[i].filter,
79 | formatted_filter,
80 | min_length(full_filter[i].filter, formatted_filter) ) == 0)
81 | {
82 | // found identical filter
83 | // just increment occurrence
84 | full_filter[i].occurrence++;
85 | return;
86 | }
87 |
88 | i++;
89 | }
90 |
91 | // else, existing filter not found, just add it to the end
92 | strncpy(full_filter[i].filter,formatted_filter,strlen(formatted_filter));
93 | full_filter[i].filter[strlen(formatted_filter)] = '\0';
94 | full_filter[i].occurrence++;
95 |
96 | full_filter[(i+1)].occurrence = 0;
97 | }
98 |
99 | // replace "src" leading characters by "dst"
100 | void leadingpad(char *string, char src, char dst)
101 | {
102 | int i = 0;
103 | while( string[i] == src )
104 | {
105 | string[i] = dst;
106 | i++;
107 | }
108 | }
109 |
110 | void display_filters(sfilter *full_filter)
111 | {
112 | int i = 0;
113 | char occurrence[13];
114 |
115 | while( full_filter[i].occurrence != 0 )
116 | {
117 | sprintf(occurrence, "%12d", full_filter[i].occurrence);
118 | leadingpad(occurrence, '0',' ');
119 | printf("|%s | %62s |\n" , occurrence, full_filter[i].filter);
120 | i++;
121 | }
122 |
123 | }
124 |
125 | void swap_filters(sfilter *full_filter, int i, int j)
126 | {
127 | int occurrence;
128 | char filter[FILTER_MAX_SIZE];
129 |
130 | // copy i filter to temporary variables
131 | occurrence = full_filter[i].occurrence;
132 | strncpy(filter, full_filter[i].filter, strlen(full_filter[i].filter));
133 | filter[strlen(full_filter[i].filter)] = '\0';
134 |
135 | // replace i filter with j values
136 | full_filter[i].occurrence = full_filter[j].occurrence;
137 | strncpy(full_filter[i].filter, full_filter[j].filter, strlen(full_filter[j].filter));
138 | full_filter[i].filter[strlen(full_filter[j].filter)] = '\0';
139 |
140 | // replace j filter with i values
141 | full_filter[j].occurrence = occurrence;
142 | strncpy(full_filter[j].filter, filter, strlen(filter));
143 | full_filter[j].filter[strlen(filter)] = '\0';
144 | }
145 |
146 | void sort_filters(sfilter *full_filter)
147 | {
148 | int i, j;
149 | int max;
150 |
151 | i = 0;
152 | while( full_filter[i].occurrence != 0 )
153 | {
154 | max = i;;
155 | j = i + 1;
156 | while( full_filter[j].occurrence != 0 )
157 | {
158 | if(full_filter[j].occurrence > full_filter[max].occurrence)
159 | {
160 | max = j;
161 | }
162 | j++;
163 | }
164 | if(max != i)
165 | {
166 | swap_filters(full_filter, max, i);
167 | }
168 | i++;
169 | }
170 |
171 | }
172 |
173 | // Replace all non * word by
174 | void format_value(char *formatted_value, char *value)
175 | {
176 | char pattern[] = "";
177 | char delim = '*';
178 |
179 | char *cursor;
180 | char *start = value;
181 |
182 | for( cursor = value; cursor[0] != '\0' ; cursor++ )
183 | {
184 | if( cursor[0] == delim )
185 | {
186 | if( (cursor - start) > 0 )
187 | {
188 | strcat(formatted_value, pattern );
189 | }
190 | strncat(formatted_value, &delim, 1 );
191 | start = cursor;
192 | start++;
193 | }
194 | }
195 | if( (cursor - start) > 0 )
196 | {
197 | strcat(formatted_value, pattern );
198 | }
199 | }
200 |
201 | void compute_filter(sfilter *full_filter, sfilter *comp_filter, char *current_filter, pcre2_code *ref)
202 | {
203 |
204 | PCRE2_SPTR cursor;
205 |
206 | /* for pcre2_match */
207 | int rc;
208 | PCRE2_SIZE* ovector;
209 | uint32_t options = 0;
210 | pcre2_match_data *match_data;
211 | uint32_t ovecsize = 128;
212 | PCRE2_SPTR attr_start, val_start;
213 | PCRE2_SIZE attr_len, val_len;
214 | PCRE2_SIZE attr_pos = 1; // first match is at position 1
215 | PCRE2_SIZE val_pos = 2; // second match is at position 2
216 |
217 | // temporary string to store attribute
218 | char attribute[ATTR_MAX_SIZE];
219 | char value[VAL_MAX_SIZE];
220 | char formatted_value[VAL_MAX_SIZE];
221 | char formatted_filter[FILTER_MAX_SIZE] = "";
222 | char formatted_comp_filter[FILTER_COMP_MAX_SIZE];
223 |
224 |
225 | cursor = (PCRE2_SPTR) current_filter;
226 | match_data = pcre2_match_data_create(ovecsize, NULL);
227 | while ( (rc = pcre2_match(ref, cursor, strlen((char *)cursor), 0, options, match_data, NULL)) >= 0 )
228 | {
229 |
230 | if(rc == 0) {
231 | // error
232 | fprintf(stderr,"offset vector too small: %d\n",rc);
233 | }
234 | else if(rc == 3) // 3 = one regex matching (1) + two matching group (2)
235 | {
236 | formatted_value[0] = '\0'; // empty string
237 | formatted_comp_filter[0] = '\0'; // reinitialize component filter
238 |
239 | ovector = pcre2_get_ovector_pointer(match_data);
240 | attr_start = cursor + ovector[2*attr_pos];
241 | attr_len = ovector[2*attr_pos+1] - ovector[2*attr_pos];
242 | val_start = cursor + ovector[2*val_pos];
243 | val_len = ovector[2*val_pos+1] - ovector[2*val_pos];
244 |
245 |
246 | // get attribute
247 | strncpy (attribute, (char *)attr_start, (int)attr_len );
248 | attribute[(int)attr_len] = '\0';
249 |
250 | // get value
251 | strncpy (value, (char *)val_start, (int)val_len );
252 | value[(int)val_len] = '\0';
253 | format_value(formatted_value, value);
254 |
255 | // compute component filter, and store it
256 | strcat(formatted_comp_filter, "(");
257 | strcat(formatted_comp_filter, attribute);
258 | strcat(formatted_comp_filter, "=");
259 | strcat(formatted_comp_filter, formatted_value);
260 | strcat(formatted_comp_filter, ")");
261 | insert_filter(comp_filter, formatted_comp_filter);
262 |
263 | // combine format_filter parts for computing full_filter
264 | strncat(formatted_filter, (char *)cursor, ovector[2*attr_pos]);
265 | strcat(formatted_filter, attribute);
266 | strcat(formatted_filter, "=");
267 | strcat(formatted_filter, formatted_value);
268 | strcat(formatted_filter, ")");
269 |
270 | cursor += ovector[2*val_pos+1];
271 | cursor++;
272 |
273 | }
274 | else
275 | {
276 | fprintf(stderr,"dummy capture groupe number: %d\n",rc);
277 | }
278 |
279 | }
280 | pcre2_match_data_free(match_data);
281 |
282 | strcat(formatted_filter, (char *)cursor);
283 | insert_filter(full_filter, formatted_filter);
284 |
285 | }
286 |
287 | int main( int argc, char **argv )
288 | {
289 | // file stuff
290 | FILE * fp;
291 | PCRE2_SPTR line = (PCRE2_SPTR) malloc(LINE_MAX_SIZE * sizeof(char));
292 |
293 | // regex stuff
294 | /* for pcre2_compile */
295 | pcre2_code *re, *ref;
296 | PCRE2_SIZE erroffset;
297 | int errcode;
298 | PCRE2_UCHAR8 buffer[128];
299 |
300 | /* for pcre2_match */
301 | int rc;
302 | PCRE2_SIZE* ovector;
303 |
304 | PCRE2_SPTR pattern = (PCRE2_SPTR) "filter=\"([^\"]+)\"";
305 | size_t pattern_size = strlen((char*) pattern);
306 |
307 | PCRE2_SPTR patternf = (PCRE2_SPTR) "\\(([^=(]+)=([^)]+)\\)";
308 | size_t patternf_size = strlen((char*) patternf);
309 |
310 | uint32_t options = 0;
311 |
312 | pcre2_match_data *match_data;
313 | uint32_t ovecsize = 128;
314 |
315 | // temporary string to store current filter
316 | char current_filter[FILTER_MAX_SIZE];
317 |
318 | // structures storing the filters
319 | sfilter *full_filter = malloc(MAX_FILTERS * sizeof(sfilter));
320 | sfilter *comp_filter = malloc(MAX_FILTERS * sizeof(sfilter));
321 | for( int i=0 ; i < MAX_FILTERS ; i++ )
322 | {
323 | full_filter[i].filter[0] = '\0';
324 | full_filter[i].occurrence = 0;
325 | comp_filter[i].filter[0] = '\0';
326 | comp_filter[i].occurrence = 0;
327 | }
328 |
329 |
330 | if(argc < 2)
331 | {
332 | fprintf(stderr,"Missing file name\n");
333 | exit(1);
334 | }
335 |
336 | re = pcre2_compile(pattern, pattern_size, options, &errcode, &erroffset, NULL);
337 | if (re == NULL)
338 | {
339 | pcre2_get_error_message(errcode, buffer, 120);
340 | fprintf(stderr,"%d\t%s\n", errcode, buffer);
341 | return 1;
342 | }
343 |
344 | ref = pcre2_compile(patternf, patternf_size, options, &errcode, &erroffset, NULL);
345 | if (ref == NULL)
346 | {
347 | pcre2_get_error_message(errcode, buffer, 120);
348 | fprintf(stderr,"%d\t%s\n", errcode, buffer);
349 | return 1;
350 | }
351 |
352 |
353 | for( int i = 1; i < argc ; i++ )
354 | {
355 | fp = fopen(argv[i], "r");
356 | if (fp == NULL)
357 | {
358 | fprintf(stderr,"Error while trying to open %s\n", argv[i]);
359 | exit(1);
360 | }
361 |
362 | // parse file
363 | while (fgets((char*) line,LINE_MAX_SIZE, fp))
364 | {
365 | // only get filter="..." part
366 | match_data = pcre2_match_data_create(ovecsize, NULL);
367 | rc = pcre2_match(re, line, strlen((char*) line), 0, options, match_data, NULL);
368 |
369 | if(rc == 0) {
370 | // error
371 | fprintf(stderr,"offset vector too small: %d\n",rc);
372 | }
373 | else if(rc == 2) // 2 = regex matching (1) + one matching group (1)
374 | {
375 | ovector = pcre2_get_ovector_pointer(match_data);
376 | PCRE2_SIZE i = 1; // first match is at position 1
377 | PCRE2_SPTR start = line + ovector[2*i];
378 | PCRE2_SIZE slen = ovector[2*i+1] - ovector[2*i];
379 | strncpy (current_filter, (char *)start, (int)slen );
380 | current_filter[(int)slen] = '\0';
381 | compute_filter(full_filter, comp_filter, current_filter, ref);
382 | }
383 | else if (rc < 0)
384 | {
385 | // no match
386 | }
387 |
388 | pcre2_match_data_free(match_data);
389 |
390 | }
391 |
392 | fclose(fp);
393 | }
394 | free((char*) line);
395 |
396 | pcre2_code_free(re);
397 | pcre2_code_free(ref);
398 |
399 | sort_filters(full_filter);
400 | printf("| Occurrences | Full filters |\n");
401 | printf("+-------------+----------------------------------------------------------------+\n");
402 | display_filters(full_filter);
403 |
404 | sort_filters(comp_filter);
405 | printf("\n");
406 | printf("| Occurrences | Filter components |\n");
407 | printf("+-------------+----------------------------------------------------------------+\n");
408 | display_filters(comp_filter);
409 |
410 | free(full_filter);
411 | free(comp_filter);
412 |
413 | exit(0);
414 | }
415 |
--------------------------------------------------------------------------------
/analyze-filters.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 | # Program: Analyze filters in OpenLDAP logs
3 | #
4 | # Source code home: https://github.com/ltb-project/ldap-scripts/analyze-filters.pl
5 | #
6 | # Author: LDAP Tool Box project
7 | # Author: David Coutadeur
8 | #
9 | # Current Version: 1
10 | #
11 | # Purpose:
12 | # Display the number of occurrences for each type of filter in OpenLDAP logs
13 | # Mainly used for index tuning
14 | #
15 | # License:
16 | #
17 | # Redistribution and use in source and binary forms, with or without
18 | # modification, are permitted only as authorized by the OpenLDAP
19 | # Public License.
20 | #
21 | # A copy of this license is available in the file LICENSE in the
22 | # top-level directory of the distribution or, alternatively, at
23 | # .
24 | #
25 | # Installation:
26 | # 1. Enable a minimum of 'loglevel 256' in OpenLDAP configuration
27 | # 2. Copy the perl script to a suitable location.
28 | # 3. Refer to the usage section for options and examples.
29 | #
30 | # Usage:
31 | # ./analyze-filters.pl slapd.log
32 | #
33 |
34 | use strict;
35 | use warnings;
36 | use Data::Dumper;
37 |
38 |
39 | # Function replacing static values by the tag
40 | # don't replace * in the values
41 | sub format_value
42 | {
43 | my $value = shift;
44 | $value =~ s/[^*]+//g;
45 | return $value;
46 | }
47 |
48 | # Get file from arguments passed to script
49 | unless( @ARGV)
50 | {
51 | print "missing file to analyze\n";
52 | exit 1;
53 | }
54 |
55 | my $full_filters; # { "full_filter" => occurrence }
56 | my $comp_filters; # { "component_filter" => occurrence }
57 |
58 |
59 | foreach my $file (@ARGV)
60 | {
61 |
62 | print "Analyze file $file\n";
63 |
64 | open(my $fh, "<", "$file") or die "Can't open < $file: $!";
65 | while(my $line = <$fh>)
66 | {
67 | if( $line =~ /filter="([^"]+)"/ )
68 | {
69 | my $full_filter = "$1";
70 | my $comp_filter = "$1";
71 |
72 | # Compute full filter
73 | $full_filter =~ s/\(([^=(]+)=([^)]+)\)/"($1=" . &format_value("$2") . ")"/eg;
74 | $full_filters->{$full_filter}++;
75 |
76 | # Compute components of filter
77 | while ($comp_filter =~ /\(([^=(]+)=([^)]+)\)/g) {
78 | $comp_filters->{"($1=" . &format_value("$2") . ")"}++;
79 | }
80 | }
81 | }
82 | }
83 |
84 | # Print table of full_filters, ordered by occurrences
85 | print "| Occurrences | Full filters |\n";
86 | print "+-------------+----------------------------------------------------------------+\n";
87 | foreach my $filter (sort {$full_filters->{$b} <=> $full_filters->{$a}} keys %$full_filters) {
88 | print sprintf "|%12s | %62s |\n", $full_filters->{$filter}, $filter;
89 | }
90 |
91 | # Print table of filter components, ordered by occurrences
92 | print "\n";
93 | print "| Occurrences | Filter components |\n";
94 | print "+-------------+----------------------------------------------------------------+\n";
95 | foreach my $filter (sort {$comp_filters->{$b} <=> $comp_filters->{$a}} keys %$comp_filters) {
96 | print sprintf "|%12s | %62s |\n", $comp_filters->{$filter}, $filter;
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/checkLdapPwdExpiration.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #====================================================================
4 | # Script for OpenLDAP with ppolicy overlay
5 | #
6 | # Does searches on LDAP directory to determine which user passwords
7 | # came to expiration. If so, sends mails to concerned users.
8 | #
9 | # Tested on :
10 | # - GNU/Linux platform ;
11 | # - SunOS 8.5 platform ;
12 | #
13 | # Dependences :
14 | # - gawk
15 | # - ldapsearch
16 | # - mailx
17 | #
18 | # Copyright (C) 2008 Clement OUDOT
19 | # Copyright (C) 2007 Thomas CHEMINEAU
20 | # Copyright (C) 2009 LTB-project.org
21 | # Copyright (C) 2019 Worteks
22 | #
23 | # This program is free software; you can redistribute it and/or
24 | # modify it under the terms of the GNU General Public License
25 | # as published by the Free Software Foundation; either version 2
26 | # of the License, or (at your option) any later version.
27 | #
28 | # This program is distributed in the hope that it will be useful,
29 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 | # GNU General Public License for more details.
32 | #
33 | # GPL License: http://www.gnu.org/licenses/gpl.txt
34 | #
35 | #====================================================================
36 |
37 | #====================================================================
38 | # Changelog
39 | #====================================================================
40 | # Version 0.5 (12/2020)
41 | # - Add parameters for ldapsearch (to make socket connections or no-wrap or pagination with pr=500/noprompt)
42 | # - Add parameter for the "from:" of the mail (mailx can't send with From in the body)
43 | # - Exclude some noise from the ldap global search result (ldap pagination print some comment's lines)
44 | # - >"LIMIT: multi-lined DN causes errors" - so use '-o ldif-wrap=no' in 'LDAP_PARAM'
45 | # Version 0.4 (11/2019)
46 | # - #11 fixed:
47 | # Add remaning time before password expiration in mail.
48 | # Version 0.3 (03/2010):
49 | # - #295 fixed:
50 | # Add a parameter to specify the search scope
51 | # Get compatibility with cut behaviour on SUSE system
52 | # Version 0.2 (08/2008):
53 | # - Use zulu time (GMT) for currentTime
54 | # - Split mail command parameters (binary and subject)
55 | # - Add script statitics to STDOUT
56 | # - Add debug info to STDERR
57 | # - Use ppolicy warning time for mail delay if provided
58 | # - Manage no default ppolicy case (just per-user ppolicies)
59 | # - LDAP user attributes are now configurable
60 | # - Jump to next user if no password change date or no ppolicy
61 | # LIMIT: multi-lined DN causes errors
62 | # TODO: use GMT time for SunOS and test the script for this OS
63 | # Author: Clement OUDOT (LINAGORA)
64 | #
65 | # Version 0.1 (2007):
66 | # - First version
67 | # Author: Thomas CHEMINEAU (LINAGORA)
68 | #====================================================================
69 |
70 |
71 | #====================================================================
72 | # Configuration
73 | #====================================================================
74 |
75 |
76 |
77 | #
78 | # LDAP host URI
79 | # eg: ldap://localhost:389
80 | # eg: ldapi://
81 | #
82 | MY_LDAP_HOSTURI="ldap://localhost:389"
83 |
84 | #
85 | # LDAP custom parameters
86 | # eg: -E pr=500/noprompt
87 | # eg: -o ldif-wrap=no
88 | # eg: -Q -Y EXTERNAL
89 | # eg (default): -x
90 | #
91 | # For Authentication use : -E pr=500/noprompt -o ldif-wrap=no -x
92 | # For socket use : -E pr=500/noprompt -o ldif-wrap=no -Q -Y EXTERNAL
93 | #
94 | #LDAP_PARAM="-E pr=500/noprompt -o ldif-wrap=no -x"
95 |
96 | #
97 | # LDAP root DN (optional)
98 | # eg: cn=Manager,dc=example,dc=com
99 | #
100 | #MY_LDAP_ROOTDN="cn=manager,dc=example,dc=com"
101 |
102 | #
103 | # LDAP root password (optional)
104 | #
105 | #MY_LDAP_ROOTPW="secret"
106 |
107 | #
108 | # LDAP default password policy DN
109 | # eg: ou=defaultPasswordPolicy,dc=example,dc=com
110 | # If commented, we suppose there are no default, and only per-user policies
111 | #
112 | #MY_LDAP_DEFAULTPWDPOLICYDN="ou=defaultPasswordPolicy,dc=example,dc=com"
113 |
114 | #
115 | # LDAP search base for users
116 | # eg: ou=People,dc=example,dc=com
117 | #
118 | MY_LDAP_SEARCHBASE="ou=People,dc=example,dc=com"
119 |
120 | #
121 | # LDAP search filter to use to get all users
122 | #
123 | MY_LDAP_SEARCHFILTER="(&(uid=*)(objectClass=inetOrgPerson))"
124 |
125 | #
126 | # LDAP search scope to use to get all users
127 | #
128 | MY_LDAP_SEARCHSCOPE="one"
129 |
130 | #
131 | # Path to LDAP search binary
132 | #
133 | MY_LDAP_SEARCHBIN="/usr/local/openldap/bin/ldapsearch"
134 |
135 | #
136 | # Delay to begin sending adverts
137 | # Comment to use the pwdExpireWarning value of the user's Password Policy
138 | #
139 | #MY_MAIL_DELAY=1296000
140 |
141 | #
142 | # LDAP attributes storing user's information
143 | # NAME: Display name of the user
144 | # LOGIN: Account ID of the user
145 | # MAIL: Email of the user
146 | #
147 | MY_LDAP_NAME_ATTR=cn
148 | MY_LDAP_LOGIN_ATTR=uid
149 | MY_LDAP_MAIL_ATTR=mail
150 |
151 | #
152 | # Locale for date
153 | # eg: export LC_ALL=en_US.UTF-8
154 | #
155 | export LC_ALL=en_US.UTF-8
156 |
157 | #
158 | # Mail from
159 | #
160 | #MY_MAIL_FROM="noreply@yo.com"
161 |
162 | # Mail body message, with particular variables :
163 | # %name : user name
164 | # %login : user login
165 | #
166 | MY_MAIL_BODY="From: noreply@example.com\n\n \
167 | Hi %name,\n\n \
168 |
169 | Please change your password. It will expire in %expireDays days on %expireTimeTZ.\n\n \
170 |
171 | As a reminder, the password policy is :\n\n \
172 |
173 | - Minimum Password Length : %pwdMinLength characters\n\n \
174 | - There is a password history, your new password must be different from you last %pwdInHistory passwords.\n\n \
175 |
176 | The LDAP team."
177 |
178 | #
179 | # Mail subject
180 | #
181 | MY_MAIL_SUBJECT="Your account will expire soon"
182 |
183 | #
184 | # Mail command binary
185 | # Replace mailx by mail for RedHat
186 | #
187 | MY_MAIL_BIN="mail"
188 |
189 | #
190 | # Log header format
191 | # Could include unix commands
192 | #
193 | MY_LOG_HEADER="$(date +\"%b %e %T\") $(hostname) $0[$$]:"
194 |
195 | #
196 | # Path to GAWK (GNU awk) binary
197 | #
198 | MY_GAWK_BIN="/usr/bin/gawk"
199 |
200 | #====================================================================
201 | # Functions
202 | #====================================================================
203 |
204 | #
205 | # Retrieves date in seconds.
206 | # This function could take one parameter, a time returned by the command
207 | # `date +"%Y %m %d %H %M %S"`. Without parameter, it returns GMT time.
208 | #
209 | getTimeInSeconds() {
210 | date=0
211 | os=$(uname -s)
212 |
213 | if [ "$1" ]; then
214 | date=$(TZ=UTC ${MY_GAWK_BIN} 'BEGIN { \
215 | if (ARGC == 2) { \
216 | print mktime(ARGV[1]) \
217 | } \
218 | exit 0 }' "$1")
219 | else
220 | if [ "${os}" = "SunOS" ]; then
221 | # Under Sun Solaris, there is no simple way to
222 | # retrieve epoch time.
223 | # TODO: manage zulu time (GMT)
224 | date=$(/usr/bin/truss /usr/bin/date 2>&1 | nawk -F= \
225 | '/^time\(\)/ {gsub(/ /,"",$2);print $2}')
226 | else
227 | now=$(date +"%Y %m %d %H %M %S" -u)
228 | date=$(getTimeInSeconds "$now")
229 | fi
230 | fi
231 |
232 | echo "${date}"
233 | }
234 |
235 | #====================================================================
236 | # Script
237 | #====================================================================
238 |
239 | ## Variables initialization
240 | tmp_dir="/tmp/$$.checkldap.tmp"
241 | result_file="${tmp_dir}/res.tmp.1"
242 | buffer_file="${tmp_dir}/buf.tmp.1"
243 | [ -z "${MY_MAIL_FROM}" ] || MY_MAIL_BIN="${MY_MAIL_BIN} -r "
244 | [ -z "${LDAP_PARAM}" ] && LDAP_PARAM="-x" # default authorization
245 | echo "${LDAP_PARAM}" | grep -E "Q|Y|x" 1>/dev/null || LDAP_PARAM="${LDAP_PARAM} -x"
246 | ldap_param="${LDAP_PARAM} -LLL -H ${MY_LDAP_HOSTURI}"
247 | nb_users=0
248 | nb_expired_users=0
249 | nb_warning_users=0
250 |
251 | ## Some tests
252 | if [ -d ${tmp_dir} ]; then
253 | echo "Error : temporary directory exists (${tmp_dir})"
254 | exit 1
255 | fi
256 | mkdir ${tmp_dir}
257 |
258 | if [ "${MY_LDAP_ROOTDN}" ]; then
259 | # shellcheck disable=SC2153
260 | ldap_param="${ldap_param} -D ${MY_LDAP_ROOTDN} -w ${MY_LDAP_ROOTPW}"
261 | fi
262 |
263 | ## Performs global search
264 | ${MY_LDAP_SEARCHBIN} "${ldap_param}" -s ${MY_LDAP_SEARCHSCOPE} \
265 | -b "${MY_LDAP_SEARCHBASE}" "${MY_LDAP_SEARCHFILTER}" \
266 | "dn" | grep -iE '^dn:' > ${result_file}
267 |
268 | ## Loops on results
269 | while read -r dnStr
270 | do
271 | # Do not use blank lines
272 | if [ ! "${dnStr}" ]; then
273 | continue
274 | fi
275 |
276 | # Process ldap search
277 | dn=$(echo "${dnStr}" | cut -d : -f 2)
278 |
279 | # Increment users counter
280 | nb_users=$(("${nb_users}" + 1))
281 |
282 | ${MY_LDAP_SEARCHBIN} "${ldap_param}" -s base -b "${dn}" \
283 | ${MY_LDAP_NAME_ATTR} ${MY_LDAP_LOGIN_ATTR} ${MY_LDAP_MAIL_ATTR} pwdChangedTime pwdPolicySubentry \
284 | > ${buffer_file}
285 |
286 | login=$(grep -w "${MY_LDAP_LOGIN_ATTR}:" ${buffer_file} | cut -d : -f 2 \
287 | | sed "s/^ *//;s/ *$//")
288 | name=$(grep -w "${MY_LDAP_NAME_ATTR}:" ${buffer_file} | cut -d : -f 2\
289 | | sed "s/^ *//;s/ *$//")
290 | mail=$(grep -w "${MY_LDAP_MAIL_ATTR}:" ${buffer_file} | cut -d : -f 2 \
291 | | sed "s/^ *//;s/ *$//")
292 | pwdChangedTime=$(grep -w "pwdChangedTime:" ${buffer_file} \
293 | | cut -d : -f 2 | cut -c 1-15 | sed "s/^ *//;s/ *$//")
294 | pwdPolicySubentry=$(grep -w "pwdPolicySubentry:" ${buffer_file} \
295 | | cut -d : -f 2 | sed "s/^ *//;s/ *$//")
296 |
297 | # Go to next entry if no pwdChangedTime
298 | if [ ! "${pwdChangedTime}" ]; then
299 | echo "${MY_LOG_HEADER} No password change date for ${login}" >&2
300 | continue
301 | fi
302 |
303 | # Go to next entry if no pwdPolicySubEntry and no default policy
304 | if [ ! "${pwdPolicySubentry}" ] && [ ! "${MY_LDAP_DEFAULTPWDPOLICYDN}" ]; then
305 | echo "${MY_LOG_HEADER} No password policy for ${login}" >&2
306 | continue
307 | fi
308 |
309 | # Retrieves user policy pwdMaxAge and pwdExpireWarning attributes
310 | ldap_search="${MY_LDAP_SEARCHBIN} ${ldap_param} -s base"
311 | if [ "${pwdPolicySubentry}" ]; then
312 | ldap_search="${ldap_search} -b ${pwdPolicySubentry}"
313 | else
314 | ldap_search="${ldap_search} -b ${MY_LDAP_DEFAULTPWDPOLICYDN}"
315 | fi
316 |
317 | ldap_search="$ldap_search pwdMaxAge pwdExpireWarning pwdMinLength pwdInHistory"
318 | pwdMaxAge=$(${ldap_search} | grep -w "pwdMaxAge:" | cut -d : -f 2 \
319 | | sed "s/^ *//;s/ *$//")
320 | pwdExpireWarning=$(${ldap_search} | grep -w "pwdExpireWarning:" | cut -d : -f 2 \
321 | | sed "s/^ *//;s/ *$//")
322 | pwdMinLength=$(${ldap_search} | grep -w "pwdMinLength:" | cut -d : -f 2 \
323 | | sed "s/^ *//;s/ *$//")
324 | pwdInHistory=$(${ldap_search} | grep -w "pwdInHistory:" | cut -d : -f 2 \
325 | | sed "s/^ *//;s/ *$//")
326 |
327 | # Go to next user if no pwdMaxAge (no expiration)
328 | if [ ! "${pwdMaxAge}" ]; then
329 | echo "${MY_LOG_HEADER} No password expiration configured for ${login}" >&2
330 | continue
331 | fi
332 |
333 | # Replace MAIL_DELAY by pwdExpireWarning if exists
334 | MY_MAIL_DELAY=${MY_MAIL_DELAY:=$pwdExpireWarning}
335 |
336 | # Retrieves time difference between today and last change.
337 | if [ "${pwdChangedTime}" ]; then
338 | s=$(echo "${pwdChangedTime}" | cut -c 13-14)
339 | m=$(echo "${pwdChangedTime}" | cut -c 11-12)
340 | h=$(echo "${pwdChangedTime}" | cut -c 9-10)
341 | d=$(echo "${pwdChangedTime}" | cut -c 7-8)
342 | M=$(echo "${pwdChangedTime}" | cut -c 5-6)
343 | y=$(echo "${pwdChangedTime}" | cut -c 1-4)
344 | currentTime=$(getTimeInSeconds)
345 | pwdChangedTime=$(getTimeInSeconds "$y $M $d $h $m $s")
346 | diffTime=$(("${currentTime}" - "${pwdChangedTime}"))
347 | fi
348 |
349 | # Go to next user if password already expired
350 | expireTime=$(("${pwdChangedTime}" + "${pwdMaxAge}"))
351 | if [ "${currentTime}" -gt "${expireTime}" ]; then
352 | nb_expired_users=$(("${nb_expired_users}" + 1))
353 | echo "${MY_LOG_HEADER} Password expired for ${login}" >&2
354 | continue
355 | fi
356 |
357 | expireTimeTZ=$(date -d @"$expireTime" "+%A %d %B %Y %T")
358 |
359 | expireTimeMail=$(date -d @"$expireTime" "+%s")
360 |
361 | now=$(date +%s)
362 |
363 | expireDays=$(( (expireTimeMail - now) / (60*60*24) ))
364 |
365 | # Print debug information on STDERR when there is no mail
366 | if [ -z "${mail}" ];then
367 | echo "${MY_LOG_HEADER} No mail attribute (${MY_LDAP_MAIL_ATTR}) for user ${login}" >&2
368 | fi
369 |
370 | # ALL LDAP attributes should be there, else continue to next user
371 | if [ "${mail}" ] && [ "${name}" ] \
372 | && [ "${login}" ] && [ "${diffTime}" ] && [ "${pwdMaxAge}" ]
373 | then
374 | # Ajusts time with delay
375 | diffTime=$(("${diffTime}" + "${MY_MAIL_DELAY}"))
376 | if [ "${diffTime}" -gt "${pwdMaxAge}" ]; then
377 | logmsg="${MY_MAIL_BODY}"
378 | logmsg=$(echo "${logmsg}" | sed "s/%name/${name}/; \
379 | s/%login/${login}/; s/%expireTimeTZ/${expireTimeTZ}/; s/%pwdMinLength/${pwdMinLength}/; s/%pwdInHistory/${pwdInHistory}/; \
380 | s/%expireDays/${expireDays}/")
381 |
382 | # Sending mail...
383 | echo "${logmsg}" | ${MY_MAIL_BIN} "${MY_MAIL_FROM}" -s "${MY_MAIL_SUBJECT}" "${mail}" >&2
384 |
385 | # Print debug information on STDERR
386 | echo "${MY_LOG_HEADER} Mail sent to user ${login} (${mail})" >&2
387 |
388 | # Increment warning counter
389 | nb_warning_users=$(("${nb_warning_users}" + 1))
390 | fi
391 | fi
392 |
393 | done < ${result_file}
394 |
395 | # Print statistics on STDOUT
396 | echo "${MY_LOG_HEADER} --- Statistics ---"
397 | echo "${MY_LOG_HEADER} Users checked: ${nb_users}"
398 | echo "${MY_LOG_HEADER} Account expired: ${nb_expired_users}"
399 | echo "${MY_LOG_HEADER} Account in warning: ${nb_warning_users}"
400 |
401 | # Delete temporary files
402 | rm -rf ${tmp_dir}
403 |
404 | # Exit
405 | exit 0
406 |
--------------------------------------------------------------------------------
/cleanLdapBrokenAliases.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #========================================================================
4 | # Script for OpenLDAP 2.3.x minimum
5 | #
6 | # This script will attempt to remove any broken aliases into an
7 | # OpenLDAP directory.
8 | #
9 | # Take some command lined parameters :
10 | # - Option "-b " specified the base where to search
11 | # for broken aliases. No default, it must be specified.
12 | #
13 | # Tested on :
14 | # - GNU/Linux platform ;
15 | #
16 | # Dependences into the PATH :
17 | # - awk
18 | # - sed
19 | # - perl
20 | # - openldap utils (ldapsearch, ldapdelete)
21 | #
22 | # Copyright (C) 2009 Thomas CHEMINEAU
23 | # Copyright (C) 2009 LINAGORA
24 | #
25 | # This program is free software; you can redistribute it and/or
26 | # modify it under the terms of the GNU General Public License
27 | # as published by the Free Software Foundation; either version 2
28 | # of the License, or (at your option) any later version.
29 | #
30 | # This program is distributed in the hope that it will be useful,
31 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
32 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 | # GNU General Public License for more details.
34 | #
35 | # GPL License: http://www.gnu.org/licenses/gpl.txt
36 | #
37 | #========================================================================
38 | # More contributions on http://www.linagora.org
39 | #========================================================================
40 |
41 | #========================================================================
42 | # Changelog
43 | #========================================================================
44 | # Version 0.1 (2009):
45 | # - First version
46 | # Author: Thomas CHEMINEAU (LINAGORA)
47 | #========================================================================
48 |
49 | #------------------------------------------------------------------------
50 | # PARAMETERS
51 | #------------------------------------------------------------------------
52 |
53 | #
54 | # LDAP host URI
55 | # eg: ldap://localhost:389
56 | #
57 | MY_LDAP_URI="ldap://localhost:389"
58 |
59 | #
60 | # LDAP bind DN which have write rights
61 | # eg: cn=Manager,dc=example,dc=com
62 | #
63 | MY_LDAP_BINDDN="cn=Manager,dc=example,dc=com"
64 |
65 | #
66 | # LDAP bind password
67 | #
68 | MY_LDAP_BINDPW="secret"
69 |
70 | #
71 | # Log header format
72 | # Could include unix commands
73 | #
74 | MY_LOG_HEADER="$(date +\"%b %e %T\") $(hostname) $(basename "$0")[$$]:"
75 |
76 | #------------------------------------------------------------------------
77 | # INIT
78 | #------------------------------------------------------------------------
79 |
80 | # Some others parameters. It is recommended to not change them.
81 |
82 | MY_LDAP_AUTHTOKEN="-D ${MY_LDAP_BINDDN} -w ${MY_LDAP_BINDPW} -H ${MY_LDAP_URI}"
83 | MY_LDAP_SEARCHBASE=""
84 | MY_SCRIPTNAME="$0"
85 |
86 | #------------------------------------------------------------------------
87 | # FUNCTIONS
88 | #------------------------------------------------------------------------
89 |
90 | #
91 | # Delete all broken aliases into a specific tree.
92 | #
93 | delete_broken_aliases() {
94 | # $1: search base dn
95 | for alias_dn in $(search_dn "$1" "sub" "(objectclass=alias)")
96 | do
97 | object_dn=$(search_aliasedObjectName "${alias_dn}")
98 | if [ "$(test_dn "${object_dn}")" -ne 0 ] ; then
99 | if [ "$(delete_dn "${alias_dn}")" -eq 0 ] ; then
100 | print_trace "removing broken alias ${alias_dn} [OK]"
101 | else
102 | print_trace "removing broken alias ${alias_dn} [FAILED]"
103 | fi
104 | fi
105 | done
106 | }
107 |
108 | #
109 | # Delete an entry identified by a DN.
110 | #
111 | delete_dn() {
112 | # $1: entry dn
113 | ldapdelete "${MY_LDAP_AUTHTOKEN}" "$1" > /dev/null 2>&1
114 | echo $?
115 | }
116 |
117 | #
118 | # Print information.
119 | #
120 | print_trace() {
121 | # $1: a message
122 | echo "${MY_LOG_HEADER} $1"
123 | }
124 |
125 | #
126 | # Print usage.
127 | #
128 | print_usage() {
129 | echo "Usage : ${MY_SCRIPTNAME}]" 1>&2
130 | echo " -b " 1>&2
131 | }
132 |
133 | #
134 | # Get the aliasedObjectName value of an LDAP alias.
135 | #
136 | search_aliasedObjectName() {
137 | # $1: alias dn
138 | ldapsearch -LLL "${MY_LDAP_AUTHTOKEN}" -b "$1" -s base aliasedObjectName \
139 | | perl -p0e 's/\n //g' | grep -i "aliasedObjectName" | awk -F': ' '{print $2}'
140 | }
141 |
142 | #
143 | # Do a LDAP search and return all DN found.
144 | #
145 | search_dn() {
146 | # $1: base dn
147 | # $2: scope
148 | # $3: filter
149 | ldapsearch -LLL "${MY_LDAP_AUTHTOKEN}" -b "$1" -S "" -s "$2" "$3" dn \
150 | | perl -p0e 's/\n //g' | awk -F': ' '{print $2}'
151 | }
152 |
153 | #
154 | # Test if a entry exists.
155 | #
156 | test_dn() {
157 | # $1: entry dn
158 | ldapsearch -LLL "${MY_LDAP_AUTHTOKEN}" -b "$1" -s base dn > /dev/null 2>&1
159 | echo $?
160 | }
161 |
162 | #------------------------------------------------------------------------
163 | # MAIN
164 | #------------------------------------------------------------------------
165 |
166 | if [ "$#" -ne "2" ]; then
167 | echo "Error: wrong number of arguments"
168 | print_usage
169 | exit 1
170 | fi
171 |
172 | while [ "$1" != "" ]; do
173 | case "$1" in
174 | -b)
175 | shift
176 | MY_LDAP_SEARCHBASE="$1"
177 | shift
178 | ;;
179 | *)
180 | print_usage
181 | exit 1
182 | ;;
183 | esac
184 | done
185 |
186 | delete_broken_aliases "${MY_LDAP_SEARCHBASE}"
187 |
188 | exit 0
189 |
190 |
--------------------------------------------------------------------------------
/convertgroup.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | #====================================================================
4 | # Script to convert groups for server migration
5 | #
6 | # Copyright (C) 2011 Clement OUDOT
7 | # Copyright (C) 2011 LTB-project.org
8 | #
9 | # This program is free software; you can redistribute it and/or
10 | # modify it under the terms of the GNU General Public License
11 | # as published by the Free Software Foundation; either version 2
12 | # of the License, or (at your option) any later version.
13 | #
14 | # This program is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # GPL License: http://www.gnu.org/licenses/gpl.txt
20 | #
21 | #====================================================================
22 |
23 | #====================================================================
24 | # Configuration
25 | #====================================================================
26 | # Source group object class
27 | my $srcGroupOC = "nsRoleDefinition";
28 |
29 | # Source group attribute name
30 | my $srcGroupAT = "cn";
31 |
32 | # Source member object class
33 | my $srcMemberOC = "inetOrgPerson";
34 |
35 | # Source reverse membership attribute
36 | my $srcMemberAT = "nsRoleDN";
37 |
38 | # Destination group objectClass
39 | my $dstGroupOC = "groupOfUniqueNames";
40 |
41 | # Destination group attribute name (RDN)
42 | my $dstGroupATName = "cn";
43 |
44 | # Destination group branch
45 | my $dstGroupBranch = "ou=groups,dc=example,dc=com";
46 |
47 | # Destination group attribute member
48 | my $dstGroupATMember = "uniqueMember";
49 |
50 | # Destination group attribute member value
51 | my $dstGroupATMemberValue = "dn";
52 |
53 | # Default member value for empty groups
54 | my $dstGroupATMemberDefaultValue = "cn=empty";
55 |
56 | # Branches to exclude
57 | my $branch_exclude = [
58 | qw(
59 | cn=config
60 | cn=monitor
61 | )
62 | ];
63 |
64 | # LDIF Options
65 |
66 | # Sort attributes
67 | my $ldif_sort = 0;
68 |
69 | # DN encoding
70 | # none, base64 or canonical
71 | my $ldif_encode = "base64";
72 |
73 | # Convert attribute names in lowercase
74 | my $ldif_lowercase = 0;
75 |
76 | # Columns wrapping
77 | my $ldif_wrap = 78;
78 |
79 | #====================================================================
80 | # Modules
81 | #====================================================================
82 | use Net::LDAP::LDIF;
83 | use Net::LDAP::Util qw/ldap_explode_dn/;
84 | use strict;
85 | use utf8;
86 |
87 | #====================================================================
88 | # Get command line arguments
89 | #====================================================================
90 | # Input file
91 | my $file = shift @ARGV;
92 |
93 | unless ($file) {
94 | print STDERR "Usage: $0 file.ldif\n";
95 | exit 1;
96 | }
97 |
98 | my $inldif = Net::LDAP::LDIF->new($file);
99 |
100 | # Output file
101 | my $outldif = Net::LDAP::LDIF->new(
102 | "$file.groupconvert", "w",
103 | sort => $ldif_sort,
104 | encode => $ldif_encode,
105 | lowercase => $ldif_lowercase,
106 | wrap => $ldif_wrap,
107 | );
108 |
109 | my $groupMembership;
110 | my $groups;
111 |
112 | # Parse source LDIF
113 | while ( not $inldif->eof() ) {
114 |
115 | my $entry = $inldif->read_entry();
116 |
117 | next unless $entry;
118 |
119 | if ( $inldif->error() ) {
120 | print STDERR "Error msg: ", $inldif->error(), "\n";
121 | print STDERR "Error lines:\n", $inldif->error_lines(), "\n";
122 | }
123 |
124 | # Test 1: excluded branch
125 | my $dn = $entry->dn();
126 | my $exclude_entry = 0;
127 |
128 | foreach my $branch (@$branch_exclude) {
129 | if ( $dn =~ /$branch$/i ) {
130 | print STDERR "DN $dn exlcuded (belongs to branch $branch)\n";
131 | $exclude_entry = 1;
132 | last;
133 | }
134 | }
135 |
136 | next if $exclude_entry;
137 |
138 | # Check objectClass
139 | next unless ( $entry->exists('objectClass') );
140 |
141 | my $ocvalues = $entry->get_value( 'objectClass', asref => 1 );
142 |
143 | # Reverse membership
144 | if ( grep ( /^$srcMemberOC$/i, @$ocvalues ) ) {
145 |
146 | # Check reverse membership attribute
147 | next unless ( $entry->exists($srcMemberAT) );
148 |
149 | # Add user to group
150 | my $userRef =
151 | ( $dstGroupATMemberValue eq "dn" )
152 | ? $entry->dn()
153 | : $entry->get_value($dstGroupATMemberValue);
154 |
155 | foreach my $group ( $entry->get_value($srcMemberAT) ) {
156 |
157 | # This value is a DN, extract the RDN
158 | my $dnval = ldap_explode_dn( $group, casefold => 'lower' );
159 | my $groupName = $dnval->[0]->{$srcGroupAT};
160 | push @{ $groupMembership->{$groupName} }, $userRef;
161 | }
162 |
163 | }
164 | elsif ( grep ( /^$srcGroupOC$/i, @$ocvalues ) ) {
165 | next unless ( $entry->exists($srcGroupAT) );
166 | push @$groups, scalar $entry->get_value($srcGroupAT);
167 | }
168 | }
169 |
170 | foreach my $newGroup (@$groups) {
171 |
172 | # Build new group
173 | my $newGroupDN = $dstGroupATName . "=" . $newGroup . "," . $dstGroupBranch;
174 | my $newGroupEntry = Net::LDAP::Entry->new();
175 | $newGroupEntry->dn($newGroupDN);
176 | $newGroupEntry->add( 'objectClass' => $dstGroupOC );
177 |
178 | # Find members
179 | if ( exists $groupMembership->{$newGroup} ) {
180 | $newGroupEntry->add(
181 | $dstGroupATMember => $groupMembership->{$newGroup} );
182 | }
183 | else {
184 | $newGroupEntry->add(
185 | $dstGroupATMember => $dstGroupATMemberDefaultValue );
186 | }
187 |
188 | # Write entry
189 | $outldif->write_entry($newGroupEntry);
190 | }
191 |
192 | #====================================================================
193 | # Exit
194 | #====================================================================
195 | $inldif->done();
196 | $outldif->done();
197 |
198 | exit 0;
199 |
--------------------------------------------------------------------------------
/convertldif.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | #====================================================================
4 | # Script to convert LDIF into LDIF for server migration
5 | #
6 | # Copyright (C) 2011 Clement OUDOT
7 | # Copyright (C) 2011 LTB-project.org
8 | #
9 | # This program is free software; you can redistribute it and/or
10 | # modify it under the terms of the GNU General Public License
11 | # as published by the Free Software Foundation; either version 2
12 | # of the License, or (at your option) any later version.
13 | #
14 | # This program is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # GPL License: http://www.gnu.org/licenses/gpl.txt
20 | #
21 | #====================================================================
22 |
23 | #====================================================================
24 | # Configuration
25 | #====================================================================
26 | # Attributes to exclude
27 | my $attr_exclude = [
28 | qw(
29 | createTimeStamp
30 | modifyTimeStamp
31 | nsRoleDN
32 | numSubordinates
33 | )
34 | ];
35 |
36 | # Values to exclude
37 | my $val_exclude =
38 | { 'objectClass' => [qw(exampleObjectClass otherObjectClass)], };
39 |
40 | # Values to map
41 | my $val_map =
42 | { 'creatorsName' => { 'cn=dirman' => 'cn=manager,dc=example,dc=com' }, };
43 |
44 | # Attributes to map
45 | my $map = {
46 | 'c' => 'co',
47 | 'co' => 'c',
48 | };
49 |
50 | # Branches to exclude
51 | my $branch_exclude = [
52 | qw(
53 | cn=config
54 | cn=monitor
55 | )
56 | ];
57 |
58 | # LDIF Options
59 |
60 | # Sort attributes
61 | my $ldif_sort = 0;
62 |
63 | # DN encoding
64 | # none, base64 or canonical
65 | my $ldif_encode = "base64";
66 |
67 | # Convert attribute names in lowercase
68 | my $ldif_lowercase = 0;
69 |
70 | # Columns wrapping
71 | my $ldif_wrap = 78;
72 |
73 | # Remove empty values
74 | my $remove_empty_values = 1;
75 |
76 | # Force UTF-8 conversion
77 | my $force_utf8_conversion = 1;
78 |
79 | #====================================================================
80 | # Modules
81 | #====================================================================
82 | use Net::LDAP::LDIF;
83 | use strict;
84 | use utf8;
85 |
86 |
87 |
88 | #====================================================================
89 | # Functions
90 | #====================================================================
91 |
92 | # unwrap the LDIF file
93 | sub unwrapLdifFile {
94 | my $f = shift;
95 | open(FH, '<', $f) or die $!;
96 | my @content;
97 | my $i = 0;
98 |
99 | while(){
100 | if( $_ =~ /^ / )
101 | {
102 | my $res = $_;
103 | $res =~ s/^[ ]+//g; # remove starting space
104 | $content[($i-1)] =~ s/\n//; # remove trailing \n
105 | $content[($i-1)] .= $res; # concat values
106 | }
107 | else
108 | {
109 | $content[$i] = $_;
110 | $i++;
111 | }
112 | }
113 | close(FH);
114 | open(FH, '>', $f) or die $!;
115 | foreach (@content)
116 | {
117 | next if $_ =~ /^control:/; # remove the control attribute
118 | print FH $_;
119 | }
120 | close(FH);
121 | }
122 |
123 |
124 |
125 |
126 | #====================================================================
127 | # Get command line arguments
128 | #====================================================================
129 | # Input file
130 | my $file = shift @ARGV;
131 |
132 | unless ($file) {
133 | print STDERR "Usage: $0 file.ldif\n";
134 | exit 1;
135 | }
136 |
137 | # unwrap the ldif file (attribute values are no more splitted in multiple lines)
138 | # + remove control attribute (sometime returned by IBM Tivoli Directory Server)
139 | #&unwrapLdifFile($file);
140 |
141 | my $inldif = Net::LDAP::LDIF->new($file);
142 |
143 | # Output file
144 | my $outldif = Net::LDAP::LDIF->new(
145 | "$file.conv.ldif", "w",
146 | sort => $ldif_sort,
147 | encode => $ldif_encode,
148 | lowercase => $ldif_lowercase,
149 | wrap => $ldif_wrap,
150 | );
151 |
152 | # Parse LDIF
153 | while ( not $inldif->eof() ) {
154 |
155 | my $entry = $inldif->read_entry();
156 |
157 | next unless $entry;
158 |
159 | if ( $inldif->error() ) {
160 | print STDERR "Error msg: ", $inldif->error(), "\n";
161 | print STDERR "Error lines:\n", $inldif->error_lines(), "\n";
162 | }
163 |
164 | # Test 1: excluded branch
165 | my $dn = $entry->dn();
166 | my $exclude_entry = 0;
167 |
168 | foreach my $branch (@$branch_exclude) {
169 | if ( $dn =~ /$branch$/i ) {
170 | print STDERR "DN $dn exlcuded (belongs to branch $branch)\n";
171 | $exclude_entry = 1;
172 | last;
173 | }
174 | }
175 |
176 | next if $exclude_entry;
177 |
178 | # Create a new entry
179 | my $new_entry = Net::LDAP::Entry->new();
180 | $new_entry->dn($dn);
181 |
182 | foreach my $attr ( $entry->attributes ) {
183 |
184 | my $exclude_attr = 0;
185 |
186 | # Test 2: excluded attribute
187 | if ( grep ( /^$attr$/i, @$attr_exclude ) ) {
188 | print STDERR "Entry $dn: attribute $attr excluded\n";
189 | next;
190 | }
191 |
192 | # Test 3: mapped values
193 | foreach my $key_val_map ( keys %$val_map ) {
194 |
195 | if ( $attr =~ /^$key_val_map$/i ) {
196 | foreach
197 | my $key_val_map_attr ( keys %{ $val_map->{$key_val_map} } )
198 | {
199 | if (
200 | grep /^$key_val_map_attr$/i,
201 | $entry->get_value($key_val_map)
202 | )
203 | {
204 | print STDERR
205 | "Entry $dn: Value substitution for attribute $key_val_map\n";
206 | $entry->delete( $key_val_map => [$key_val_map_attr] );
207 | $entry->add( $key_val_map =>
208 | $val_map->{$key_val_map}->{$key_val_map_attr} );
209 | }
210 | }
211 | }
212 | }
213 |
214 | # Exclude empty attribute
215 | if ($remove_empty_values) {
216 | my $old_values = $entry->get_value( $attr, asref => 1 );
217 | my $new_values = [];
218 | my $need_remove = 0;
219 | foreach my $old_value (@$old_values) {
220 | unless ( grep( /^\s*$/, $old_value ) ) {
221 | push @$new_values, $old_value;
222 | }
223 | else {
224 | print STDERR
225 | "Entry $dn: empty value for attribute $attr excluded\n";
226 | $need_remove = 1;
227 | }
228 | }
229 | if ($need_remove) {
230 | if ( defined $new_values->[0] ) {
231 | $entry->replace( $attr => $new_values );
232 | }
233 | else {
234 | $entry->delete( $attr => [] );
235 | }
236 | }
237 | }
238 |
239 | # Force UT8 encoding
240 | if ($force_utf8_conversion) {
241 | my $old_values = $entry->get_value( $attr, asref => 1 );
242 | my $new_values = [];
243 |
244 | require Encode;
245 |
246 | foreach my $old_value (@$old_values) {
247 | eval {
248 | my $safevalue = $old_value;
249 | Encode::from_to( $safevalue, "utf8", "iso-8859-1",
250 | Encode::FB_CROAK );
251 | };
252 | if ($@) {
253 | Encode::from_to( $old_value, "iso-8859-1",
254 | "utf8", Encode::FB_CROAK );
255 | print STDERR
256 | "Entry $dn: Force value utf8 conversion for attribute $attr\n";
257 | }
258 |
259 | push @$new_values, $old_value;
260 | }
261 |
262 | $entry->replace( $attr => $new_values );
263 | }
264 |
265 | # Test 4: excluded value
266 | foreach my $key_val_exclude ( keys %$val_exclude ) {
267 |
268 | if ( $attr =~ /^$key_val_exclude$/i ) {
269 | my $val = $val_exclude->{$key_val_exclude};
270 | my $old_values = $entry->get_value( $attr, asref => 1 );
271 | my $new_values = [];
272 |
273 | foreach my $old_value (@$old_values) {
274 | unless ( grep( /^$old_value$/i, @$val ) ) {
275 | push @$new_values, $old_value;
276 | }
277 | else {
278 | print STDERR
279 | "Entry $dn: value $old_value for attribute $attr excluded\n";
280 | }
281 | }
282 |
283 | $new_entry->add( $attr => $new_values );
284 | $exclude_attr = 1;
285 | }
286 |
287 | }
288 |
289 | next if $exclude_attr;
290 |
291 | # Test 5: mapped attribute
292 | foreach my $key_map ( keys %$map ) {
293 | if ( $attr =~ /^$key_map$/i ) {
294 | my $mapped_attr = $map->{$key_map};
295 |
296 | if ( ref $mapped_attr eq 'ARRAY' ) {
297 | my $vals = [];
298 | foreach my $ma ( @$mapped_attr ) {
299 |
300 | if ( $entry->exists($ma) ) {
301 |
302 | print STDERR
303 | "Entry $dn: Use $ma value for attribute $attr\n";
304 | my @v = @{$entry->get_value( $ma, asref => 1 )};
305 | foreach my $v ( @v ) {
306 | push(@$vals, $v) unless grep{ lc($_) eq lc($v) } @$vals;
307 | }
308 | }
309 |
310 | }
311 | $new_entry->add(
312 | $attr => $vals,
313 | );
314 | }
315 | else {
316 |
317 | if ( $entry->exists($mapped_attr) ) {
318 |
319 | print STDERR
320 | "Entry $dn: Use $mapped_attr value for attribute $attr\n";
321 | $new_entry->add(
322 | $attr => $entry->get_value( $mapped_attr, asref => 1 )
323 | );
324 | }
325 |
326 | }
327 | $exclude_attr = 1;
328 |
329 |
330 | }
331 | }
332 |
333 | next if $exclude_attr;
334 |
335 | # Here, we just keep the attribute
336 | $new_entry->add( $attr => $entry->get_value( $attr, asref => 1 ) )
337 | if defined $entry->get_value($attr);
338 |
339 | }
340 |
341 | # Map attributes that do not exists in entry
342 | foreach my $key_map ( keys %$map ) {
343 | my $mapped_attr = $map->{$key_map};
344 | if ( !$new_entry->exists($key_map) and $entry->exists($mapped_attr) ) {
345 |
346 | print STDERR
347 | "Entry $dn: Use $mapped_attr value for attribute $key_map\n";
348 | $new_entry->add(
349 | $key_map => $entry->get_value( $mapped_attr, asref => 1 ) );
350 |
351 | }
352 | }
353 |
354 | # Print new entry in LDIF
355 | $outldif->write_entry($new_entry);
356 | }
357 |
358 | #====================================================================
359 | # Exit
360 | #====================================================================
361 | $inldif->done();
362 | $outldif->done();
363 |
364 | exit 0;
365 |
--------------------------------------------------------------------------------
/file2ldif.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 |
3 | #====================================================================
4 | # Script to convert CSV or LDIF into LDIF
5 | #
6 | # Copyright (C) 2009 Clement OUDOT
7 | # Copyright (C) 2009 LTB-project.org
8 | #
9 | # This program is free software; you can redistribute it and/or
10 | # modify it under the terms of the GNU General Public License
11 | # as published by the Free Software Foundation; either version 2
12 | # of the License, or (at your option) any later version.
13 | #
14 | # This program is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # GPL License: http://www.gnu.org/licenses/gpl.txt
20 | #
21 | #====================================================================
22 |
23 | #====================================================================
24 | # Configuration
25 | #====================================================================
26 | # Containers begin and end characters for replacement
27 | my $beginc = "{";
28 | my $endc = "}";
29 |
30 | # Write changes and not full entries
31 | my $change = 1;
32 |
33 | # CSV delimiter (default is ",")
34 | my $csv_delimiter = ";";
35 |
36 | # CSV multi values delimiter
37 | my $csv_multivalues_delimiter = ",";
38 |
39 | # Strip CSV headers (jump to second line)
40 | my $csv_strip_headers = 1;
41 |
42 | # Mapping configuration
43 | my $map = {
44 | l_person => {
45 | dn => 'uid={(lc)uid},ou=users,dc=example,dc=com',
46 | objectClass =>
47 | [ 'top', 'person', 'organizationalPerson', 'inetOrgPerson' ],
48 | cn => '{cn}',
49 | sn => '{(uc)sn}',
50 | givenName => '{(ucfirstlc)givenname}',
51 | mail => '{(fmail)givenname}.{(fmail)sn}@example.com',
52 | userPassword => '{(random12)}',
53 | },
54 | l_group => {
55 | dn => 'cn={employeetype},ou=groups,dc=example,dc=com',
56 | objectClass => [ 'top', 'groupOfUniqueNames' ],
57 | uniqueMember => 'cn=empty',
58 | },
59 | l_group_add => {
60 | change_op => 'add',
61 | dn => 'cn={(lc)employeetype},ou=groups,dc=example,dc=com',
62 | uniqueMember => 'uid={(lc)uid},ou=users,dc=example,dc=com',
63 | },
64 | l_group_del => {
65 | change_op => 'delete',
66 | dn => 'cn={(lc)employeetype},ou=groups,dc=example,dc=com',
67 | uniqueMember => 'cn=empty',
68 | },
69 | c_person => {
70 | dn => 'uid={1},ou=users,dc=example,dc=com',
71 | objectClass =>
72 | [ 'top', 'person', 'organizationalPerson', 'inetOrgPerson' ],
73 | uid => '{1}',
74 | givenName => '{2}',
75 | sn => '{3}',
76 | cn => '{2} {3}',
77 | },
78 | c_group => {
79 | dn => 'cn={1},ou=groups,dc=example,dc=com',
80 | objectClass => [ 'top', 'groupOfUniqueNames' ],
81 | cn => '{1}',
82 | uniqueMember => 'uid={2},ou=users,dc=example,dc=com',
83 | },
84 | };
85 |
86 | #====================================================================
87 | # Modules
88 | #====================================================================
89 | use Net::LDAP::LDIF;
90 | use strict;
91 |
92 | #====================================================================
93 | # Get command line arguments
94 | #====================================================================
95 | # TODO: use getopts
96 | # Task
97 | my $task = shift @ARGV;
98 |
99 | # Input file
100 | my $file = shift @ARGV;
101 |
102 | # Changetype (add, modify, delete, modrdn)
103 | my $changetype = shift @ARGV;
104 | $changetype = "add" unless $changetype;
105 |
106 | # Output file
107 | my $outldif = Net::LDAP::LDIF->new( "$task.ldif", "w", change => $change );
108 | my $inldif;
109 |
110 | # Determine input type file (CSV or LDIF)
111 | my ($type) = ( $file =~ m/.*\.(\w+)/ );
112 |
113 | # If CSV, generate a tmp LDIF file
114 | if ( $type =~ m/csv/i ) {
115 |
116 | # Load Text::CSV module
117 | use Text::CSV;
118 |
119 | # Open CSV
120 | my $csv = Text::CSV->new(
121 | {
122 | sep_char => $csv_delimiter,
123 | binary => 1,
124 | }
125 | );
126 | open( CSV, "<", $file ) or die $!;
127 |
128 | # Parse CSV
129 | $inldif = Net::LDAP::LDIF->new( "$file.ldif", "w" );
130 |
131 | while () {
132 |
133 | # Strip headers
134 | next if ( ( $. == 1 ) and ( $csv_strip_headers == 1 ) );
135 |
136 | # Parse CSV line
137 | if ( $csv->parse($_) ) {
138 | my @columns = $csv->fields();
139 |
140 | # Write every column as attribute
141 | my $entry = Net::LDAP::Entry->new('o=fakedn');
142 | for my $i ( 0 .. $#columns ) {
143 | if ($csv_multivalues_delimiter) {
144 | my @values =
145 | split( /\Q$csv_multivalues_delimiter\E/, $columns[$i] );
146 | $entry->add( $i + 1 => \@values );
147 | }
148 | else {
149 | $entry->add( $i + 1 => $columns[$i] );
150 | }
151 | }
152 | $inldif->write_entry($entry);
153 | }
154 | else {
155 |
156 | # Error in parsing
157 | my $err = $csv->error_input;
158 | print STDERR "Failed to parse line: $err\n";
159 | }
160 |
161 | # Next line
162 | next;
163 | }
164 |
165 | close CSV;
166 | $inldif->done();
167 | $inldif = Net::LDAP::LDIF->new("$file.ldif");
168 | }
169 | else {
170 | $inldif = Net::LDAP::LDIF->new($file);
171 | }
172 |
173 | # Parse LDIF
174 | while ( not $inldif->eof() ) {
175 | my $entry = $inldif->read_entry();
176 | next unless $entry;
177 | if ( $inldif->error() ) {
178 | print STDERR "Error msg: ", $inldif->error(), "\n";
179 | print STDERR "Error lines:\n", $inldif->error_lines(), "\n";
180 | }
181 | else {
182 |
183 | # Replace strings in map
184 | my %localmap = %{ $map->{$task} };
185 |
186 | while ( my ( $k, $v ) = each %localmap ) {
187 | if ( ref($v) eq "ARRAY" ) {
188 | my @values = @$v;
189 | my @all_values;
190 | foreach (@values) {
191 | my $new_values = &generate_value( $entry, $_ );
192 | push @all_values, @$new_values if $new_values;
193 | }
194 | $v = \@all_values;
195 | }
196 | else {
197 | $v = &generate_value( $entry, $v );
198 | }
199 | $localmap{$k} = $v;
200 | }
201 |
202 | # DN
203 | my $dn = shift @{ $localmap{'dn'} };
204 | delete $localmap{dn};
205 |
206 | # Change operation
207 | my $change_op = shift @{ $localmap{change_op} };
208 | delete $localmap{change_op};
209 | $change_op = "add" unless $change_op;
210 |
211 | # Remove empty values
212 | while ( my ( $key, $value ) = each(%localmap) ) {
213 | delete $localmap{$key} if ( $value eq "" );
214 | delete $localmap{$key}
215 | if ( ref($value) eq "ARRAY" and @$value == 0 );
216 | }
217 |
218 | # Write entry
219 | my $outentry = Net::LDAP::Entry->new($dn);
220 | $outentry->changetype($changetype);
221 | $outentry->$change_op(%localmap);
222 | $outldif->write_entry($outentry);
223 | if ( my $ldif_error = $outldif->error() ) {
224 | print STDERR "Fail to add entry in LDIF: $ldif_error\n";
225 | }
226 | }
227 | }
228 |
229 | # Takes all values of wanted attribute
230 | # Removes the whitespaces from begin and end
231 | # Apply subroutine if any
232 | # @return ARRAYREF of values
233 | sub replace_value {
234 | my $entry = shift;
235 | my $key = shift;
236 | my $sub;
237 | my $attr;
238 | my $value;
239 | my @result;
240 |
241 | # Check subroutine presence
242 | if ( $key =~ m/\((.*)\)(.*)?/ ) {
243 | $sub = $1;
244 | $attr = $2;
245 |
246 | # If no attribute, apply only subroutine and return value
247 | unless ($attr) {
248 | return [ &apply_sub( undef, $sub ) ];
249 | }
250 | }
251 | else { $attr = $key }
252 |
253 | # Replace DN
254 | if ( $attr eq "dn" ) { $value = [ $entry->dn() ]; }
255 |
256 | # Get all attribute values
257 | else { $value = $entry->get_value( $attr, asref => 1 ); }
258 |
259 | # Empty value
260 | return "" unless defined $value;
261 |
262 | foreach my $val (@$value) {
263 |
264 | my $safe_val = $val;
265 |
266 | # Trim begin and end whitespaces
267 | $safe_val =~ s/^\s+|\s+$//g;
268 |
269 | # Apply subroutine if any
270 | $safe_val = &apply_sub( $val, $sub ) if ($sub);
271 |
272 | push @result, $safe_val;
273 | }
274 |
275 | return \@result;
276 | }
277 |
278 | # Create the new values
279 | # Call replace_value to get the mapping
280 | # @return ARRAYREF of new values
281 | sub generate_value {
282 | my $entry = shift;
283 | my $value = shift;
284 | my $key;
285 | my @result;
286 |
287 | if ( $value =~ m/$beginc([^$endc]*)?$endc/ ) {
288 | my @keys = ( $value =~ m/$beginc([^$endc]*)?$endc/g );
289 |
290 | # If multiple keys, use only first attribute value
291 | if ( $#keys > 0 ) {
292 | my $hValues = {};
293 | foreach $key (@keys) {
294 | my $new_values = &replace_value( $entry, $key );
295 | if ($new_values) {
296 | $hValues->{$key} = shift @$new_values;
297 |
298 | }
299 | }
300 | my $safe_value = $value;
301 | $safe_value =~ s/$beginc([^$endc]*)?$endc/$hValues->{$1}/ge;
302 | push @result, $safe_value;
303 | }
304 | else {
305 | # Else use all attributes values
306 | $key = shift @keys;
307 | my $new_values = &replace_value( $entry, $key );
308 | if ($new_values) {
309 | foreach my $new_value (@$new_values) {
310 | my $safe_value = $value;
311 | $safe_value =~ s/$beginc([^$endc]*)?$endc/$new_value/ge;
312 | push @result, $safe_value;
313 | }
314 | }
315 | }
316 | }
317 | else {
318 | push @result, $value;
319 | }
320 |
321 | return \@result;
322 | }
323 |
324 | # Apply subroutine
325 | sub apply_sub {
326 | my $value = shift;
327 | my $sub = shift;
328 |
329 | $value = lc($value) if ( $sub eq "lc" );
330 | $value = lcfirst($value) if ( $sub eq "lcfirst" );
331 | $value = uc($value) if ( $sub eq "uc" );
332 | $value = ucfirst($value) if ( $sub eq "ucfirst" );
333 | $value = ucfirst( lc($value) ) if ( $sub eq "ucfirstlc" );
334 | $value = &fmail($value) if ( $sub eq "fmail" );
335 |
336 | if ( $sub =~ /^random(\d+)/ ) {
337 | $value = &randomval($1);
338 | }
339 |
340 | return $value;
341 | }
342 |
343 | # Formate values for mail address
344 | sub fmail {
345 | my $value = shift;
346 |
347 | # Force lower case
348 | $value = lc($value);
349 |
350 | # Replace spaces by -
351 | $value =~ s/(\s+)/-/g;
352 |
353 | # Remove accents
354 | eval { require Text::Unaccent };
355 | if ($@) { return $value; }
356 | else {
357 | $value = unac_string( 'UTF-8', $value );
358 | return $value;
359 | }
360 | }
361 |
362 | # Generate random value
363 | sub randomval {
364 | my $length = shift;
365 | my $value;
366 |
367 | eval { require String::Random };
368 | if ($@) { return $value; }
369 | else {
370 | my $pass = String::Random->new;
371 | $value = $pass->randregex("[A-Za-z0-9]{$length}");
372 | }
373 |
374 | return $value;
375 | }
376 |
377 | #====================================================================
378 | # Exit
379 | #====================================================================
380 | $inldif->done();
381 | $outldif->done();
382 | unlink "$file.ldif" if ( $type =~ m/csv/i );
383 | exit 0;
384 |
--------------------------------------------------------------------------------
/ldap-stats.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env perl
2 | #
3 | # Program: Generate LDAP Statistics Reports
4 | #
5 | # Source code home: http://prefetch.net/code/ldap-stats.pl
6 | #
7 | # Author: Matty < matty91 @ gmail dot com >
8 | # Author: LDAP Tool Box project
9 | # Author: David Coutadeur
10 | #
11 | # Current Version: 10
12 | #
13 | # Revision History:
14 | #
15 | # Version 10
16 | # - add option --csv to output some stats in CSV (#38)
17 | # - reorder logfiles list by the date in their first line (#42)
18 | #
19 | # Version 9
20 | # - add option --sunds to parse Sun DS logs (#36)
21 | #
22 | # Version 8
23 | # - add option --log24 for old openldap log format (logs without qtime/etime) (#35)
24 | # - Display n longer requests in ldap-stats.pl (qtime/etime) (#31)
25 | #
26 | # Version 7
27 | # - add option (--log26) for new openldap 2.6 log format compatibility (#27)
28 | #
29 | # Version 6
30 | # - Choose syslog date format
31 | #
32 | # Version 5.2
33 | # Perl::Tidy and Perl::Critic -- Gavin Henry, Suretec Systems Ltd.
34 | #
35 | # Version 5.1
36 | # - Changed the location of the uc() statement -- Quanah Gibson-Mount
37 | #
38 | # Version 5.0
39 | # - Changed reporting structure to be dynamic -- Quanah Gibson-Mount
40 | # - Fixed a bug with name resolution -- Quanah Gibson-Mount
41 | # - Added the URL to the script -- Quanah Gibson-Mount
42 | #
43 | # Version 4.2
44 | # - Utilize strict mode -- Peter Schober
45 | #
46 | # Version 4.1
47 | # - Fixed a typo in the length() function -- Peter Schober
48 | #
49 | # Version 4.0
50 | # - Added "-d" option to print all days
51 | # - Fixed day sort order
52 | # - Added "-m" option to print all months
53 | # - Fixed month sort order
54 | # - Correct spelling. -- Dave Horsfall
55 | # - Align headings. -- Dave Horsfall
56 | # - Support ldapi:// connections ("LOCAL-SOCKET"). -- Dave Horsfall
57 | # - Only do lookup if numeric IP. -- Dave Horsfall
58 | #
59 | # Version 3.0 - 3.4
60 | # - Added ability to resolve IP addresses to hostnames with "-n" option
61 | # - Adjusted print() routines to limit lines to 80-characters -- Dave Horsfall
62 | # - Clean up unnecessary (..) in regexes -- Peter Marschall
63 | # - Split attributes found in searches (controlled by new option -s) -- Peter Marschall
64 | # - Added report to print which filters are used
65 | # - Added report to print explicit attributes requested -- Francis Swasey
66 | # - Fix usage: correct line break, all lines < 80 chars -- Peter Marschall
67 | # - Replace unnecessary printf() by print -- Peter Marschall
68 | # - Concatenate arguments into one call to print instead of multiple calls -- Peter Marschall
69 | # - Adapt underlining of some headers to length of logfile / date -- Peter Marschall
70 | # - Added additional checks to address missing entries during logfile rotation
71 | # - Fixed "uninitialized value in hash element" -- Todd Lyons
72 | # - Added additional comments to code
73 | # - Added report for operations by time of day
74 | # - Added report for operations per day
75 | # - Added report for operations per month
76 | # - Removed debug statements to speedup logfile processing
77 | # - Changed printf() format specifiers to match column definitions
78 | #
79 | # Version 2.0 - 2.2
80 | # - Adjusted the Search base comparison to catch ""
81 | # - Translate "" to RootDSE in the search base results
82 | # - Only print "Unindexed attribute" if unindexed attributes exist
83 | # - Normalize the bind DN and search base to avoid duplicates
84 | # - Fix typo with binddn array
85 | # - Improved filter for anonymous and authenticated binds -- Peter Marschall
86 | # - Logfiles are now passed as arguments to ldap-stats.pl
87 | # (e.g, ldap-stats.pl openldap1 openldap2 openldap3 old* ) -- Peter Marschall
88 | # - Cleaned up and combined filters for ADDs, MODs, DELs -- Peter Marschall
89 | # - Added support for CMPs & MODRDNs -- Peter Marschall
90 | # - Reduced number of regular expressions to one per filter -- Peter Marschall
91 | # - Removed head and tail program requirements, as dates are read on the fly from the
92 | # decoded logfile -- Peter Marschall
93 | # - Support for gzip and bzip2 compressed files -- Peter Marschall
94 | # - Optimized some expressions -- Peter Marschall
95 | # - Removed several Perl warnings, and added "-w" to default runtime options -- Peter Marschall
96 | # - Support for regular expressions in logfile names (e.g., ldap-stats.pl /var/log/openldap* ) -- Peter Marschall
97 | # - Changed default Perl interpreter to /usr/bin/perl
98 | # - Changed to OpenLDAP license
99 | #
100 | # Version 1.1 - 1.9
101 | # - Updated the bind, binddn, search, search base, and unindexed search regexs to
102 | # match a wider array of characters -- added by Peter Marschall
103 | # - Shortened several regular expressions by replacing "[0-9]" with "\d" -- added by Peter Marschall
104 | # - Fixed a divide by zero bug when logfiles contain 0 connections -- added by Dave Horsfall
105 | # - Removed unnecessary file open(s)
106 | # - Removed end of line ($) character from anonymous BIND regular expressions
107 | # - Added "-l" option to print lines as they are processed from a logfile
108 | # - Updated documentation
109 | # - Updated formatting of search dn report
110 | # - Updated formatting of search base report
111 | # - Added an additional report with the number of binds per DN
112 | # - Updated examples
113 | # - Added additional debug messages to connection setup
114 | # - Fixed documentation issues
115 | # - Added debugging flag (-d) to give detailed information on logfile processing
116 | # - Added "usage" subroutine to ease option maintenance
117 | # - Fixed a bug in the BIND calculations -- found and fixed by Quanah Gibson-Mount
118 | # - Fixed a bug in the MOD calculations -- found and fixed by Quanah Gibson-Mount
119 | # - Fixed a bug in the SRCH calculations -- found and fixed by Quanah Gibson-Mount
120 | # - Added a connection associative array to coorelate conn identifiers w/hosts -- Quanah Gibson-Mount
121 | # - Updated the usage message with information on "-c" option
122 | # - The "-f" option now accepts multiple logfiles
123 | # - Changed the headers to include information on all logfiles processed
124 | # - Added the day the report was run to the report headers
125 | #
126 | # Version 1.0
127 | # Original release
128 | #
129 | # Last Updated: 21-02-2022
130 | #
131 | # Purpose:
132 | # Produces numerous reports from OpenLDAP 2.1, 2.2, 2.3, 2.4, 2.5 and 2.6 logfiles.
133 | #
134 | # License:
135 | #
136 | # Redistribution and use in source and binary forms, with or without
137 | # modification, are permitted only as authorized by the OpenLDAP
138 | # Public License.
139 | #
140 | # A copy of this license is available in the file LICENSE in the
141 | # top-level directory of the distribution or, alternatively, at
142 | # .
143 | #
144 | # Installation:
145 | # 1. Enable a minimum of 'loglevel 256' in the slapd.conf configuration file.
146 | # 2. Copy the shell script to a suitable location.
147 | # 3. Refer to the usage section for options and examples.
148 | #
149 | # Usage:
150 | # Refer to the usage subroutine,
151 | #
152 | # Example:
153 | # Refer to http://prefetch.net/code/ldap-stats.pl.txt to see sample output
154 |
155 | use strict;
156 | use warnings;
157 | use List::Util qw(min max);
158 | use Date::Parse;
159 | use Getopt::Long;
160 | use Socket;
161 | use Carp;
162 | use 5.006; # As returned by Perl::MinimumVersion
163 |
164 | #######################
165 | ### usage subroutine
166 | ### Parameters: None
167 | #######################
168 | sub usage {
169 | print
170 | "Usage: ldap-stats.pl [ -s ] [ -c ] [ -l ] [ -h ] ...\n"
171 | . " -c Number of lines to display for each report [25]\n"
172 | . " -d Display all available days in the day of month report\n"
173 | . " -h Display a usage help screen\n"
174 | . " -l Print status message after processing lines [0]\n"
175 | . " -m Display all available months in the month of year report\n"
176 | . " -n Resolve IP addresses to hostnames\n"
177 | . " -o -o ... Operations to print in the reports [ALL]\n"
178 | . " Valid operations are: CONNECT, FAILURES, BIND, UNBIND,\n"
179 | . " SRCH, CMP, ADD, MOD, MODRDN, DEL\n"
180 | . " Predefined reports are: ALL, READ, WRITE\n"
181 | . " -s Split attributes found used in searches\n"
182 | . " -D Use RFC5424 date format\n"
183 | . " --log24 Use OpenLDAP 2.4 log format (no qtime/etime)\n"
184 | . " --log26 Use OpenLDAP 2.6 log format\n"
185 | . " --sunds Use Sun DS log format\n"
186 | . " --csv export operations stats to a csv file (overwriting it)\n";
187 | return;
188 | }
189 |
190 | ### Declare lexical variables
191 | my ( $logfile, $i, $counter, $help );
192 | my ( %unindexed, %search, @operations );
193 |
194 | ### Allow the number of entries displayed to be variable
195 | my $count = 25;
196 |
197 | ### Figure out if we need to print "Processing X lines"
198 | my $increment = 0;
199 |
200 | ## tell whether to split attributes in searches
201 | my $splitattrs = 0;
202 |
203 | # Tell whether to lookup names
204 | my $resolvename = 0;
205 |
206 | # Print all months
207 | my $printmonths = 0;
208 |
209 | # Print all days
210 | my $printdays = 0;
211 |
212 | # Use RFC5242 date format
213 | my $dateformat = 0;
214 |
215 | # Use OpenLDAP 2.4 log format
216 | my $log24 = 0;
217 |
218 | # Use OpenLDAP 2.6 log format
219 | my $log26 = 0;
220 |
221 | # Use SunDS log format
222 | my $sunds = 0;
223 |
224 | # output operations stats to a csv file
225 | my $csv = "";
226 |
227 | # Maximum number of greater qtimes to display
228 | my $max_qtimes = 10;
229 |
230 | # Maximum number of greater etimes to display
231 | my $max_etimes = 10;
232 |
233 | ###################################
234 | #### Get some options from the user
235 | ###################################
236 | #getopts("o:l:c:nhsmd", \%options);
237 |
238 | GetOptions(
239 | 'count|c=i' => \$count,
240 | 'days|d' => \$printdays,
241 | 'dateformat|D' => \$dateformat,
242 | 'help|h' => \$help,
243 | 'length|l=i' => \$increment,
244 | 'months|m' => \$printmonths,
245 | 'network|n' => \$resolvename,
246 | 'operations|o=s' => \@operations,
247 | 'split|s' => \$splitattrs,
248 | 'log24' => \$log24,
249 | 'log26' => \$log26,
250 | 'sunds' => \$sunds,
251 | 'csv=s' => \$csv,
252 | );
253 |
254 | ### print a nice usage message
255 | if ($help) {
256 | usage;
257 | exit 1;
258 | }
259 |
260 | ### Make sure there is at least one logfile
261 | if ( !@ARGV ) {
262 | usage;
263 | exit 1;
264 | }
265 |
266 | ############################
267 | ### Define various variables
268 | ############################
269 | my $date = localtime time;
270 |
271 | if ( !@operations ) {
272 | @operations = ('ALL');
273 | }
274 |
275 | my %stats = (
276 | TOTAL_CONNECT => 0,
277 | TOTAL_BIND => 0,
278 | TOTAL_UNBIND => 0,
279 | TOTAL_SRCH => 0,
280 | TOTAL_DEL => 0,
281 | TOTAL_ADD => 0,
282 | TOTAL_CMP => 0,
283 | TOTAL_MOD => 0,
284 | TOTAL_MODRDN => 0,
285 | TOTAL_UNINDEXED => 0,
286 | TOTAL_AUTHFAILURES => 0,
287 | );
288 |
289 | my %hours; # Hash to store the time of day (e.g., 21st of August)
290 | my %days; # Hash to store the days of each month (e.g., 21st)
291 | my %months; # Hash to store the day of the month (e.g., Dec)
292 | my %hosts; # Hash to store client IP addresses
293 | my %conns; # Hash to store connection identifiers
294 | my %binddns; # Hash to store bind DNs
295 | my %logarray; # Hash to store logfiles
296 | my %filters; # Hash to store search filters
297 | my %searchattributes; # Hash to store specific attributes that are requested
298 | my %operations; # Hash to store operations information
299 | my %qtimes; # Hash to store qtimes { conn,op => qtime,... }
300 | my %etimes; # Hash to store etimes { conn,op => etime,... }
301 | my %ops; # Hash to store operations { conn,op => operation,... }
302 | my $machine = ""; # Scalar to store the machine name
303 |
304 | $operations{CONNECT} = {
305 | DATA => 0,
306 | STRING => ' Connect',
307 | SPACING => ' --------',
308 | FIELD => '%8s',
309 | };
310 |
311 | $operations{FAILURES} = {
312 | DATA => 0,
313 | STRING => ' Failed',
314 | SPACING => ' ------',
315 | FIELD => '%6s',
316 | };
317 |
318 | $operations{BIND} = {
319 | DATA => 0,
320 | STRING => ' Bind',
321 | SPACING => ' -------',
322 | FIELD => '%7s',
323 | };
324 |
325 | $operations{UNBIND} = {
326 | DATA => 0,
327 | STRING => ' Unbind',
328 | SPACING => ' -------',
329 | FIELD => '%7s',
330 | };
331 |
332 | $operations{SRCH} = {
333 | DATA => 0,
334 | STRING => ' Search',
335 | SPACING => ' --------',
336 | FIELD => '%8s',
337 | };
338 |
339 | $operations{ADD} = {
340 | DATA => 0,
341 | STRING => ' Add',
342 | SPACING => ' -----',
343 | FIELD => '%5s',
344 | };
345 |
346 | $operations{CMP} = {
347 | DATA => 0,
348 | STRING => ' Cmp',
349 | SPACING => ' -----',
350 | FIELD => '%5s',
351 | };
352 |
353 | $operations{MOD} = {
354 | DATA => 0,
355 | STRING => ' Mod',
356 | SPACING => ' -----',
357 | FIELD => '%5s',
358 | };
359 |
360 | $operations{MODRDN} = {
361 | DATA => 0,
362 | STRING => ' ModRDN',
363 | SPACING => ' ------',
364 | FIELD => '%6s',
365 | };
366 |
367 | $operations{DEL} = {
368 | DATA => 0,
369 | STRING => ' Del',
370 | SPACING => ' ----',
371 | FIELD => '%4s',
372 | };
373 |
374 | my $dateregexp_full;
375 | my $dateregexp_split;
376 |
377 | # RFC 5424 format
378 | if ($dateformat) {
379 | $dateregexp_full = '(\d+-\d+-\d+T\d+:\d+:\d+\.\d+\+\d+:\d+)';
380 | $dateregexp_split = '\d+-(\d+)-(\d+)T(\d+):(\d+):(\d+)\.\d+\+\d+:\d+';
381 | }
382 |
383 | # standard OpenLDAP 2.4/2.5 log format
384 | else {
385 | $dateregexp_full = '(\w+\s+\d+\s+\d+:\d+:\d+)';
386 | $dateregexp_split = '(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)';
387 | }
388 |
389 | # standard 2.6 log format
390 | if ($log26) {
391 | $dateregexp_full = '([0-9a-h]{8}\.[0-9a-h]{8})';
392 | $dateregexp_split = '([0-9a-h]{8})\.([0-9a-h]{8})';
393 | }
394 |
395 | # Function extracting month, day and hour from given log line
396 | sub getTimeComponents {
397 | my $line = shift;
398 | my ( $month, $day, $hour ) = ( "undef", "undef", "undef" );
399 | if ($log26) {
400 | if ( $line =~ /^$dateregexp_split.*$/m ) {
401 |
402 | # compute time components
403 | my $ts = hex( "0x" . $1 ); # number of second since epoch
404 | my $tn = hex( "0x" . $2 ); # number of nanoseconds
405 | my $completedate = scalar localtime $ts;
406 | ( $month, $day, $hour ) =
407 | $completedate =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+):/m;
408 | }
409 | }
410 | else {
411 | if ( $line =~ /^$dateregexp_split.*$/m ) {
412 |
413 | # return direct matched time components
414 | ( $month, $day, $hour ) = ( $1, $2, $3 );
415 | }
416 | }
417 |
418 | return ( $month, $day, $hour );
419 | }
420 |
421 | # Function extracting full date from given log line
422 | sub getFullDate {
423 | my $line = shift;
424 | my ( $month, $day, $hour, $min, $sec );
425 | my $fulldate = "";
426 |
427 | if ($log26) {
428 | if ( $line =~ /^$dateregexp_split/m ) {
429 |
430 | # compute time components
431 | my $ts = hex( "0x" . $1 ); # number of second since epoch
432 | my $tn = hex( "0x" . $2 ); # number of nanoseconds
433 | my $completedate = scalar localtime $ts;
434 | ( $month, $day, $hour, $min, $sec ) =
435 | $completedate =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)/m;
436 |
437 | $fulldate = "$month $day $hour:$min:$sec." . $tn;
438 | }
439 | }
440 | else {
441 | if ( $line =~ /^$dateregexp_full/m ) {
442 | $fulldate = $1;
443 | }
444 | }
445 | return $fulldate;
446 | }
447 |
448 | # Function that store the operation lines with correct format
449 | sub storeOp {
450 | my $connop = shift;
451 | my $line = shift;
452 |
453 | if ( $ops{"$connop"} ) {
454 | $ops{"$1,$2"} .= " $line";
455 | }
456 | else {
457 | $ops{"$1,$2"} .= "$line";
458 | }
459 | }
460 |
461 | #####################################################################
462 | ### First pass: reorder logfiles list by the date of their first line
463 | #####################################################################
464 |
465 | for my $file (@ARGV) {
466 | $logfile = $file;
467 |
468 | ### find open filter to use
469 | my $openfilter = '<' . $logfile . q{};
470 |
471 | ### decode gzipped / bzip2-compressed files
472 | if ( $logfile =~ /\.bz2$/mx ) {
473 | $openfilter = q{bzip2 -dc "} . $logfile . q{"|}
474 | or carp "Problem decompressing!: $!\n";
475 | }
476 |
477 | if ( $logfile =~ /\.(gz|Z)$/mx ) {
478 | $openfilter = q{gzip -dc "} . $logfile . q{"|}
479 | or carp "Problem decompressing!: $!\n";
480 | }
481 |
482 | ### If the logfile isn't valid, move on to the next one
483 | if ( !open LOGFILE, $openfilter ) {
484 | print "ERROR: unable to open '$logfile': $!\n";
485 | next;
486 | }
487 |
488 | ### setup the arrray to hold the start/stop times
489 | $logarray{$logfile} = {
490 | SDATE => q{},
491 | EDATE => q{},
492 | STIMESTAMP => q{},
493 | };
494 |
495 | my $line = ;
496 | my $fulldate = getFullDate($line);
497 |
498 | # Get machine name in logfile if possible
499 | if( $line =~ / ([^ ]+) [^ ]+: conn=[0-9]+ op=[0-9]+ / )
500 | {
501 | $machine = $1;
502 | }
503 |
504 | ### Get start date converted as unix timestamp
505 | if ( $line =~ /^$dateregexp_full/mx ) {
506 | $logarray{$logfile}{STIMESTAMP} = str2time($fulldate);
507 | }
508 |
509 | close LOGFILE;
510 | }
511 |
512 | # sort logfiles list by the date of their first line
513 | my @orderedLogFiles = sort {
514 | $logarray{$a}{STIMESTAMP} <=> $logarray{$b}{STIMESTAMP}
515 | } keys(%logarray);
516 |
517 | ###################################################
518 | ### Open the logfile and process all of the entries
519 | ###################################################
520 | for my $file (@orderedLogFiles) {
521 | $logfile = $file;
522 | my $lines = 0;
523 |
524 | ### find open filter to use
525 | my $openfilter = '<' . $logfile . q{};
526 |
527 | ### decode gzipped / bzip2-compressed files
528 | if ( $logfile =~ /\.bz2$/mx ) {
529 | $openfilter = q{bzip2 -dc "} . $logfile . q{"|}
530 | or carp "Problem decompressing!: $!\n";
531 | }
532 |
533 | if ( $logfile =~ /\.(gz|Z)$/mx ) {
534 | $openfilter = q{gzip -dc "} . $logfile . q{"|}
535 | or carp "Problem decompressing!: $!\n";
536 | }
537 |
538 | ### If the logfile isn't valid, move on to the next one
539 | if ( !open LOGFILE, $openfilter ) {
540 | print "ERROR: unable to open '$logfile': $!\n";
541 | next;
542 | }
543 |
544 | ### Only print banner if requested
545 | if ( $increment > 0 ) {
546 | ### Print a banner and initialize the $counter variable
547 | print "\nProcessing file \"$logfile\"\n"
548 | . q{-} x ( 18 + length ${$logfile} ) . "\n";
549 | $counter = 0;
550 | $lines = $increment;
551 | }
552 |
553 | while ( my $line = ) {
554 |
555 | my $fulldate = getFullDate($line);
556 |
557 | ### check start and end dates
558 | if ( $line =~ /^$dateregexp_full/mx ) {
559 | if ( !$logarray{$logfile}{SDATE} ) {
560 | $logarray{$logfile}{SDATE} = $fulldate;
561 | }
562 | $logarray{$logfile}{EDATE} = $fulldate;
563 | }
564 |
565 | ### Check to see if we have processed $lines lines
566 | if ( ( $lines > 0 ) && ( $counter == $lines ) ) {
567 | print " Processed $lines lines in \"$logfile\"\n";
568 | $lines += $increment;
569 | }
570 |
571 | my ( $month, $day, $hour ) = getTimeComponents($line);
572 |
573 | ### Check for a new connection
574 | if (
575 | (
576 | $sunds
577 | and $line =~
578 | /conn=(\d+) op=[-]?\d+ msgId=[-]?\d+ - fd=\d+ slot=\d+ LDAP connection from/m
579 | )
580 | or $line =~
581 | /conn=(\d+) [ ] fd=\d+ [ ] (?:ACCEPT|connection) [ ] from/mx
582 | )
583 | {
584 | my $conn = $1;
585 | my $host;
586 |
587 | if ( $sunds and $line =~ /from (\d+\.\d+\.\d+\.\d+)/m ) {
588 | $host = $1;
589 | }
590 | elsif ( $line =~ /IP=(\d+\.\d+\.\d+\.\d+):/mx ) {
591 | $host = $1;
592 | }
593 | elsif ( $line =~ /PATH=(\S+)/mx ) {
594 | $host = 'LOCAL-SOCKET';
595 | }
596 | else {
597 | $host = 'UNKNOWN';
598 | }
599 |
600 | ### Create an array to store the list of hosts
601 | if ( !( defined $hosts{$host} ) ) {
602 | $hosts{$host} = {
603 | CONNECT => 1,
604 | AUTHFAILURES => 0,
605 | BIND => 0,
606 | UNBIND => 0,
607 | SRCH => 0,
608 | ADD => 0,
609 | CMP => 0,
610 | MOD => 0,
611 | MODRDN => 0,
612 | DEL => 0,
613 | };
614 | }
615 | else {
616 | ### Entry exists, increment the CONNECT value
617 | $hosts{$host}{CONNECT}++;
618 | }
619 |
620 | ### Create an array to store the hours
621 | if ( !( defined $hours{$hour} ) ) {
622 | $hours{$hour} = {
623 | CONNECT => 1,
624 | AUTHFAILURES => 0,
625 | BIND => 0,
626 | UNBIND => 0,
627 | SRCH => 0,
628 | ADD => 0,
629 | CMP => 0,
630 | MOD => 0,
631 | MODRDN => 0,
632 | DEL => 0,
633 | };
634 | }
635 | else {
636 | ### Entry exists, increment the CONNECT value
637 | $hours{$hour}{CONNECT}++;
638 | }
639 |
640 | ### Create an array to store the months
641 | if ( !( defined $months{$month} ) ) {
642 | $months{$month} = {
643 | CONNECT => 1,
644 | AUTHFAILURES => 0,
645 | BIND => 0,
646 | UNBIND => 0,
647 | SRCH => 0,
648 | ADD => 0,
649 | CMP => 0,
650 | MOD => 0,
651 | MODRDN => 0,
652 | DEL => 0,
653 | };
654 | }
655 | else {
656 | ### Entry exists, increment the CONNECT value
657 | $months{$month}{CONNECT}++;
658 | }
659 |
660 | ### Create an array to store the days
661 | if ( !( defined $days{$day} ) ) {
662 | $days{$day} = {
663 | CONNECT => 1,
664 | AUTHFAILURES => 0,
665 | BIND => 0,
666 | UNBIND => 0,
667 | SRCH => 0,
668 | ADD => 0,
669 | CMP => 0,
670 | MOD => 0,
671 | MODRDN => 0,
672 | DEL => 0,
673 | };
674 | }
675 | else {
676 | ### Entry exists, increment the CONNECT value
677 | $days{$day}{CONNECT}++;
678 | }
679 |
680 | ### Add the host to the connection table
681 | $conns{$conn} = $host;
682 |
683 | ### Increment the total number of connections
684 | $stats{TOTAL_CONNECT}++;
685 |
686 | ### Check for anonymous binds
687 | }
688 | elsif (
689 | (
690 | $sunds
691 | and $line =~
692 | /conn=(\d+) op=(\d+) msgId=\d+ - BIND dn="" method=128/m
693 | )
694 | or $line =~
695 | /conn=(\d+) [ ] op=(\d+) [ ] BIND [ ] dn="" [ ] method=128/mx
696 | )
697 | {
698 | my $conn = $1;
699 | storeOp( "$1,$2", "$line" );
700 |
701 | ### Increment the counters
702 | if ( defined $conns{$conn}
703 | && defined $hosts{ $conns{$conn} } )
704 | {
705 | $hosts{ $conns{$conn} }{BIND}++;
706 | $hours{$hour}{BIND}++;
707 | $days{$day}{BIND}++;
708 | $months{$month}{BIND}++;
709 | $stats{TOTAL_BIND}++;
710 | }
711 |
712 | ### Add the binddn to the binddns array
713 | $binddns{anonymous}++;
714 |
715 | ### Check for non-anonymous binds
716 | }
717 | elsif (
718 | (
719 | $sunds
720 | and $line =~
721 | /conn=(\d+) op=(\d+) msgId=\d+ - BIND dn="([^"]+)" method=/m
722 | )
723 | or $line =~
724 | /conn=(\d+) [ ] op=(\d+) [ ] BIND [ ] dn="([^"]+)" [ ] mech=/mx
725 | )
726 | {
727 | my $conn = $1;
728 | storeOp( "$1,$2", "$line" );
729 | my $binddn = lc $3;
730 |
731 | ### Increment the counters
732 | if ( defined $conns{$conn}
733 | && defined $hosts{ $conns{$conn} } )
734 | {
735 | $hosts{ $conns{$conn} }{BIND}++;
736 | $hours{$hour}{BIND}++;
737 | $days{$day}{BIND}++;
738 | $months{$month}{BIND}++;
739 | $stats{TOTAL_BIND}++;
740 | }
741 |
742 | ### Add the binddn to the binddns array
743 | $binddns{$binddn}++;
744 |
745 | ### Check the search base
746 | }
747 | elsif (
748 | (
749 | $sunds
750 | and $line =~
751 | /conn=(\d+) op=(\d+) msgId=\d+ - SRCH base="([^"]*?)" scope=\d filter="([^"]*?)" attrs=(.+)/m
752 | )
753 | or $line =~
754 | /\bconn=(\d+) [ ] op=(\d+) [ ] SRCH [ ] base="([^"]*?)" [ ] .*filter="([^"]*?)"/mx
755 | )
756 | {
757 | my $conn = $1;
758 | my $base = lc $3;
759 | storeOp( "$1,$2", "$line" );
760 | my $filter = $4;
761 |
762 | ### Stuff the search base into an array
763 | if ( defined $base ) {
764 | $search{$base}++;
765 | }
766 |
767 | if ( defined $filter ) {
768 | $filters{$filter}++;
769 | }
770 |
771 | if ($sunds) {
772 | my $attrs = lc $5;
773 | $attrs =~ s/^"//;
774 | $attrs =~ s/"$//;
775 | if ($splitattrs) {
776 | for my $attr ( split q{ }, $attrs ) {
777 | $searchattributes{$attr}++;
778 | }
779 | }
780 | else {
781 | $searchattributes{$attrs}++;
782 | }
783 | ### Increment the counters
784 | if ( defined $conns{$conn}
785 | && defined $hosts{ $conns{$conn} } )
786 | {
787 | $hosts{ $conns{$conn} }{SRCH}++;
788 | $hours{$hour}{SRCH}++;
789 | $days{$day}{SRCH}++;
790 | $months{$month}{SRCH}++;
791 | $stats{TOTAL_SRCH}++;
792 | }
793 | }
794 |
795 | ### Check for search attributes
796 | }
797 | elsif ( $line =~ /\bconn=(\d+) [ ] op=(\d+) [ ] SRCH [ ] attr=(.+)/mx )
798 | {
799 | storeOp( "$1,$2", "$line" );
800 | my $attrs = lc $3;
801 |
802 | if ($splitattrs) {
803 | for my $attr ( split q{ }, $attrs ) {
804 | $searchattributes{$attr}++;
805 | }
806 | }
807 | else {
808 | $searchattributes{$attrs}++;
809 | }
810 |
811 | ### Check for SEARCHES (log format 2.5 or 2.6)
812 | }
813 | elsif ( not $log24
814 | and $line =~
815 | /conn=(\d+) [ ] op=(\d+) [ ] SEARCH [ ] RESULT [ ] .*qtime=([\d.]+) .* etime=([\d.]+)/mx
816 | )
817 | {
818 | my $conn = $1;
819 | my $op = $2;
820 | storeOp( "$1,$2", "$line" );
821 | my $qtime = $3;
822 | $qtime =~ tr/\.//d; # remove . => microsecond format
823 | my $etime = $4;
824 | $etime =~ tr/\.//d; # remove . => microsecond format
825 | $qtimes{"$conn,$op"} = $qtime;
826 | $etimes{"$conn,$op"} = $etime;
827 |
828 | ### Increment the counters
829 | if ( defined $conns{$conn}
830 | && defined $hosts{ $conns{$conn} } )
831 | {
832 | $hosts{ $conns{$conn} }{SRCH}++;
833 | $hours{$hour}{SRCH}++;
834 | $days{$day}{SRCH}++;
835 | $months{$month}{SRCH}++;
836 | $stats{TOTAL_SRCH}++;
837 | }
838 |
839 | ### Check for SEARCHES (log format 2.4)
840 | }
841 | elsif ( $log24
842 | and $line =~ /conn=(\d+) [ ] op=(\d+) [ ] SEARCH [ ] RESULT/mx )
843 | {
844 | my $conn = $1;
845 | my $op = $2;
846 | storeOp( "$1,$2", "$line" );
847 |
848 | ### Increment the counters
849 | if ( defined $conns{$conn}
850 | && defined $hosts{ $conns{$conn} } )
851 | {
852 | $hosts{ $conns{$conn} }{SRCH}++;
853 | $hours{$hour}{SRCH}++;
854 | $days{$day}{SRCH}++;
855 | $months{$month}{SRCH}++;
856 | $stats{TOTAL_SRCH}++;
857 | }
858 |
859 | ### Check for unbinds
860 | }
861 | elsif (
862 | ( $sunds and $line =~ /conn=(\d+) op=(\d+) msgId=\d+ - UNBIND/m )
863 | or $line =~ /conn=(\d+) [ ] op=(\d+) [ ] UNBIND/mx )
864 | {
865 | my $conn = $1;
866 | storeOp( "$1,$2", "$line" );
867 |
868 | ### Increment the counters
869 | if ( defined $conns{$conn}
870 | && defined $hosts{ $conns{$conn} } )
871 | {
872 | $hosts{ $conns{$conn} }{UNBIND}++;
873 | $hours{$hour}{UNBIND}++;
874 | $days{$day}{UNBIND}++;
875 | $months{$month}{UNBIND}++;
876 | $stats{TOTAL_UNBIND}++;
877 | }
878 |
879 | ### Check the result of the last operation (log format 2.5 or 2.6)
880 | ### TODO: Add other err=X values from contrib/ldapc++/src/LDAPResult.h
881 | }
882 | elsif ( not $log24
883 | and $line =~
884 | /conn=(\d+) [ ] op=(\d+)(?: SEARCH)? [ ] RESULT [ ] .*qtime=([\d.]+) .* etime=([\d.]+)/mx
885 | )
886 | {
887 | my $conn = $1;
888 | my $op = $2;
889 | storeOp( "$1,$2", "$line" );
890 | my $qtime = $3;
891 | $qtime =~ tr/\.//d; # remove . => microsecond format
892 | my $etime = $4;
893 | $etime =~ tr/\.//d; # remove . => microsecond format
894 | $qtimes{"$conn,$op"} = $qtime;
895 | $etimes{"$conn,$op"} = $etime;
896 |
897 | if ( $line =~ /\berr=49\b/mx ) {
898 | ### Increment the counters
899 | if ( defined $conns{$conn}
900 | && defined $hosts{ $conns{$conn} } )
901 | {
902 | $hosts{ $conns{$conn} }{AUTHFAILURES}++;
903 | $hours{$hour}{AUTHFAILURES}++;
904 | $days{$day}{AUTHFAILURES}++;
905 | $months{$month}{AUTHFAILURES}++;
906 | $stats{TOTAL_AUTHFAILURES}++;
907 | }
908 | }
909 | ### Check the result of the last operation (log format 2.4)
910 | ### TODO: Add other err=X values from contrib/ldapc++/src/LDAPResult.h
911 | }
912 | elsif ( $log24
913 | and $line =~
914 | /conn=(\d+) [ ] op=(\d+)(?: SEARCH)? [ ] RESULT [ ]/mx )
915 | {
916 | my $conn = $1;
917 | my $op = $2;
918 | storeOp( "$1,$2", "$line" );
919 |
920 | if ( $line =~ /\berr=49\b/mx ) {
921 | ### Increment the counters
922 | if ( defined $conns{$conn}
923 | && defined $hosts{ $conns{$conn} } )
924 | {
925 | $hosts{ $conns{$conn} }{AUTHFAILURES}++;
926 | $hours{$hour}{AUTHFAILURES}++;
927 | $days{$day}{AUTHFAILURES}++;
928 | $months{$month}{AUTHFAILURES}++;
929 | $stats{TOTAL_AUTHFAILURES}++;
930 | }
931 | }
932 |
933 | ### Check the result of the last operation (log format SUNDS)
934 | ### TODO: Add other err=X values from contrib/ldapc++/src/LDAPResult.h
935 | }
936 | elsif ( $sunds
937 | and $line =~
938 | /conn=(\d+) op=(\d+) msgId=\d+ - RESULT err=\d+ tag=\d+ nentries=\d+ etime=([\d.]+)/m
939 | )
940 | {
941 | my $conn = $1;
942 | my $op = $2;
943 | storeOp( "$1,$2", "$line" );
944 | my $etime = $3;
945 | $etime =~ tr/\.//d; # remove . => microsecond format
946 | $etimes{"$conn,$op"} = $etime;
947 |
948 | if ( $line =~ /\berr=49\b/mx ) {
949 | ### Increment the counters
950 | if ( defined $conns{$conn}
951 | && defined $hosts{ $conns{$conn} } )
952 | {
953 | $hosts{ $conns{$conn} }{AUTHFAILURES}++;
954 | $hours{$hour}{AUTHFAILURES}++;
955 | $days{$day}{AUTHFAILURES}++;
956 | $months{$month}{AUTHFAILURES}++;
957 | $stats{TOTAL_AUTHFAILURES}++;
958 | }
959 | }
960 |
961 | ### Check for entry changes: add, modify modrdn, delete
962 | }
963 | elsif (
964 | (
965 | $sunds
966 | and $line =~
967 | /conn=(\d+) op=(\d+) msgId=\d+ - (ADD|CMP|MOD|MODRDN|DEL)/m
968 | )
969 | or $line =~
970 | /conn=(\d+) [ ] op=(\d+) [ ] (ADD|CMP|MOD|MODRDN|DEL) [ ] dn=/mx
971 | )
972 | {
973 | my $conn = $1;
974 | storeOp( "$1,$2", "$line" );
975 | my $type = $3;
976 |
977 | ### Increment the counters
978 | if ( defined $conns{$conn}
979 | && defined $hosts{ $conns{$conn} } )
980 | {
981 | $hosts{ $conns{$conn} }{$type}++;
982 | $hours{$hour}{$type}++;
983 | $days{$day}{$type}++;
984 | $months{$month}{$type}++;
985 | $stats{ 'TOTAL_' . $type }++;
986 | }
987 |
988 | ### Check for unindexed searches
989 | }
990 | elsif ( $line =~
991 | /: [ ] \(([a-zA-Z0-9\;\-]+)\) [ ] index_param [ ] failed/mx )
992 | {
993 | my $attr = $1;
994 |
995 | $unindexed{$attr}++;
996 | $stats{TOTAL_UNINDEXED}++;
997 | }
998 | $counter++;
999 | }
1000 | close LOGFILE;
1001 | }
1002 |
1003 | # Get min and max dates in all log files
1004 | # %sdates = { unix_timestamp => date_string_in_log }
1005 | my %sdates = map { str2time($_->{SDATE}) => $_->{SDATE} } values %logarray;
1006 | my %edates = map { str2time($_->{EDATE}) => $_->{EDATE} } values %logarray;
1007 | my $sdate = $sdates{min(keys %sdates)};
1008 | my $edate = $edates{max(keys %edates)};
1009 |
1010 | ###################################################################
1011 | ### Print a nice header with the logfiles and date ranges processed
1012 | ###################################################################
1013 | ## Please see file perltidy.ERR
1014 | print "\n\n"
1015 | . "Report Generated for $machine from " . $sdate . " to " . $edate . "\n"
1016 | . q{-} x ( 31 + length($sdate) + length($edate) + length($machine) ) . "\n";
1017 |
1018 | for my $logfile ( @orderedLogFiles ) {
1019 | if ( !-z $logfile ) {
1020 | printf "Processed \"$logfile\": %s - %s\n", $logarray{$logfile}{SDATE},
1021 | $logarray{$logfile}{EDATE};
1022 | }
1023 | else {
1024 | printf "Processed \"$logfile\": no data\n";
1025 | }
1026 | }
1027 |
1028 | #######################################
1029 | ### Print an overall report with totals
1030 | #######################################
1031 |
1032 | my $total_operations =
1033 | $stats{TOTAL_BIND} +
1034 | $stats{TOTAL_UNBIND} +
1035 | $stats{TOTAL_SRCH} +
1036 | $stats{TOTAL_MOD} +
1037 | $stats{TOTAL_ADD} +
1038 | $stats{TOTAL_MODRDN} +
1039 | $stats{TOTAL_DEL};
1040 |
1041 | print "\n\n" . "Operation totals\n" . "----------------\n";
1042 | printf "Total operations : %d\n", $total_operations;
1043 | printf "Total connections : %d\n", $stats{TOTAL_CONNECT};
1044 | printf "Total authentication failures : %d\n", $stats{TOTAL_AUTHFAILURES};
1045 | printf "Total binds : %d\n", $stats{TOTAL_BIND};
1046 | printf "Total unbinds : %d\n", $stats{TOTAL_UNBIND};
1047 | printf "Total searches : %d\n", $stats{TOTAL_SRCH};
1048 | printf "Total compares : %d\n", $stats{TOTAL_CMP};
1049 | printf "Total modifications : %d\n", $stats{TOTAL_MOD};
1050 | printf "Total modrdns : %d\n", $stats{TOTAL_MODRDN};
1051 | printf "Total additions : %d\n", $stats{TOTAL_ADD};
1052 | printf "Total deletions : %d\n", $stats{TOTAL_DEL};
1053 | printf "Unindexed attribute requests : %d\n", $stats{TOTAL_UNINDEXED};
1054 | printf "Operations per connection : %.2f\n",
1055 | $stats{TOTAL_CONNECT} ? $total_operations / $stats{TOTAL_CONNECT} : 0;
1056 |
1057 | if($csv)
1058 | {
1059 | my $CSV; # csv file handler
1060 | open($CSV, '>', $csv) or
1061 | printf STDERR "Error while trying to open file for writting: ".$!;
1062 |
1063 | # Print title
1064 | print $CSV "Operations on directory $machine from " . $sdate . " to " . $edate ."\n";
1065 |
1066 | # Print header
1067 | print $CSV "operations;connections;authentication failures;binds;unbinds;";
1068 | print $CSV "searches;compares;modifications;modrdns;additions;deletions\n";
1069 |
1070 | # Print operations stats
1071 | print $CSV $total_operations.";";
1072 | print $CSV $stats{TOTAL_CONNECT}.";";
1073 | print $CSV $stats{TOTAL_AUTHFAILURES}.";";
1074 | print $CSV $stats{TOTAL_BIND}.";";
1075 | print $CSV $stats{TOTAL_UNBIND}.";";
1076 | print $CSV $stats{TOTAL_SRCH}.";";
1077 | print $CSV $stats{TOTAL_CMP}.";";
1078 | print $CSV $stats{TOTAL_MOD}.";";
1079 | print $CSV $stats{TOTAL_MODRDN}.";";
1080 | print $CSV $stats{TOTAL_ADD}.";";
1081 | print $CSV $stats{TOTAL_DEL}."\n";
1082 |
1083 | close($CSV);
1084 |
1085 | }
1086 |
1087 | ###################################################
1088 | ### Process the host information and print a report
1089 | ###################################################
1090 | for my $selected (@operations) {
1091 | $selected = uc $selected;
1092 |
1093 | my $ops_ref = {
1094 | CONNECT => sub { $operations{CONNECT}{DATA} = 1 },
1095 | FAILURES => sub { $operations{FAILURES}{DATA} = 1 },
1096 | BIND => sub { $operations{BIND}{DATA} = 1 },
1097 | UNBIND => sub { $operations{UNBIND}{DATA} = 1 },
1098 | SRCH => sub { $operations{SRCH}{DATA} = 1 },
1099 | CMP => sub { $operations{CMP}{DATA} = 1 },
1100 | ADD => sub { $operations{ADD}{DATA} = 1 },
1101 | MOD => sub { $operations{MOD}{DATA} = 1 },
1102 | MODRDN => sub { $operations{MODRDN}{DATA} = 1 },
1103 | DEL => sub { $operations{DEL}{DATA} = 1 },
1104 | ALL => sub {
1105 | $operations{CONNECT}{DATA} = 1;
1106 | $operations{FAILURES}{DATA} = 1;
1107 | $operations{BIND}{DATA} = 1;
1108 | $operations{UNBIND}{DATA} = 1;
1109 | $operations{SRCH}{DATA} = 1;
1110 | $operations{CMP}{DATA} = 1;
1111 | $operations{ADD}{DATA} = 1;
1112 | $operations{MOD}{DATA} = 1;
1113 | $operations{MODRDN}{DATA} = 1;
1114 | $operations{DEL}{DATA} = 1;
1115 | },
1116 | READ => sub {
1117 | $operations{CONNECT}{DATA} = 1;
1118 | $operations{BIND}{DATA} = 1;
1119 | $operations{UNBIND}{DATA} = 1;
1120 | $operations{SRCH}{DATA} = 1;
1121 | $operations{CMP}{DATA} = 1;
1122 | },
1123 | WRITE => sub {
1124 | $operations{CONNECT}{DATA} = 1;
1125 | $operations{BIND}{DATA} = 1;
1126 | $operations{UNBIND}{DATA} = 1;
1127 | $operations{ADD}{DATA} = 1;
1128 | $operations{MOD}{DATA} = 1;
1129 | $operations{MODRDN}{DATA} = 1;
1130 | $operations{DEL}{DATA} = 1;
1131 | },
1132 | };
1133 | if ( $ops_ref->{$selected} ) { $ops_ref->{$selected}->() }
1134 | else { croak "Unknown operation: '$selected';\n" }
1135 | }
1136 |
1137 | print "\n\n";
1138 | my $printstr = 'Hostname ';
1139 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{STRING} : q{};
1140 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{STRING} : q{};
1141 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{STRING} : q{};
1142 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{STRING} : q{};
1143 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{STRING} : q{};
1144 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{STRING} : q{};
1145 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{STRING} : q{};
1146 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{STRING} : q{};
1147 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{STRING} : q{};
1148 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{STRING} : q{};
1149 | $printstr .= "\n";
1150 | print $printstr;
1151 | $printstr = '---------------';
1152 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{SPACING} : q{};
1153 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{SPACING} : q{};
1154 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{SPACING} : q{};
1155 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{SPACING} : q{};
1156 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{SPACING} : q{};
1157 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{SPACING} : q{};
1158 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{SPACING} : q{};
1159 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{SPACING} : q{};
1160 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{SPACING} : q{};
1161 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{SPACING} : q{};
1162 | print "$printstr\n";
1163 |
1164 | for my $index ( sort keys %hosts ) {
1165 |
1166 | ### Resolve IP addresses to names if requested
1167 | my $host = $index;
1168 |
1169 | ### Convert the IP address to an Internet address, and resolve with gethostbyaddr()
1170 | if ( $resolvename && ( $index =~ /\d+\.\d+\.\d+\.\d+/mx ) ) {
1171 | my $ipaddr = inet_aton($index);
1172 | $host = gethostbyaddr $ipaddr, AF_INET;
1173 | if ( !defined $host ) {
1174 | $host = $index;
1175 | }
1176 | }
1177 | printf '%-15.15s', $host;
1178 | if ( $operations{CONNECT}{DATA} ) {
1179 | printf " $operations{CONNECT}{FIELD}",
1180 | $hosts{$index}{CONNECT} ? $hosts{$index}{CONNECT} : 0;
1181 | }
1182 | if ( $operations{FAILURES}{DATA} ) {
1183 | printf " $operations{FAILURES}{FIELD}",
1184 | $hosts{$index}{AUTHFAILURES} ? $hosts{$index}{AUTHFAILURES} : 0;
1185 | }
1186 | if ( $operations{BIND}{DATA} ) {
1187 | printf " $operations{BIND}{FIELD}",
1188 | $hosts{$index}{BIND} ? $hosts{$index}{BIND} : 0;
1189 | }
1190 | if ( $operations{UNBIND}{DATA} ) {
1191 | printf " $operations{UNBIND}{FIELD}",
1192 | $hosts{$index}{UNBIND} ? $hosts{$index}{UNBIND} : 0;
1193 | }
1194 | if ( $operations{SRCH}{DATA} ) {
1195 | printf " $operations{SRCH}{FIELD}",
1196 | $hosts{$index}{SRCH} ? $hosts{$index}{SRCH} : 0;
1197 | }
1198 | if ( $operations{CMP}{DATA} ) {
1199 | printf " $operations{CMP}{FIELD}",
1200 | $hosts{$index}{CMP} ? $hosts{$index}{CMP} : 0;
1201 | }
1202 | if ( $operations{ADD}{DATA} ) {
1203 | printf " $operations{ADD}{FIELD}",
1204 | $hosts{$index}{ADD} ? $hosts{$index}{ADD} : 0;
1205 | }
1206 | if ( $operations{MOD}{DATA} ) {
1207 | printf " $operations{MOD}{FIELD}",
1208 | $hosts{$index}{MOD} ? $hosts{$index}{MOD} : 0;
1209 | }
1210 | if ( $operations{MODRDN}{DATA} ) {
1211 | printf " $operations{MODRDN}{FIELD}",
1212 | $hosts{$index}{MODRDN} ? $hosts{$index}{MODRDN} : 0;
1213 | }
1214 | if ( $operations{DEL}{DATA} ) {
1215 | printf " $operations{DEL}{FIELD}",
1216 | $hosts{$index}{DEL} ? $hosts{$index}{DEL} : 0;
1217 | }
1218 | print "\n";
1219 | }
1220 |
1221 | #######################################################
1222 | ### Process the hours information and print a report
1223 | ########################################################
1224 | print "\n\n";
1225 | $printstr = 'Hour of Day ';
1226 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{STRING} : q{};
1227 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{STRING} : q{};
1228 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{STRING} : q{};
1229 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{STRING} : q{};
1230 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{STRING} : q{};
1231 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{STRING} : q{};
1232 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{STRING} : q{};
1233 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{STRING} : q{};
1234 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{STRING} : q{};
1235 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{STRING} : q{};
1236 | $printstr .= "\n";
1237 | print $printstr;
1238 | $printstr = '-------------';
1239 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{SPACING} : q{};
1240 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{SPACING} : q{};
1241 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{SPACING} : q{};
1242 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{SPACING} : q{};
1243 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{SPACING} : q{};
1244 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{SPACING} : q{};
1245 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{SPACING} : q{};
1246 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{SPACING} : q{};
1247 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{SPACING} : q{};
1248 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{SPACING} : q{};
1249 | print "$printstr\n";
1250 |
1251 | for my $index ( sort keys %hours ) {
1252 | printf '%-2s:00 - %2s:59', $index, $index;
1253 | if ( $operations{CONNECT}{DATA} ) {
1254 | printf " $operations{CONNECT}{FIELD}",
1255 | $hours{$index}{CONNECT} ? $hours{$index}{CONNECT} : 0;
1256 | }
1257 | if ( $operations{FAILURES}{DATA} ) {
1258 | printf " $operations{FAILURES}{FIELD}",
1259 | $hours{$index}{AUTHFAILURES} ? $hours{$index}{AUTHFAILURES} : 0;
1260 | }
1261 | if ( $operations{BIND}{DATA} ) {
1262 | printf " $operations{BIND}{FIELD}",
1263 | $hours{$index}{BIND} ? $hours{$index}{BIND} : 0;
1264 | }
1265 | if ( $operations{UNBIND}{DATA} ) {
1266 | printf " $operations{UNBIND}{FIELD}",
1267 | $hours{$index}{UNBIND} ? $hours{$index}{UNBIND} : 0;
1268 | }
1269 | if ( $operations{SRCH}{DATA} ) {
1270 | printf " $operations{SRCH}{FIELD}",
1271 | $hours{$index}{SRCH} ? $hours{$index}{SRCH} : 0;
1272 | }
1273 | if ( $operations{CMP}{DATA} ) {
1274 | printf " $operations{CMP}{FIELD}",
1275 | $hours{$index}{CMP} ? $hours{$index}{CMP} : 0;
1276 | }
1277 | if ( $operations{ADD}{DATA} ) {
1278 | printf " $operations{ADD}{FIELD}",
1279 | $hours{$index}{ADD} ? $hours{$index}{ADD} : 0;
1280 | }
1281 | if ( $operations{MOD}{DATA} ) {
1282 | printf " $operations{MOD}{FIELD}",
1283 | $hours{$index}{MOD} ? $hours{$index}{MOD} : 0;
1284 | }
1285 | if ( $operations{MODRDN}{DATA} ) {
1286 | printf " $operations{MODRDN}{FIELD}",
1287 | $hours{$index}{MODRDN} ? $hours{$index}{MODRDN} : 0;
1288 | }
1289 | if ( $operations{DEL}{DATA} ) {
1290 | printf " $operations{DEL}{FIELD}",
1291 | $hours{$index}{DEL} ? $hours{$index}{DEL} : 0;
1292 | }
1293 | print "\n";
1294 | }
1295 |
1296 | #######################################################
1297 | ### Process the month information and print a report
1298 | ########################################################
1299 | print "\n\n";
1300 | $printstr = 'Day of Month ';
1301 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{STRING} : q{};
1302 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{STRING} : q{};
1303 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{STRING} : q{};
1304 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{STRING} : q{};
1305 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{STRING} : q{};
1306 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{STRING} : q{};
1307 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{STRING} : q{};
1308 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{STRING} : q{};
1309 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{STRING} : q{};
1310 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{STRING} : q{};
1311 | $printstr .= "\n";
1312 | print $printstr;
1313 | $printstr = '-------------';
1314 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{SPACING} : q{};
1315 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{SPACING} : q{};
1316 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{SPACING} : q{};
1317 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{SPACING} : q{};
1318 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{SPACING} : q{};
1319 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{SPACING} : q{};
1320 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{SPACING} : q{};
1321 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{SPACING} : q{};
1322 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{SPACING} : q{};
1323 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{SPACING} : q{};
1324 | print "$printstr\n";
1325 |
1326 | for ( 1 .. 31 ) {
1327 | if ( defined $days{$_} || $printdays ) {
1328 | printf ' %-11s', $_;
1329 | if ( $operations{CONNECT}{DATA} ) {
1330 | printf " $operations{CONNECT}{FIELD}",
1331 | $days{$_}{CONNECT} ? $days{$_}{CONNECT} : 0;
1332 | }
1333 | if ( $operations{FAILURES}{DATA} ) {
1334 | printf " $operations{FAILURES}{FIELD}",
1335 | $days{$_}{AUTHFAILURES} ? $days{$_}{AUTHFAILURES} : 0;
1336 | }
1337 | if ( $operations{BIND}{DATA} ) {
1338 | printf " $operations{BIND}{FIELD}",
1339 | $days{$_}{BIND} ? $days{$_}{BIND} : 0;
1340 | }
1341 | if ( $operations{UNBIND}{DATA} ) {
1342 | printf " $operations{UNBIND}{FIELD}",
1343 | $days{$_}{UNBIND} ? $days{$_}{UNBIND} : 0;
1344 | }
1345 | if ( $operations{SRCH}{DATA} ) {
1346 | printf " $operations{SRCH}{FIELD}",
1347 | $days{$_}{SRCH} ? $days{$_}{SRCH} : 0;
1348 | }
1349 | if ( $operations{CMP}{DATA} ) {
1350 | printf " $operations{CMP}{FIELD}",
1351 | $days{$_}{CMP} ? $days{$_}{CMP} : 0;
1352 | }
1353 | if ( $operations{ADD}{DATA} ) {
1354 | printf " $operations{ADD}{FIELD}",
1355 | $days{$_}{ADD} ? $days{$_}{ADD} : 0;
1356 | }
1357 | if ( $operations{MOD}{DATA} ) {
1358 | printf " $operations{MOD}{FIELD}",
1359 | $days{$_}{MOD} ? $days{$_}{MOD} : 0;
1360 | }
1361 | if ( $operations{MODRDN}{DATA} ) {
1362 | printf " $operations{MODRDN}{FIELD}",
1363 | $days{$_}{MODRDN} ? $days{$_}{MODRDN} : 0;
1364 | }
1365 | if ( $operations{DEL}{DATA} ) {
1366 | printf " $operations{DEL}{FIELD}",
1367 | $days{$_}{DEL} ? $days{$_}{DEL} : 0;
1368 | }
1369 | print "\n";
1370 | }
1371 | }
1372 | #######################################################
1373 | ### Process the month information and print a report
1374 | ########################################################
1375 | print "\n\n";
1376 | $printstr = ' Month ';
1377 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{STRING} : q{};
1378 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{STRING} : q{};
1379 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{STRING} : q{};
1380 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{STRING} : q{};
1381 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{STRING} : q{};
1382 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{STRING} : q{};
1383 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{STRING} : q{};
1384 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{STRING} : q{};
1385 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{STRING} : q{};
1386 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{STRING} : q{};
1387 | $printstr .= "\n";
1388 | print $printstr;
1389 | $printstr = '-------------';
1390 | $printstr .= $operations{CONNECT}{DATA} ? $operations{CONNECT}{SPACING} : q{};
1391 | $printstr .= $operations{FAILURES}{DATA} ? $operations{FAILURES}{SPACING} : q{};
1392 | $printstr .= $operations{BIND}{DATA} ? $operations{BIND}{SPACING} : q{};
1393 | $printstr .= $operations{UNBIND}{DATA} ? $operations{UNBIND}{SPACING} : q{};
1394 | $printstr .= $operations{SRCH}{DATA} ? $operations{SRCH}{SPACING} : q{};
1395 | $printstr .= $operations{CMP}{DATA} ? $operations{CMP}{SPACING} : q{};
1396 | $printstr .= $operations{ADD}{DATA} ? $operations{ADD}{SPACING} : q{};
1397 | $printstr .= $operations{MOD}{DATA} ? $operations{MOD}{SPACING} : q{};
1398 | $printstr .= $operations{MODRDN}{DATA} ? $operations{MODRDN}{SPACING} : q{};
1399 | $printstr .= $operations{DEL}{DATA} ? $operations{DEL}{SPACING} : q{};
1400 | print "$printstr\n";
1401 |
1402 | my $month_table;
1403 | if ($dateformat) {
1404 | $month_table = [qw(01 02 03 04 05 06 07 08 09 10 11 12)];
1405 | }
1406 | else {
1407 | $month_table = [qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)];
1408 | }
1409 |
1410 | for my $index (@$month_table) {
1411 | if ( defined $months{$index} || $printmonths ) {
1412 | printf ' %-11s', $index;
1413 | if ( $operations{CONNECT}{DATA} ) {
1414 | printf " $operations{CONNECT}{FIELD}",
1415 | $months{$index}{CONNECT} ? $months{$index}{CONNECT} : 0;
1416 | }
1417 | if ( $operations{FAILURES}{DATA} ) {
1418 | printf " $operations{FAILURES}{FIELD}",
1419 | $months{$index}{AUTHFAILURES}
1420 | ? $months{$index}{AUTHFAILURES}
1421 | : 0;
1422 | }
1423 | if ( $operations{BIND}{DATA} ) {
1424 | printf " $operations{BIND}{FIELD}",
1425 | $months{$index}{BIND} ? $months{$index}{BIND} : 0;
1426 | }
1427 | if ( $operations{UNBIND}{DATA} ) {
1428 | printf " $operations{UNBIND}{FIELD}",
1429 | $months{$index}{UNBIND} ? $months{$index}{UNBIND} : 0;
1430 | }
1431 | if ( $operations{SRCH}{DATA} ) {
1432 | printf " $operations{SRCH}{FIELD}",
1433 | $months{$index}{SRCH} ? $months{$index}{SRCH} : 0;
1434 | }
1435 | if ( $operations{CMP}{DATA} ) {
1436 | printf " $operations{CMP}{FIELD}",
1437 | $months{$index}{CMP} ? $months{$index}{CMP} : 0;
1438 | }
1439 | if ( $operations{ADD}{DATA} ) {
1440 | printf " $operations{ADD}{FIELD}",
1441 | $months{$index}{ADD} ? $months{$index}{ADD} : 0;
1442 | }
1443 | if ( $operations{MOD}{DATA} ) {
1444 | printf " $operations{MOD}{FIELD}",
1445 | $months{$index}{MOD} ? $months{$index}{MOD} : 0;
1446 | }
1447 | if ( $operations{MODRDN}{DATA} ) {
1448 | printf " $operations{MODRDN}{FIELD}",
1449 | $months{$index}{MODRDN} ? $months{$index}{MODRDN} : 0;
1450 | }
1451 | if ( $operations{DEL}{DATA} ) {
1452 | printf " $operations{DEL}{FIELD}",
1453 | $months{$index}{DEL} ? $months{$index}{DEL} : 0;
1454 | }
1455 | print "\n";
1456 | }
1457 | }
1458 |
1459 | ####################################################
1460 | ### Process the unindexed searches and print a report
1461 | ####################################################
1462 | my @sarray; # sort array
1463 | if ( $stats{TOTAL_UNINDEXED} > 0 ) {
1464 |
1465 | print "\n\n"
1466 | . "# Uses Unindexed attribute\n"
1467 | . "---------- -----------------------------------------------------------\n";
1468 |
1469 | @sarray =
1470 | reverse sort { $unindexed{$a} <=> $unindexed{$b} } keys %unindexed;
1471 | UNINDEXED:
1472 | for my $num ( 0 .. $#sarray ) {
1473 | if ( $num > $count ) {
1474 | last UNINDEXED;
1475 | }
1476 | printf " %-8d %-60s\n", $unindexed{ $sarray[$num] }, $sarray[$num];
1477 | }
1478 | }
1479 |
1480 | ######################################################
1481 | ### Process the stored search bases and print a report
1482 | ######################################################
1483 | print "\n\n"
1484 | . "# Searches Search base\n"
1485 | . "---------- -----------------------------------------------------------\n";
1486 |
1487 | @sarray = reverse sort { $search{$a} <=> $search{$b} } keys %search;
1488 | SEARCH:
1489 | for my $num ( 0 .. $#sarray ) {
1490 | if ( $num > $count ) {
1491 | last SEARCH;
1492 | }
1493 | printf " %-8d %-60s\n", $search{ $sarray[$num] },
1494 | $sarray[$num] || 'RootDSE';
1495 | }
1496 |
1497 | ######################################################
1498 | ### Process the stored search filters
1499 | ######################################################
1500 | print "\n\n"
1501 | . "# Uses Filter\n"
1502 | . "---------- -----------------------------------------------------------\n";
1503 |
1504 | @sarray = reverse sort { $filters{$a} <=> $filters{$b} } keys %filters;
1505 | FILTER:
1506 | for my $num ( 0 .. $#sarray ) {
1507 | if ( $num > $count ) {
1508 | last FILTER;
1509 | }
1510 | printf " %-8d %-60s\n", $filters{ $sarray[$num] }, $sarray[$num];
1511 | }
1512 |
1513 | ######################################################
1514 | ### Process the stored attribute array
1515 | ######################################################
1516 | print "\n\n"
1517 | . "# Uses Attributes explicitly requested in search string\n"
1518 | . "---------- -------------------------------------------------\n";
1519 |
1520 | @sarray =
1521 | reverse sort { $searchattributes{$a} <=> $searchattributes{$b} }
1522 | keys %searchattributes;
1523 | SEARCHATTR:
1524 | for my $num ( 0 .. $#sarray ) {
1525 | if ( $num > $count ) {
1526 | last SEARCHATTR;
1527 | }
1528 | printf " %-8d %-60s\n", $searchattributes{ $sarray[$num] },
1529 | $sarray[$num];
1530 | }
1531 |
1532 | ######################################################
1533 | ### Process the stored binddns and print a report
1534 | ######################################################
1535 | print "\n\n"
1536 | . "# Binds Bind DN\n"
1537 | . "---------- --------------------------------------------------------------\n";
1538 |
1539 | @sarray = reverse sort { $binddns{$a} <=> $binddns{$b} } keys %binddns;
1540 | BINDDN:
1541 | for my $num ( 0 .. $#sarray ) {
1542 | if ( $num > $count ) {
1543 | last BINDDN;
1544 | }
1545 | printf " %-8d %-60s\n", $binddns{ $sarray[$num] }, $sarray[$num];
1546 | }
1547 |
1548 | ###################################################
1549 | ### Process greater qtimes and etimes
1550 | ###################################################
1551 |
1552 | unless ($log24) {
1553 | unless ($sunds) {
1554 | print "\n\n"
1555 | . "# qtime (s) Operation\n"
1556 | . "------------ --------------------------------------------------------------\n";
1557 |
1558 | # sort qtimes by their value (descending) and only select the n first ones
1559 | my %greater_qtimes =
1560 | map { $_ => $qtimes{$_} }
1561 | ( sort { $qtimes{$b} <=> $qtimes{$a} } keys %qtimes )
1562 | [ 0 .. $max_qtimes ];
1563 |
1564 | # for each greater qtime (from the greater to the lower)
1565 | foreach
1566 | my $connop ( sort { $greater_qtimes{$b} <=> $greater_qtimes{$a} }
1567 | keys %greater_qtimes )
1568 | {
1569 | # format time from µs (123456789) to s (123.456789)
1570 | my $qt = substr( $greater_qtimes{$connop}, 0, -6 ) . '.'
1571 | . substr( $greater_qtimes{$connop}, -6 );
1572 |
1573 | # if we find some associated operation(s) display them
1574 | if ( $ops{"$connop"} ) {
1575 | printf " %-12s %s\n", $qt, $ops{"$connop"};
1576 | }
1577 |
1578 | # else, just display conn + op
1579 | else {
1580 | printf " %-12s %s\n", $qt,
1581 | "operation not found (conn,op) = (" . $connop . ")";
1582 | }
1583 | }
1584 | }
1585 |
1586 | print "\n\n"
1587 | . "# etime (s) Operation\n"
1588 | . "------------ --------------------------------------------------------------\n";
1589 |
1590 | # sort etimes by their value (descending) and only select the n first ones
1591 | my %greater_etimes = map { $_ => $etimes{$_} }
1592 | ( sort { $etimes{$b} <=> $etimes{$a} } keys %etimes )[ 0 .. $max_etimes ];
1593 |
1594 | # for each greater etime (from the greater to the lower)
1595 | foreach my $connop ( sort { $greater_etimes{$b} <=> $greater_etimes{$a} }
1596 | keys %greater_etimes )
1597 | {
1598 | # format time from µs (123456789) to s (123.456789)
1599 | my $et = substr( $greater_etimes{$connop}, 0, -6 ) . '.'
1600 | . substr( $greater_etimes{$connop}, -6 );
1601 |
1602 | # if we find some associated operation(s) display them
1603 | if ( $ops{"$connop"} ) {
1604 | printf " %-12s %s\n", $et, $ops{"$connop"};
1605 | }
1606 |
1607 | # else, just display conn + op
1608 | else {
1609 | printf " %-12s %s\n", $et,
1610 | "operation not found (conn,op) = (" . $connop . ")";
1611 | }
1612 | }
1613 | }
1614 |
1615 | print "\n\n";
1616 |
1617 | # EOF
1618 |
--------------------------------------------------------------------------------
/spreadPwdChangedTime.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use DateTime;
7 | use Net::LDAP;
8 | use Net::LDAP::Control;
9 | use Net::LDAP::Constant qw( LDAP_CONTROL_RELAX );
10 | use Getopt::Long;
11 |
12 | ################################################################################
13 | # Variables
14 | ################################################################################
15 |
16 | my $force;
17 | my $verbose;
18 | my $help;
19 |
20 | GetOptions ( "force|f" => \$force,
21 | "verbose|v" => \$verbose,
22 | "help|h" => \$help
23 | )
24 | or &usage();
25 |
26 | my (
27 | $uri,
28 | $base,
29 | $filter,
30 | $binddn,
31 | $bindpw,
32 | $min,
33 | $max
34 | ) = @ARGV;
35 |
36 |
37 | my $users; # { dn => { replace => { "pwdChangedTime" => "20250101120000Z" } } }
38 |
39 | ################################################################################
40 | # Functions
41 | ################################################################################
42 |
43 |
44 | sub usage
45 | {
46 | print "Missing or bad argument\n";
47 | print "USAGE: $0 [-h] [-f] [-v] \n";
48 | print "DESCRIPTION: find users and spread homogeneously their pwdChangedTime from min to max days ago\n";
49 | print " * -f: option to force applying modifications of pwdChangedTime\n";
50 | print " * -h: display this help message\n";
51 | print " * -v: verbose mode\n";
52 | print " * uri: FQDN or LDAP uri, like ldap://host.domain.com or ldaps://host.domain.com\n";
53 | print " * base: LDAP search base\n";
54 | print " * filter: LDAP filter for selecting users\n";
55 | print " * binddn: service account that binds for searching users and modifying pwdChangedTime\n";
56 | print " * bindpw: password for service account\n";
57 | print " * min: change password from min days ago\n";
58 | print " * max: change password up to max days ago\n";
59 | exit 1;
60 | }
61 |
62 |
63 | sub get_users_dn
64 | {
65 | my ($uri, $base, $filter, $binddn, $bindpw) = @_;
66 |
67 | my $result;
68 |
69 | my $ldap = Net::LDAP->new( $uri )
70 | or die "Unable to connect to LDAP server $uri: $@";
71 |
72 | my $bind_result = $ldap->bind( "$binddn",
73 | password => "$bindpw" );
74 |
75 | $bind_result->code and die $bind_result->error;
76 |
77 | my $search_result = $ldap->search(
78 | base => $base,
79 | filter => $filter,
80 | scope => "sub",
81 | attrs => [ 'pwdChangedTime' ]
82 | );
83 |
84 | $search_result->code and die $search_result->error;
85 |
86 | foreach my $entry ($search_result->entries)
87 | {
88 | $result->{$entry->dn} = {};
89 | }
90 |
91 | $ldap->unbind;
92 |
93 | return $result;
94 | }
95 |
96 | sub compute_pwd_changed_time
97 | {
98 |
99 | my ( $users, $min, $max ) = @_;
100 |
101 | my $now = DateTime->now;
102 | my $date;
103 |
104 | my $days = $min;
105 | foreach my $user ( keys %$users )
106 | {
107 |
108 | $date = $now->clone();
109 | $date->subtract(days => $days);
110 | #print "days: $days " . $date->strftime('%Y%m%d%H%M%SZ')."\n";
111 |
112 | $users->{$user} = { replace => { "pwdChangedTime" => $date->strftime('%Y%m%d000000Z') } };
113 | if( $days < $max)
114 | {
115 | $days++;
116 | }
117 | else
118 | {
119 | $days = $min;
120 | }
121 | }
122 |
123 | return $users;
124 | }
125 |
126 | sub display_modifications
127 | {
128 | my ( $users ) = @_;
129 |
130 | print "\nModifications to apply to LDAP directory\n";
131 | print "----------------------------------------\n";
132 | foreach my $user ( keys %$users )
133 | {
134 | my $date = $users->{$user}->{replace}->{pwdChangedTime};
135 | $date =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z/$1-$2-$3 $4:$5:$6/;
136 | print sprintf "User: %-64s pwdChangedTime: %s\n", "$user", "$date";
137 | }
138 |
139 | }
140 |
141 |
142 | sub apply_modifications
143 | {
144 |
145 | my ($uri, $base, $filter, $binddn, $bindpw, $users) = @_;
146 |
147 | my $ldap = Net::LDAP->new( $uri )
148 | or die "Unable to connect to LDAP server $uri: $@";
149 |
150 | my $bind_result = $ldap->bind( "$binddn",
151 | password => "$bindpw" );
152 |
153 | $bind_result->code and die $bind_result->error;
154 |
155 | my $relax_control = Net::LDAP::Control->new( type => LDAP_CONTROL_RELAX );
156 | my $mod;
157 |
158 | foreach my $user ( keys %$users )
159 | {
160 | $mod = $ldap->modify( $user, %{ $users->{$user} }, control => [ $relax_control ] );
161 | $mod->code and die "Error while modifying $user: " . $mod->error;
162 | }
163 |
164 | $ldap->unbind;
165 | }
166 |
167 |
168 | ################################################################################
169 | # Entry point
170 | ################################################################################
171 |
172 | if( $help or !$uri or !$base or !$filter or !$binddn or !$bindpw or !$min or !$max )
173 | {
174 | &usage();
175 | }
176 |
177 | unless( $min =~ /^\d+$/ )
178 | {
179 | print "min: $min is not an integer\n";
180 | exit 1;
181 | }
182 |
183 | unless( $max =~ /^\d+$/ )
184 | {
185 | print "max: $max is not an integer\n";
186 | exit 1;
187 | }
188 |
189 | $users = &get_users_dn($uri, $base, $filter, $binddn, $bindpw);
190 |
191 | if(! keys %$users)
192 | {
193 | print "No users found, aborting\n";
194 | exit 2;
195 | }
196 |
197 | $users = &compute_pwd_changed_time( $users, $min, $max );
198 |
199 | if($verbose)
200 | {
201 | &display_modifications( $users );
202 | }
203 |
204 | print "\nNumber of modifications to apply: ".scalar(keys %$users)."\n";
205 |
206 | if($force)
207 | {
208 | &apply_modifications($uri, $base, $filter, $binddn, $bindpw, $users);
209 | print "Modifications successfully applied\n";
210 | }
211 | else
212 | {
213 | print "Modifications not applied (use -f if you want to)\n";
214 | }
215 |
216 | exit 0;
217 |
--------------------------------------------------------------------------------